Compare commits
No commits in common. "master" and "pregitcleanup" have entirely different histories.
master
...
pregitclea
2
.env
2
.env
@ -16,7 +16,7 @@
|
||||
|
||||
###> symfony/framework-bundle ###
|
||||
APP_ENV=dev
|
||||
APP_SECRET=
|
||||
APP_SECRET=a617c2ab616c5688ff5b0e95ad646641
|
||||
###< symfony/framework-bundle ###
|
||||
|
||||
###> doctrine/doctrine-bundle ###
|
||||
|
4
.env.dev
4
.env.dev
@ -1,4 +0,0 @@
|
||||
|
||||
###> symfony/framework-bundle ###
|
||||
APP_SECRET=452d8323de922537717fb88b5fa6f80e
|
||||
###< symfony/framework-bundle ###
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -8,6 +8,4 @@
|
||||
/var/
|
||||
/vendor/
|
||||
###< symfony/framework-bundle ###
|
||||
|
||||
release.json
|
||||
http-client.private.env.json
|
19
.http
19
.http
@ -1,19 +0,0 @@
|
||||
### api me
|
||||
GET {{host}}/api/me
|
||||
Accept: application/json
|
||||
X-AUTH-TOKEN: {{apiKey}}
|
||||
|
||||
### api snip get
|
||||
GET {{host}}/api/snip/22
|
||||
Accept: application/json
|
||||
X-AUTH-TOKEN: {{apiKey}}
|
||||
|
||||
### api snip post edit
|
||||
POST {{host}}/api/snip/22
|
||||
Content-Type: application/json
|
||||
X-AUTH-TOKEN: {{apiKey}}
|
||||
|
||||
{
|
||||
"name": "new snip name",
|
||||
"content": "new snip content"
|
||||
}
|
@ -4,10 +4,6 @@
|
||||
use App\Kernel;
|
||||
use Symfony\Bundle\FrameworkBundle\Console\Application;
|
||||
|
||||
if (!is_dir(dirname(__DIR__).'/vendor')) {
|
||||
throw new LogicException('Dependencies are missing. Try running "composer install".');
|
||||
}
|
||||
|
||||
if (!is_file(dirname(__DIR__).'/vendor/autoload_runtime.php')) {
|
||||
throw new LogicException('Symfony Runtime is missing. Try running "composer require symfony/runtime".');
|
||||
}
|
||||
|
@ -4,40 +4,35 @@
|
||||
"minimum-stability": "stable",
|
||||
"prefer-stable": true,
|
||||
"require": {
|
||||
"php": ">=8.4",
|
||||
"php": ">=8.3",
|
||||
"ext-ctype": "*",
|
||||
"ext-iconv": "*",
|
||||
"czproject/git-php": "^4.1",
|
||||
"doctrine/doctrine-bundle": "^2.9",
|
||||
"doctrine/doctrine-migrations-bundle": "^3.2",
|
||||
"doctrine/orm": "^2.14",
|
||||
"league/commonmark": "^2.6",
|
||||
"league/pipeline": "^1.0",
|
||||
"phpdocumentor/reflection-docblock": "^5.6",
|
||||
"phpstan/phpdoc-parser": "^2.1",
|
||||
"symfony/console": "7.2.*",
|
||||
"symfony/dotenv": "7.2.*",
|
||||
"symfony/console": "7.0.*",
|
||||
"symfony/dotenv": "7.0.*",
|
||||
"symfony/flex": "^2",
|
||||
"symfony/form": "7.2.*",
|
||||
"symfony/framework-bundle": "7.2.*",
|
||||
"symfony/form": "7.0.*",
|
||||
"symfony/framework-bundle": "7.0.*",
|
||||
"symfony/monolog-bundle": "^3.0",
|
||||
"symfony/property-access": "7.2.*",
|
||||
"symfony/property-info": "7.2.*",
|
||||
"symfony/runtime": "7.2.*",
|
||||
"symfony/security-bundle": "7.2.*",
|
||||
"symfony/serializer": "7.2.*",
|
||||
"symfony/twig-bundle": "7.2.*",
|
||||
"symfony/uid": "7.2.*",
|
||||
"symfony/validator": "7.2.*",
|
||||
"symfony/yaml": "7.2.*",
|
||||
"symfony/runtime": "7.0.*",
|
||||
"symfony/security-bundle": "7.0.*",
|
||||
"symfony/twig-bundle": "7.0.*",
|
||||
"symfony/uid": "7.0.*",
|
||||
"symfony/validator": "7.0.*",
|
||||
"symfony/yaml": "7.0.*",
|
||||
"twig/extra-bundle": "^2.12|^3.0",
|
||||
"twig/twig": "^3.0"
|
||||
"twig/twig": "^2.12|^3.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"deployer/deployer": "^7.3",
|
||||
"symfony/debug-bundle": "7.2.*",
|
||||
"symfony/debug-bundle": "7.0.*",
|
||||
"symfony/maker-bundle": "^1.48",
|
||||
"symfony/stopwatch": "7.2.*",
|
||||
"symfony/web-profiler-bundle": "7.2.*"
|
||||
"symfony/stopwatch": "7.0.*",
|
||||
"symfony/web-profiler-bundle": "7.0.*"
|
||||
},
|
||||
"config": {
|
||||
"allow-plugins": {
|
||||
@ -52,6 +47,11 @@
|
||||
"App\\": "src/"
|
||||
}
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"App\\Tests\\": "tests/"
|
||||
}
|
||||
},
|
||||
"replace": {
|
||||
"symfony/polyfill-ctype": "*",
|
||||
"symfony/polyfill-iconv": "*",
|
||||
@ -79,7 +79,7 @@
|
||||
"extra": {
|
||||
"symfony": {
|
||||
"allow-contrib": false,
|
||||
"require": "7.2.*"
|
||||
"require": "7.0.*"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
2338
composer.lock
generated
2338
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@ -1,11 +0,0 @@
|
||||
# Enable stateless CSRF protection for forms and logins/logouts
|
||||
framework:
|
||||
form:
|
||||
csrf_protection:
|
||||
token_id: submit
|
||||
|
||||
csrf_protection:
|
||||
stateless_token_ids:
|
||||
- submit
|
||||
- authenticate
|
||||
- logout
|
@ -4,28 +4,18 @@ doctrine:
|
||||
|
||||
# IMPORTANT: You MUST configure your server version,
|
||||
# either here or in the DATABASE_URL env var (see .env file)
|
||||
#server_version: '16'
|
||||
|
||||
profiling_collect_backtrace: '%kernel.debug%'
|
||||
use_savepoints: true
|
||||
#server_version: '15'
|
||||
orm:
|
||||
report_fields_where_declared: true
|
||||
auto_generate_proxy_classes: true
|
||||
enable_lazy_ghost_objects: true
|
||||
validate_xml_mapping: true
|
||||
naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware
|
||||
identity_generation_preferences:
|
||||
Doctrine\DBAL\Platforms\PostgreSQLPlatform: identity
|
||||
auto_mapping: true
|
||||
mappings:
|
||||
App:
|
||||
type: attribute
|
||||
is_bundle: false
|
||||
dir: '%kernel.project_dir%/src/Entity'
|
||||
prefix: 'App\Entity'
|
||||
alias: App
|
||||
controller_resolver:
|
||||
auto_mapping: false
|
||||
|
||||
when@test:
|
||||
doctrine:
|
||||
|
@ -1,12 +1,22 @@
|
||||
# see https://symfony.com/doc/current/reference/configuration/framework.html
|
||||
framework:
|
||||
secret: '%env(APP_SECRET)%'
|
||||
#csrf_protection: true
|
||||
http_method_override: false
|
||||
handle_all_throwables: true
|
||||
|
||||
# Note that the session will be started ONLY if you read or write from it.
|
||||
session: true
|
||||
# Enables session support. Note that the session will ONLY be started if you read or write from it.
|
||||
# Remove or comment this section to explicitly disable session support.
|
||||
session:
|
||||
handler_id: null
|
||||
cookie_secure: auto
|
||||
cookie_samesite: lax
|
||||
storage_factory_id: session.storage.factory.native
|
||||
|
||||
#esi: true
|
||||
#fragments: true
|
||||
php_errors:
|
||||
log: true
|
||||
|
||||
when@test:
|
||||
framework:
|
||||
|
@ -59,4 +59,3 @@ when@prod:
|
||||
type: stream
|
||||
channels: [deprecation]
|
||||
path: php://stderr
|
||||
formatter: monolog.formatter.json
|
||||
|
@ -1,5 +1,7 @@
|
||||
framework:
|
||||
router:
|
||||
utf8: true
|
||||
|
||||
# Configure how to generate URLs in non-HTTP contexts, such as CLI commands.
|
||||
# See https://symfony.com/doc/current/routing.html#generating-urls-in-commands
|
||||
#default_uri: http://localhost
|
||||
|
@ -24,8 +24,6 @@ security:
|
||||
remember_me:
|
||||
secret: '%kernel.secret%' # required
|
||||
lifetime: 2419200 # 4 weeks in seconds
|
||||
custom_authenticators:
|
||||
- App\Security\TokenAuthenticator
|
||||
|
||||
secured_area:
|
||||
form_login:
|
||||
@ -41,14 +39,11 @@ security:
|
||||
# Note: Only the *first* access control that matches will be used
|
||||
access_control:
|
||||
- { path: ^/login$, role: PUBLIC_ACCESS }
|
||||
- { path: ^/register$, role: PUBLIC_ACCESS }
|
||||
- { path: ^/register, role: PUBLIC_ACCESS }
|
||||
- { path: ^/logout$, role: ROLE_USER }
|
||||
- { path: ^/admin, role: ROLE_ADMIN }
|
||||
|
||||
- { path: ^/$, role: PUBLIC_ACCESS }
|
||||
- { path: ^/snip/single, role: PUBLIC_ACCESS }
|
||||
- { path: ^/snip/raw, role: PUBLIC_ACCESS }
|
||||
- { path: ^/snip/public$, role: PUBLIC_ACCESS }
|
||||
|
||||
- { path: ^/, role: ROLE_USER }
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
twig:
|
||||
default_path: '%kernel.project_dir%/templates'
|
||||
form_themes: ['bootstrap_5_layout.html.twig']
|
||||
file_name_pattern: '*.twig'
|
||||
|
||||
when@test:
|
||||
twig:
|
||||
|
4
config/packages/uid.yaml
Normal file
4
config/packages/uid.yaml
Normal file
@ -0,0 +1,4 @@
|
||||
framework:
|
||||
uid:
|
||||
default_uuid_version: 7
|
||||
time_based_uuid_version: 7
|
@ -1,5 +1,7 @@
|
||||
framework:
|
||||
validation:
|
||||
email_validation_mode: html5
|
||||
|
||||
# Enables validator auto-mapping support.
|
||||
# For instance, basic validation constraints will be inferred from Doctrine's metadata.
|
||||
#auto_mapping:
|
||||
|
@ -1,11 +1,17 @@
|
||||
when@dev:
|
||||
web_profiler:
|
||||
toolbar: true
|
||||
intercept_redirects: false
|
||||
|
||||
framework:
|
||||
profiler:
|
||||
only_exceptions: false
|
||||
collect_serializer_data: true
|
||||
|
||||
when@test:
|
||||
web_profiler:
|
||||
toolbar: false
|
||||
intercept_redirects: false
|
||||
|
||||
framework:
|
||||
profiler: { collect: false }
|
||||
|
@ -3,12 +3,3 @@ controllers:
|
||||
path: ../src/Controller/
|
||||
namespace: App\Controller
|
||||
type: attribute
|
||||
exclude: ../src/Controller/Api/
|
||||
|
||||
rest_controllers:
|
||||
resource:
|
||||
path: ../src/Controller/Api/
|
||||
namespace: App\Controller\Api
|
||||
type: attribute
|
||||
prefix: /api
|
||||
defaults: { _format: "json" }
|
||||
|
@ -1,3 +0,0 @@
|
||||
_security_logout:
|
||||
resource: security.route_loader.logout
|
||||
type: service
|
@ -4,6 +4,8 @@
|
||||
# Put parameters here that don't need to change on each machine where the app is deployed
|
||||
# https://symfony.com/doc/current/best_practices.html#use-parameters-for-application-configuration
|
||||
parameters:
|
||||
snipStorageType: 'db' # 'db' or 'git
|
||||
gitStoragePath: '%kernel.project_dir%/var/snips'
|
||||
|
||||
services:
|
||||
# default configuration for services in *this* file
|
||||
@ -18,4 +20,13 @@ services:
|
||||
exclude:
|
||||
- '../src/DependencyInjection/'
|
||||
- '../src/Entity/'
|
||||
- '../src/Kernel.php'
|
||||
- '../src/Kernel.php'
|
||||
|
||||
App\Service\LastRelease:
|
||||
arguments:
|
||||
- '%kernel.project_dir%/release.json'
|
||||
|
||||
App\Service\SnipServiceFactory:
|
||||
arguments:
|
||||
$gitStoragePath: '%gitStoragePath%'
|
||||
$storageType: '%snipStorageType%'
|
85
deploy.php
85
deploy.php
@ -3,7 +3,6 @@
|
||||
namespace Deployer;
|
||||
|
||||
require_once 'recipe/common.php';
|
||||
require_once 'deploy/git.php';
|
||||
|
||||
// Project name
|
||||
set('application', 'snips');
|
||||
@ -14,64 +13,96 @@ set('repository', 'git@git.loken.nl:ardent/Snips.git');
|
||||
// [Optional] Allocate tty for git clone. Default value is false.
|
||||
set('git_tty', true);
|
||||
|
||||
// Shared files/dirs between deploys
|
||||
set('shared_dirs', ['var/log', 'var/sessions']);
|
||||
// Shared files/dirs between deploys
|
||||
set('shared_dirs', ['var/log', 'var/sessions', 'var/snips']);
|
||||
set('shared_files', ['.env.local']);
|
||||
//set('writable_dirs', ['var']);
|
||||
|
||||
set('migrations_config', '');
|
||||
set('allow_anonymous_stats', false);
|
||||
|
||||
set('console_options', fn() => '--no-interaction');
|
||||
set('bin/console', fn() => parse('{{release_path}}/bin/console'));
|
||||
|
||||
set('composer_options', '--verbose --prefer-dist --no-progress --no-interaction --no-dev --optimize-autoloader --no-scripts');
|
||||
|
||||
// Hosts
|
||||
host('snips.loken.nl')
|
||||
->setRemoteUser('www-data')
|
||||
->set('branch', function () {
|
||||
return input()->getOption('branch') ?: 'master';
|
||||
return input()->getOption('branch') ?: 'main';
|
||||
})
|
||||
->set('deploy_path', '~/snips.loken.nl')
|
||||
;
|
||||
->set('deploy_path', '~/snips.loken.nl');
|
||||
|
||||
set('bin/console', function () {
|
||||
return parse('{{release_path}}/bin/console');
|
||||
});
|
||||
|
||||
set('console_options', function () {
|
||||
return '--no-interaction';
|
||||
});
|
||||
|
||||
desc('Clear cache');
|
||||
task('cache:clear', fn() => run('{{bin/php}} {{bin/console}} cache:clear {{console_options}} --no-warmup'));
|
||||
task('cache:clear', function () {
|
||||
run('{{bin/php}} {{bin/console}} cache:clear {{console_options}} --no-warmup');
|
||||
});
|
||||
|
||||
desc('Warm up cache');
|
||||
task('cache:warmup', fn() => run('{{bin/php}} {{bin/console}} cache:warmup {{console_options}}'));
|
||||
task('cache:warmup', function () {
|
||||
run('{{bin/php}} {{bin/console}} cache:warmup {{console_options}}');
|
||||
});
|
||||
|
||||
desc('Migrate database');
|
||||
task('database:migrate', function () {
|
||||
$options = '--allow-no-migration';
|
||||
if (get('migrations_config') !== '') {
|
||||
$options = sprintf('%s --configuration={{release_path}}/{{migrations_config}}', $options);
|
||||
}
|
||||
|
||||
run(sprintf('{{bin/php}} {{bin/console}} doctrine:migrations:migrate %s {{console_options}}', $options));
|
||||
// $options = '--allow-no-migration';
|
||||
// if (get('migrations_config') !== '') {
|
||||
// $options = sprintf('%s --configuration={{release_path}}/{{migrations_config}}', $options);
|
||||
// }
|
||||
//
|
||||
// run(sprintf('{{bin/php}} {{bin/console}} doctrine:migrations:migrate %s {{console_options}}', $options));
|
||||
run('{{bin/php}} {{bin/console}} doctrine:schema:update --force');
|
||||
});
|
||||
|
||||
desc('Shows current deployed version');
|
||||
task('deploy:current', function () {
|
||||
$current = run('readlink {{deploy_path}}/current');
|
||||
writeln("Current deployed version: $current");
|
||||
task('deployment:log', function () { //https://stackoverflow.com/questions/59686270/how-to-log-deployments-in-deployer
|
||||
$branch = parse('{{branch}}');
|
||||
$date = date('Y-m-d H:i:s');
|
||||
$commitHashShort = runLocally('git rev-parse --short HEAD');
|
||||
// $commitHash = runLocally('git rev-parse HEAD');
|
||||
$commit = explode(PHP_EOL, runLocally('git log -1 --pretty="%H%n%ci"'));
|
||||
$commitHash = $commit[0];
|
||||
$commitDate = $commit[1];
|
||||
|
||||
// $line = sprintf('%s %s branch="%s" hash="%s"', $date, $commitHashShort, $branch, $commitHash);
|
||||
$projectUrlBase = 'https://git.loken.nl/ardent/AnimeRSS4';
|
||||
$array = [
|
||||
'branch' => $branch,
|
||||
'branchUrl' => sprintf('%s/src/branch/%s', $projectUrlBase, $branch),
|
||||
'date' => $date,
|
||||
'commitHashShort' => $commitHashShort,
|
||||
'commitHashLong' => $commitHash,
|
||||
'commitDate' => $commitDate,
|
||||
'commitUrl' => sprintf('%s/commit/%s', $projectUrlBase, $commitHash),
|
||||
'projectUrl' => $projectUrlBase,
|
||||
];
|
||||
$json = json_encode($array, JSON_PRETTY_PRINT);
|
||||
|
||||
runLocally("echo '$json' > release.json");
|
||||
upload('release.json', '{{release_path}}/release.json');
|
||||
});
|
||||
|
||||
//desc('Deploy project');
|
||||
//task('deploy', [
|
||||
// 'deployment:log',
|
||||
//]);
|
||||
|
||||
desc('Deploy project');
|
||||
task('deploy', [
|
||||
'deploy:prepare',
|
||||
'deploy:vendors',
|
||||
'database:migrate',
|
||||
'cache:clear',
|
||||
'cache:warmup',
|
||||
'database:migrate',
|
||||
'deployment:log',
|
||||
'deploy:symlink',
|
||||
'deploy:unlock',
|
||||
'deploy:cleanup',
|
||||
'deploy:current',
|
||||
]);
|
||||
|
||||
after('deploy', 'deploy:success');
|
||||
|
||||
after('deploy:failed', 'deploy:unlock');
|
||||
// [Optional] if deploy fails automatically unlock.
|
||||
after('deploy:failed', 'deploy:unlock');
|
||||
|
@ -1,27 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Deployer;
|
||||
|
||||
desc('Transfers information about current git commit to server');
|
||||
task('deployment:log', function () { //https://stackoverflow.com/questions/59686270/how-to-log-deployments-in-deployer
|
||||
$branch = parse('{{branch}}');
|
||||
$date = date('Y-m-d H:i:s');
|
||||
$commitHashShort = runLocally('git rev-parse --short HEAD');
|
||||
// $commitHash = runLocally('git rev-parse HEAD');
|
||||
$commit = explode(PHP_EOL, runLocally('git log -1 --pretty="%H%n%ci"'));
|
||||
$commitHash = $commit[0];
|
||||
$commitDate = $commit[1];
|
||||
|
||||
// $line = sprintf('%s %s branch="%s" hash="%s"', $date, $commitHashShort, $branch, $commitHash);
|
||||
$array = [
|
||||
'branch' => $branch,
|
||||
'date' => $date,
|
||||
'commitHashShort' => $commitHashShort,
|
||||
'commitHashLong' => $commitHash,
|
||||
'commitDate' => $commitDate,
|
||||
];
|
||||
$json = json_encode($array, JSON_PRETTY_PRINT);
|
||||
|
||||
runLocally("echo '$json' > release.json");
|
||||
upload('release.json', '{{release_path}}/release.json');
|
||||
});
|
@ -1,5 +0,0 @@
|
||||
{
|
||||
"dev": {
|
||||
"host": "http://snips.local.loken.nl"
|
||||
}
|
||||
}
|
@ -1,35 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Auto-generated Migration: Please modify to your needs!
|
||||
*/
|
||||
final class Version20231220204107 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
// this up() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql('ALTER TABLE snip ADD active_version_id BINARY(16) DEFAULT NULL COMMENT \'(DC2Type:ulid)\'');
|
||||
$this->addSql('ALTER TABLE snip ADD CONSTRAINT FK_FEBD97966A1E45F3 FOREIGN KEY (active_version_id) REFERENCES snip_content (id)');
|
||||
$this->addSql('CREATE INDEX IDX_FEBD97966A1E45F3 ON snip (active_version_id)');
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
// this down() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql('ALTER TABLE snip DROP FOREIGN KEY FK_FEBD97966A1E45F3');
|
||||
$this->addSql('DROP INDEX IDX_FEBD97966A1E45F3 ON snip');
|
||||
$this->addSql('ALTER TABLE snip DROP active_version_id');
|
||||
}
|
||||
}
|
@ -1,35 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Auto-generated Migration: Please modify to your needs!
|
||||
*/
|
||||
final class Version20250414192457 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
// this up() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE snip_content ADD diff JSON DEFAULT NULL COMMENT '(DC2Type:json)'
|
||||
SQL);
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
// this down() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE snip_content DROP diff
|
||||
SQL);
|
||||
}
|
||||
}
|
@ -1,38 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Auto-generated Migration: Please modify to your needs!
|
||||
*/
|
||||
final class Version20250422222542 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
// this up() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE snip ADD parser VARCHAR(255) NOT NULL
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
UPDATE snip SET parser = 'generic'
|
||||
SQL);
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
// this down() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE snip DROP parser
|
||||
SQL);
|
||||
}
|
||||
}
|
@ -1,38 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Auto-generated Migration: Please modify to your needs!
|
||||
*/
|
||||
final class Version20250425182334 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
// this up() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE snip ADD visible TINYINT(1) NOT NULL
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
UPDATE snip SET visible = 1
|
||||
SQL);
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
// this down() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE snip DROP visible
|
||||
SQL);
|
||||
}
|
||||
}
|
@ -1,35 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Auto-generated Migration: Please modify to your needs!
|
||||
*/
|
||||
final class Version20250427184240 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
// this up() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE user ADD api_key VARCHAR(255) DEFAULT NULL
|
||||
SQL);
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
// this down() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE `user` DROP api_key
|
||||
SQL);
|
||||
}
|
||||
}
|
@ -1,39 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controller\Api;
|
||||
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class AbstractApiController extends AbstractController
|
||||
{
|
||||
private function apiResponse(bool $success, array|NormalizableInterface $data): Response
|
||||
{
|
||||
if ($data instanceof NormalizableInterface) {
|
||||
$data = $data->normalize();
|
||||
}
|
||||
if (is_array($data)) {
|
||||
foreach ($data as $key => $value) {
|
||||
if ($value instanceof NormalizableInterface) {
|
||||
$data[$key] = $value->normalize();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new JsonResponse([
|
||||
'success' => $success,
|
||||
'data' => $data,
|
||||
]);
|
||||
}
|
||||
|
||||
final protected function errorResponse(string $message): Response
|
||||
{
|
||||
return $this->apiResponse(false, ['message' => $message]);
|
||||
}
|
||||
|
||||
final protected function successResponse(array|NormalizableInterface $data = []): Response
|
||||
{
|
||||
return $this->apiResponse(true, $data);
|
||||
}
|
||||
}
|
@ -1,76 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controller\Api;
|
||||
|
||||
use App\Dto\SnipPostRequest;
|
||||
use App\Entity\Snip;
|
||||
use App\Entity\User;
|
||||
use App\Repository\SnipRepository;
|
||||
use App\Security\Voter\SnipVoter;
|
||||
use App\Service\SnipContent\SnipContentService;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\Attribute\MapRequestPayload;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
|
||||
class ApiController extends AbstractApiController
|
||||
{
|
||||
#[Route('/me', methods: ['GET'])]
|
||||
public function me(): Response
|
||||
{
|
||||
/** @var User $user */
|
||||
$user = $this->getUser();
|
||||
|
||||
return $this->successResponse([
|
||||
'id' => $user->getId(),
|
||||
'name' => $user->getName(),
|
||||
'email' => $user->getEmail(),
|
||||
'apiKey' => $user->getApiKey(),
|
||||
]);
|
||||
}
|
||||
|
||||
#[Route('/snip/{snip}', methods: ['GET'])]
|
||||
public function getSnip(Snip $snip, SnipContentService $cs): Response
|
||||
{
|
||||
$this->denyAccessUnlessGranted(SnipVoter::VIEW, $snip);
|
||||
|
||||
return $this->successResponse([
|
||||
'id' => $snip->getId(),
|
||||
'content' => $cs->getActiveText($snip),
|
||||
'createdBy' => [
|
||||
'id' => $snip->getCreatedBy()->getId(),
|
||||
'name' => $snip->getCreatedBy()->getName(),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
#[Route('/snip/{snip}', methods: ['POST'])]
|
||||
public function postSnip(
|
||||
Snip $snip,
|
||||
#[MapRequestPayload] SnipPostRequest $request,
|
||||
SnipContentService $cs,
|
||||
SnipRepository $repo,
|
||||
): Response
|
||||
{
|
||||
$this->denyAccessUnlessGranted(SnipVoter::EDIT, $snip);
|
||||
|
||||
if (!($snip->getActiveVersion() === $snip->getLatestVersion())) {
|
||||
return $this->errorResponse('Snip is not the latest version');
|
||||
}
|
||||
|
||||
$request->pushToSnip($snip);
|
||||
$repo->save($snip);
|
||||
if ($request->content !== null) {
|
||||
$cs->update($snip, $request->content);
|
||||
}
|
||||
|
||||
return $this->successResponse([
|
||||
'id' => $snip->getId(),
|
||||
'name' => $snip->getName(),
|
||||
'content' => $cs->getActiveText($snip),
|
||||
'createdBy' => [
|
||||
'id' => $snip->getCreatedBy()->getId(),
|
||||
'name' => $snip->getCreatedBy()->getName(),
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
@ -1,8 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controller\Api;
|
||||
|
||||
interface NormalizableInterface
|
||||
{
|
||||
public function normalize(): array;
|
||||
}
|
@ -3,37 +3,39 @@
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Entity\Snip;
|
||||
use App\Entity\SnipContent;
|
||||
use App\Security\Voter\SnipVoter;
|
||||
use App\Service\SnipContent\SnipContentService;
|
||||
use App\Service\SnipServiceFactory;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
|
||||
#[Route('/version/{snip}', name: 'version')]
|
||||
class VersionController extends AbstractController
|
||||
#[Route('/history/{snip}', name: 'history')]
|
||||
class HistoryController extends AbstractController
|
||||
{
|
||||
public function __construct(
|
||||
private readonly SnipContentService $contentService,
|
||||
private readonly SnipServiceFactory $snipServiceFactory,
|
||||
) {}
|
||||
|
||||
#[Route('/', name: '_index')]
|
||||
public function index(Snip $snip): Response
|
||||
{
|
||||
$this->denyAccessUnlessGranted(SnipVoter::EDIT, $snip);
|
||||
|
||||
return $this->render('version/index.html.twig', [
|
||||
|
||||
$snipService = $this->snipServiceFactory->create($snip);
|
||||
return $this->render('history/index.html.twig', [
|
||||
'snip' => $snip,
|
||||
'versions' => $snipService->getVersions(),
|
||||
'latestVersion' => $snipService->getLatestVersion(),
|
||||
]);
|
||||
}
|
||||
|
||||
#[Route('/set/{version}', name: '_set')]
|
||||
public function set(Snip $snip, SnipContent $version): Response
|
||||
public function set(Snip $snip, string $version): Response
|
||||
{
|
||||
$this->denyAccessUnlessGranted(SnipVoter::EDIT, $snip);
|
||||
|
||||
$this->contentService->setVersion($snip, $version);
|
||||
$this->addFlash('success', 'Snip version set to ' . $version->getId());
|
||||
$this->snipServiceFactory->create($snip)->setVersion($version);
|
||||
$this->addFlash('success', 'Snip version set to ' . $version);
|
||||
return $this->redirectToRoute('snip_single', ['snip' => $snip->getId()]);
|
||||
}
|
||||
}
|
@ -4,17 +4,13 @@ namespace App\Controller;
|
||||
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
|
||||
class HomeController extends AbstractController
|
||||
{
|
||||
#[Route('/', name: 'home')]
|
||||
public function home(): Response
|
||||
{
|
||||
if ($this->getUser()) {
|
||||
return $this->redirectToRoute('snip_index');
|
||||
} else {
|
||||
return $this->redirectToRoute('snip_public');
|
||||
}
|
||||
return $this->redirectToRoute('snip_index');
|
||||
}
|
||||
}
|
@ -11,7 +11,7 @@ use Symfony\Bundle\SecurityBundle\Security;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;
|
||||
|
||||
class SecurityController extends AbstractController
|
||||
|
@ -1,39 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Entity\SnipContent;
|
||||
use App\Security\Voter\SnipVoter;
|
||||
use App\Service\SnipContent\MyersDiff;
|
||||
use App\Service\SnipContent\SnipContentService;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
|
||||
#[Route('/content', name: 'content')]
|
||||
class SnipContentController extends AbstractController
|
||||
{
|
||||
public function __construct(
|
||||
private readonly SnipContentService $contentService,
|
||||
) {}
|
||||
|
||||
#[Route('/compare/{to}/{from}', name: '_compare')]
|
||||
public function compare(SnipContent $to, ?SnipContent $from = null): Response
|
||||
{
|
||||
$this->denyAccessUnlessGranted(SnipVoter::VIEW, $to->getSnip());
|
||||
|
||||
if ($from === null) {
|
||||
$from = $to->getParent();
|
||||
}
|
||||
|
||||
$diff = MyersDiff::buildDiffLines(
|
||||
$this->contentService->rebuildText($from),
|
||||
$this->contentService->rebuildText($to),
|
||||
);
|
||||
|
||||
return $this->render('content/compare.html.twig', [
|
||||
'snip' => $to->getSnip(),
|
||||
'diff' => $diff,
|
||||
]);
|
||||
}
|
||||
}
|
@ -2,64 +2,68 @@
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Dto\SnipFilterRequest;
|
||||
use App\Entity\Snip;
|
||||
use App\Form\ConfirmationType;
|
||||
use App\Form\SnipType;
|
||||
use App\Repository\SnipRepository;
|
||||
use App\Security\Voter\SnipVoter;
|
||||
use App\Service\SnipContent\SnipContentService;
|
||||
use App\Service\SnipParser\ParserFactory;
|
||||
use App\Service\SnipParser\Pipeline;
|
||||
use App\Service\SnipServiceFactory;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\Attribute\MapQueryString;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
|
||||
#[Route('/snip', name: 'snip')]
|
||||
class SnipController extends AbstractController
|
||||
{
|
||||
public function __construct(
|
||||
private readonly SnipRepository $repository,
|
||||
private readonly SnipContentService $contentService,
|
||||
) {}
|
||||
private readonly SnipServiceFactory $snipServiceFactory,
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
#[Route('/', name: '_index')]
|
||||
public function index(#[MapQueryString] SnipFilterRequest $request): Response
|
||||
public function index(): Response
|
||||
{
|
||||
return $this->render('snip/index.html.twig', [
|
||||
'snips' => $this->repository->findByRequest($this->getUser(), $request),
|
||||
'request' => $request,
|
||||
'snips' => $this->repository->findByUser($this->getUser()),
|
||||
'title' => 'My Snips',
|
||||
]);
|
||||
}
|
||||
|
||||
#[Route('/public', name: '_public')]
|
||||
public function public(): Response
|
||||
{
|
||||
return $this->render('snip/public.html.twig', [
|
||||
'snips' => $this->repository->findPublic(),
|
||||
return $this->render('snip/index.html.twig', [
|
||||
'snips' => $this->repository->findPublic($this->getUser()),
|
||||
'title' => 'Public Snips',
|
||||
]);
|
||||
}
|
||||
|
||||
#[Route('/single/{snip}', name: '_single')]
|
||||
public function single(Snip $snip, ParserFactory $pf): Response
|
||||
public function single(Snip $snip, Pipeline $pl): Response
|
||||
{
|
||||
$this->denyAccessUnlessGranted(SnipVoter::VIEW, $snip);
|
||||
|
||||
$snipService = $this->snipServiceFactory->create($snip);
|
||||
dump($snipService);
|
||||
return $this->render('snip/single.html.twig', [
|
||||
'snip' => $snip,
|
||||
'content' => $pf->getBySnip($snip)->parseView($this->contentService->getActiveText($snip)),
|
||||
'content' => $pl->parse($snipService->get()),
|
||||
'branch' => $snipService->getCommit(),
|
||||
]);
|
||||
}
|
||||
|
||||
#[Route('/raw/{snip}', name: '_raw')]
|
||||
public function raw(Snip $snip, ParserFactory $pf, Request $request): Response
|
||||
public function raw(Snip $snip, Pipeline $pl, Request $request): Response
|
||||
{
|
||||
$this->denyAccessUnlessGranted(SnipVoter::VIEW, $snip);
|
||||
|
||||
$response = new Response(
|
||||
$pf->getBySnip($snip)->parseRaw($this->contentService->getActiveText($snip)),
|
||||
$pl->clean($this->snipServiceFactory->create($snip)->get()),
|
||||
Response::HTTP_OK,
|
||||
['Content-Type' => 'text/html']
|
||||
);
|
||||
@ -67,8 +71,7 @@ class SnipController extends AbstractController
|
||||
->setVary(['Accept', 'Accept-Encoding'])
|
||||
->setEtag(md5($response->getContent()))
|
||||
->setTtl(3600)
|
||||
->setClientTtl(300)
|
||||
;
|
||||
->setClientTtl(300);
|
||||
|
||||
if (!$request->isNoCache()) {
|
||||
$response->isNotModified($request);
|
||||
@ -82,30 +85,16 @@ class SnipController extends AbstractController
|
||||
{
|
||||
$this->denyAccessUnlessGranted(SnipVoter::EDIT, $snip);
|
||||
|
||||
/**
|
||||
* Temporary solution to prevent editing of old versions
|
||||
* It technically fully works, but rendering the version history needs an update first
|
||||
*/
|
||||
$isLatest = $snip->getActiveVersion() === $snip->getLatestVersion();
|
||||
if (!$isLatest) {
|
||||
$this->addFlash('error', 'Snip is not the latest version, changes will not be saved.');
|
||||
}
|
||||
|
||||
$form = $this->createForm(SnipType::class, $snip);
|
||||
$form->add('Save', SubmitType::class);
|
||||
if ($snip->getId()) {
|
||||
$form->get('content')->setData($this->contentService->getActiveText($snip));
|
||||
$form->get('content')->setData($this->snipServiceFactory->create($snip)->get());
|
||||
}
|
||||
|
||||
$form->handleRequest($request);
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
if (!$isLatest) {
|
||||
return $this->redirectToRoute('snip_single', [
|
||||
'snip' => $snip->getId(),
|
||||
]);
|
||||
}
|
||||
$this->repository->save($snip);
|
||||
$this->contentService->update($snip, $form->get('content')->getData());
|
||||
$this->snipServiceFactory->create($snip)->update($form->get('content')->getData());
|
||||
|
||||
$this->addFlash('success', sprintf('Snip "%s" saved', $snip));
|
||||
|
||||
@ -124,9 +113,8 @@ class SnipController extends AbstractController
|
||||
public function new(Request $request): Response
|
||||
{
|
||||
$snip = new Snip();
|
||||
$snip->setCreatedAtNow()
|
||||
->setCreatedBy($this->getUser())
|
||||
;
|
||||
$snip->setCreatedAtTodayNoSeconds()
|
||||
->setCreatedBy($this->getUser());
|
||||
|
||||
return $this->edit($snip, $request);
|
||||
}
|
||||
@ -139,13 +127,13 @@ class SnipController extends AbstractController
|
||||
$form = $this->createForm(ConfirmationType::class);
|
||||
$form->handleRequest($request);
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
$this->contentService->delete($snip);
|
||||
$this->snipServiceFactory->create($snip)->delete();
|
||||
$this->repository->remove($snip);
|
||||
$this->addFlash('success', sprintf('Snip "%s" deleted', $snip));
|
||||
return $this->redirectToRoute('snip_index');
|
||||
}
|
||||
|
||||
return $this->render('generic/form.html.twig', [
|
||||
return $this->render('form.html.twig', [
|
||||
'message' => sprintf('Do you really want to delete "%s"?', $snip),
|
||||
'form' => $form->createView(),
|
||||
]);
|
||||
|
@ -4,13 +4,14 @@ namespace App\Controller;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\Form\ProfileType;
|
||||
use App\Form\UserSettingsType;
|
||||
use App\Service\LastRelease;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Uid\Uuid;
|
||||
|
||||
#[Route('/user', name: 'user')]
|
||||
@ -59,20 +60,4 @@ class UserController extends AbstractController
|
||||
'release' => $lastRelease,
|
||||
]);
|
||||
}
|
||||
|
||||
#[Route('/apikey/generate', name: '_apikey_generate')]
|
||||
public function apiKeyGenerate(): Response
|
||||
{
|
||||
/** @var User $user */
|
||||
$user = $this->getUser();
|
||||
|
||||
$apiKey = Uuid::v4()->toBase58();
|
||||
$user->setApiKey($apiKey);
|
||||
$this->em->persist($user);
|
||||
$this->em->flush();
|
||||
|
||||
$this->addFlash('success', sprintf('Successfully generated new api key: "%s"', $apiKey));
|
||||
|
||||
return $this->redirectToRoute('user_profile');
|
||||
}
|
||||
}
|
@ -1,17 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Dto;
|
||||
|
||||
readonly class SnipFilterRequest
|
||||
{
|
||||
public function __construct(
|
||||
public bool $onlyVisible = true,
|
||||
) {}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'onlyVisible' => $this->onlyVisible,
|
||||
];
|
||||
}
|
||||
}
|
@ -1,28 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Dto;
|
||||
|
||||
use App\Entity\Snip;
|
||||
|
||||
class SnipPostRequest
|
||||
{
|
||||
public function __construct(
|
||||
public ?string $name = null,
|
||||
public ?string $content = null,
|
||||
public ?bool $public = null,
|
||||
public ?bool $visible = null,
|
||||
) {}
|
||||
|
||||
public function pushToSnip(Snip $snip): void
|
||||
{
|
||||
if ($this->name !== null) {
|
||||
$snip->setName($this->name);
|
||||
}
|
||||
if ($this->public !== null) {
|
||||
$snip->setPublic($this->public);
|
||||
}
|
||||
if ($this->visible !== null) {
|
||||
$snip->setVisible($this->visible);
|
||||
}
|
||||
}
|
||||
}
|
@ -40,17 +40,10 @@ trait TrackedTrait
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setCreatedAtNowNoSeconds(): self
|
||||
public function setCreatedAtTodayNoSeconds(): self
|
||||
{
|
||||
$this->setCreatedAt(DateTime::createFromFormat('Y-m-d H:i', date('Y-m-d H:i')));
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setCreatedAtNow(): self
|
||||
{
|
||||
$this->setCreatedAt(new DateTime());
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
@ -22,19 +22,13 @@ class Snip
|
||||
private ?string $name = null;
|
||||
|
||||
#[ORM\Column]
|
||||
private bool $public = false;
|
||||
private ?bool $public = null;
|
||||
|
||||
#[ORM\OneToMany(mappedBy: 'snip', targetEntity: SnipContent::class, orphanRemoval: true)]
|
||||
private Collection $snipContents;
|
||||
|
||||
#[ORM\OneToOne]
|
||||
private ?SnipContent $activeVersion = null;
|
||||
|
||||
#[ORM\Column(length: 255)]
|
||||
private ?string $parser = null;
|
||||
|
||||
#[ORM\Column]
|
||||
private bool $visible = true;
|
||||
#[ORM\Column(length: 255, nullable: true)]
|
||||
private ?string $activeCommit = null;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
@ -105,43 +99,14 @@ class Snip
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getLatestVersion(): ?SnipContent
|
||||
public function getActiveCommit(): ?string
|
||||
{
|
||||
return $this->snipContents->last() ?: null;
|
||||
return $this->activeCommit;
|
||||
}
|
||||
|
||||
public function getActiveVersion(): ?SnipContent
|
||||
public function setActiveCommit(?string $activeCommit): static
|
||||
{
|
||||
return $this->activeVersion;
|
||||
}
|
||||
|
||||
public function setActiveVersion(?SnipContent $activeVersion): static
|
||||
{
|
||||
$this->activeVersion = $activeVersion;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getParser(): ?string
|
||||
{
|
||||
return $this->parser;
|
||||
}
|
||||
|
||||
public function setParser(string $parser): static
|
||||
{
|
||||
$this->parser = $parser;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function isVisible(): ?bool
|
||||
{
|
||||
return $this->visible;
|
||||
}
|
||||
|
||||
public function setVisible(bool $visible): static
|
||||
{
|
||||
$this->visible = $visible;
|
||||
$this->activeCommit = $activeCommit;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
@ -32,9 +32,6 @@ class SnipContent
|
||||
#[ORM\Column(type: Types::TEXT, nullable: true)]
|
||||
private ?string $text = null;
|
||||
|
||||
#[ORM\Column(nullable: true)]
|
||||
private ?array $diff = null;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->children = new ArrayCollection();
|
||||
@ -110,16 +107,4 @@ class SnipContent
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getDiff(): ?array
|
||||
{
|
||||
return $this->diff;
|
||||
}
|
||||
|
||||
public function setDiff(?array $diff): static
|
||||
{
|
||||
$this->diff = $diff;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
@ -34,9 +34,6 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
|
||||
#[ORM\Column(length: 255)]
|
||||
private ?string $email = null;
|
||||
|
||||
#[ORM\Column(length: 255, nullable: true)]
|
||||
private ?string $apiKey = null;
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
return $this->name ?? '';
|
||||
@ -135,16 +132,4 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getApiKey(): ?string
|
||||
{
|
||||
return $this->apiKey;
|
||||
}
|
||||
|
||||
public function setApiKey(?string $apiKey): static
|
||||
{
|
||||
$this->apiKey = $apiKey;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
@ -3,33 +3,22 @@
|
||||
namespace App\Form;
|
||||
|
||||
use App\Entity\Snip;
|
||||
use App\Service\SnipParser\ParserFactory;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
class SnipType extends AbstractType
|
||||
{
|
||||
public function __construct(
|
||||
private readonly ParserFactory $parserFactory,
|
||||
) {}
|
||||
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
$builder
|
||||
->add('name')
|
||||
->add('parser', ChoiceType::class, [
|
||||
'choice_label' => fn(string $parser) => ucfirst($parser),
|
||||
'choices' => $this->parserFactory->getChoices(),
|
||||
])
|
||||
->add('content', TextareaType::class, [
|
||||
'attr' => ['rows' => 20],
|
||||
'mapped' => false,
|
||||
])
|
||||
->add('public', SwitchType::class)
|
||||
->add('visible', SwitchType::class)
|
||||
->add('public')
|
||||
;
|
||||
}
|
||||
|
||||
|
@ -1,22 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Form;
|
||||
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\Form\FormView;
|
||||
|
||||
class SwitchType extends AbstractType
|
||||
{
|
||||
public function getParent(): string
|
||||
{
|
||||
return CheckboxType::class;
|
||||
}
|
||||
|
||||
public function buildView(FormView $view, FormInterface $form, array $options): void
|
||||
{
|
||||
$view->vars['label_attr']['class'] = trim(($view->vars['label_attr']['class'] ?? '') . ' checkbox-switch');
|
||||
$view->vars['required'] = false;
|
||||
}
|
||||
}
|
13
src/Git/CustomGit.php
Normal file
13
src/Git/CustomGit.php
Normal file
@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace App\Git;
|
||||
|
||||
use CzProject\GitPhp\Git;
|
||||
|
||||
class CustomGit extends Git
|
||||
{
|
||||
public function open($directory): CustomGitRepository
|
||||
{
|
||||
return new CustomGitRepository($directory, $this->runner);
|
||||
}
|
||||
}
|
33
src/Git/CustomGitRepository.php
Normal file
33
src/Git/CustomGitRepository.php
Normal file
@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace App\Git;
|
||||
|
||||
use CzProject\GitPhp\GitRepository;
|
||||
use DateTime;
|
||||
|
||||
class CustomGitRepository extends GitRepository
|
||||
{
|
||||
/**
|
||||
* @return array<SimpleCommit>
|
||||
* @throws \CzProject\GitPhp\GitException
|
||||
*/
|
||||
public function getAllCommits(): array
|
||||
{
|
||||
$result = $this->run('log', '--pretty=%H,%cI');
|
||||
|
||||
if (empty($result->getOutput())) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$commits = [];
|
||||
foreach ($result->getOutput() as $line) {
|
||||
$parts = explode(',', $line);
|
||||
$commits[] = new SimpleCommit(
|
||||
$parts[0],
|
||||
new DateTime($parts[1])
|
||||
);
|
||||
}
|
||||
|
||||
return $commits;
|
||||
}
|
||||
}
|
26
src/Git/SimpleCommit.php
Normal file
26
src/Git/SimpleCommit.php
Normal file
@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace App\Git;
|
||||
|
||||
use DateTime;
|
||||
|
||||
class SimpleCommit
|
||||
{
|
||||
|
||||
public function __construct(
|
||||
private readonly string $hash,
|
||||
private readonly DateTime $date,
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
public function getHash(): string
|
||||
{
|
||||
return $this->hash;
|
||||
}
|
||||
|
||||
public function getDate(): DateTime
|
||||
{
|
||||
return $this->date;
|
||||
}
|
||||
}
|
0
src/Repository/.gitignore
vendored
Normal file
0
src/Repository/.gitignore
vendored
Normal file
@ -2,12 +2,10 @@
|
||||
|
||||
namespace App\Repository;
|
||||
|
||||
use App\Dto\SnipFilterRequest;
|
||||
use App\Entity\Snip;
|
||||
use App\Entity\User;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
use Symfony\Component\Security\Core\User\UserInterface;
|
||||
|
||||
/**
|
||||
* @extends ServiceEntityRepository<Snip>
|
||||
@ -42,36 +40,24 @@ class SnipRepository extends ServiceEntityRepository
|
||||
}
|
||||
}
|
||||
|
||||
public function findByRequest(UserInterface $user, SnipFilterRequest $request): array
|
||||
public function findByUser(User $user): array
|
||||
{
|
||||
$qb = $this
|
||||
->createQueryBuilder('s')
|
||||
->where('s.createdBy = :user')
|
||||
$qb = $this->createQueryBuilder('s');
|
||||
$qb->where('s.createdBy = :user')
|
||||
->setParameter('user', $user)
|
||||
->orderBy('s.createdAt', 'DESC')
|
||||
;
|
||||
|
||||
$qb->andWhere('s.visible = :visible')
|
||||
->setParameter('visible', $request->onlyVisible)
|
||||
;
|
||||
->orderBy('s.createdAt', 'DESC');
|
||||
|
||||
return $qb->getQuery()->getResult();
|
||||
}
|
||||
|
||||
public function findPublic(?User $user = null): array
|
||||
public function findPublic(User $user): array
|
||||
{
|
||||
$qb = $this
|
||||
->createQueryBuilder('s')
|
||||
->where('s.public = true')
|
||||
->andWhere('s.visible = true')
|
||||
->orderBy('s.createdAt', 'DESC')
|
||||
;
|
||||
|
||||
if ($user) {
|
||||
$qb->andWhere('s.createdBy != :user')
|
||||
->setParameter('user', $user)
|
||||
;
|
||||
}
|
||||
$qb = $this->createQueryBuilder('s');
|
||||
$qb->where('s.public = :public')
|
||||
->andWhere('s.createdBy != :user')
|
||||
->setParameter('public', true)
|
||||
->setParameter('user', $user)
|
||||
->orderBy('s.createdAt', 'DESC');
|
||||
|
||||
return $qb->getQuery()->getResult();
|
||||
}
|
||||
|
@ -1,68 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Security;
|
||||
|
||||
use App\Entity\User;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||
use Symfony\Component\Security\Core\Exception\AuthenticationException;
|
||||
use Symfony\Component\Security\Core\Exception\CustomUserMessageAuthenticationException;
|
||||
use Symfony\Component\Security\Http\Authenticator\AbstractAuthenticator;
|
||||
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
|
||||
use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
|
||||
use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport;
|
||||
|
||||
class TokenAuthenticator extends AbstractAuthenticator
|
||||
{
|
||||
public function __construct(
|
||||
private readonly EntityManagerInterface $em,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Called on every request to decide if this authenticator should be
|
||||
* used for the request. Returning false will cause this authenticator
|
||||
* to be skipped.
|
||||
*/
|
||||
public function supports(Request $request): ?bool
|
||||
{
|
||||
return $request->headers->has('X-AUTH-TOKEN');
|
||||
}
|
||||
|
||||
public function authenticate(Request $request): Passport
|
||||
{
|
||||
$apiToken = $request->headers->get('X-AUTH-TOKEN');
|
||||
if (empty($apiToken)) {
|
||||
// The token header was empty, authentication fails with HTTP Status
|
||||
// Code 401 "Unauthorized"
|
||||
throw new CustomUserMessageAuthenticationException('No API token provided');
|
||||
}
|
||||
|
||||
return new SelfValidatingPassport(
|
||||
new UserBadge($apiToken, function ($apiToken) {
|
||||
return $this->em->getRepository(User::class)->findOneBy(['apiKey' => $apiToken]);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
|
||||
{
|
||||
// on success, let the request continue
|
||||
return null;
|
||||
}
|
||||
|
||||
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
|
||||
{
|
||||
$data = [
|
||||
// you may want to customize or obfuscate the message first
|
||||
'message' => strtr($exception->getMessageKey(), $exception->getMessageData()),
|
||||
|
||||
// or to translate this message
|
||||
// $this->translator->trans($exception->getMessageKey(), $exception->getMessageData())
|
||||
];
|
||||
|
||||
return new JsonResponse($data, Response::HTTP_UNAUTHORIZED);
|
||||
}
|
||||
}
|
@ -3,7 +3,6 @@
|
||||
namespace App\Service;
|
||||
|
||||
use JetBrains\PhpStorm\ArrayShape;
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||
|
||||
class LastRelease
|
||||
{
|
||||
@ -19,7 +18,7 @@ class LastRelease
|
||||
])]
|
||||
private array $lastRelease = [];
|
||||
|
||||
public function __construct(#[Autowire('%kernel.project_dir%/release.json')] string $jsonFile)
|
||||
public function __construct(string $jsonFile)
|
||||
{
|
||||
if (file_exists($jsonFile)) {
|
||||
$this->lastRelease = json_decode(file_get_contents($jsonFile), true);
|
||||
|
@ -1,15 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Service\SnipContent;
|
||||
|
||||
enum DiffTypeEnum: string
|
||||
{
|
||||
case INSERT = 'I';
|
||||
case DELETE = 'D';
|
||||
case KEEP = 'K';
|
||||
|
||||
public function is(string $diffType): bool
|
||||
{
|
||||
return $this->value === $diffType;
|
||||
}
|
||||
}
|
@ -1,236 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Service\SnipContent;
|
||||
|
||||
class MyersDiff
|
||||
{
|
||||
private const string NEWLINE = "\r\n";
|
||||
|
||||
/**
|
||||
* Backtrack through the intermediate results to extract the "snakes" that
|
||||
* are visited on the chosen "D-path".
|
||||
*
|
||||
* @param string[] $v_save Intermediate results
|
||||
* @param int $x End position
|
||||
* @param int $y End position
|
||||
*
|
||||
* @return int[][]
|
||||
*/
|
||||
private static function extractSnakes(array $v_save, int $x, int $y): array
|
||||
{
|
||||
$snakes = [];
|
||||
for ($d = count($v_save) - 1; $x >= 0 && $y >= 0; $d--) {
|
||||
array_unshift($snakes, [$x, $y]);
|
||||
|
||||
$v = $v_save[$d];
|
||||
$k = $x - $y;
|
||||
|
||||
if ($k === -$d || $k !== $d && $v[$k - 1] < $v[$k + 1]) {
|
||||
$k_prev = $k + 1;
|
||||
} else {
|
||||
$k_prev = $k - 1;
|
||||
}
|
||||
|
||||
$x = $v[$k_prev];
|
||||
$y = $x - $k_prev;
|
||||
}
|
||||
|
||||
return $snakes;
|
||||
}
|
||||
|
||||
private static function formatCompact(array $snakes, array $b): array
|
||||
{
|
||||
$solution = [];
|
||||
$x = 0;
|
||||
$y = 0;
|
||||
|
||||
foreach ($snakes as $snake) {
|
||||
// Deletions
|
||||
while ($snake[0] - $snake[1] > $x - $y) {
|
||||
$count = 0;
|
||||
while ($snake[0] - $snake[1] > $x - $y) {
|
||||
$x++;
|
||||
$count++;
|
||||
}
|
||||
$solution[] = [DiffTypeEnum::DELETE->value, $count];
|
||||
}
|
||||
|
||||
// Insertions
|
||||
while ($snake[0] - $snake[1] < $x - $y) {
|
||||
$values = [];
|
||||
while ($snake[0] - $snake[1] < $x - $y) {
|
||||
$values[] = $b[$y];
|
||||
$y++;
|
||||
}
|
||||
$solutionKey = count($solution) - 1;
|
||||
if ($solutionKey >= 0 && DiffTypeEnum::INSERT->is($solution[$solutionKey][0])) {
|
||||
$solution[$solutionKey][1] = array_merge($solution[$solutionKey][1], $values);
|
||||
} else {
|
||||
$solution[] = [DiffTypeEnum::INSERT->value, $values];
|
||||
}
|
||||
}
|
||||
|
||||
// Keeps (snake diagonals)
|
||||
$count = 0;
|
||||
while ($x < $snake[0]) {
|
||||
$x++;
|
||||
$y++;
|
||||
$count++;
|
||||
}
|
||||
if ($count > 0) {
|
||||
$solution[] = [DiffTypeEnum::KEEP->value, $count];
|
||||
}
|
||||
}
|
||||
|
||||
return $solution;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the shortest edit sequence to convert $x into $y.
|
||||
*
|
||||
* @param string|array $textFrom - tokens (characters, words or lines)
|
||||
* @param string|array $textTo - tokens (characters, words or lines)
|
||||
* @param ?callable $compare - comparison function for tokens. Signature is compare($x, $y):bool. If null, === is used.
|
||||
*
|
||||
* @return array[] - pairs of token and edit (-1 for delete, 0 for keep, +1 for insert)
|
||||
*/
|
||||
public static function calculate(string|array $textFrom, string|array $textTo, ?callable $compare = null): array
|
||||
{
|
||||
if (is_string($textFrom)) {
|
||||
$a = self::explode($textFrom);
|
||||
} else {
|
||||
$a = $textFrom;
|
||||
}
|
||||
if (is_string($textTo)) {
|
||||
$b = self::explode($textTo);
|
||||
} else {
|
||||
$b = $textTo;
|
||||
}
|
||||
|
||||
if ($compare === null) {
|
||||
$compare = function ($x, $y) {
|
||||
return $x === $y;
|
||||
};
|
||||
}
|
||||
|
||||
$n = count($a);
|
||||
$m = count($b);
|
||||
$a = array_values($a);
|
||||
$b = array_values($b);
|
||||
$max = $m + $n;
|
||||
|
||||
$v_save = [];
|
||||
|
||||
$v = [1 => 0];
|
||||
for ($d = 0; $d <= $max; $d++) {
|
||||
for ($k = -$d; $k <= $d; $k += 2) {
|
||||
if ($k === -$d || $k !== $d && $v[$k - 1] < $v[$k + 1]) {
|
||||
$x = $v[$k + 1];
|
||||
} else {
|
||||
$x = $v[$k - 1] + 1;
|
||||
}
|
||||
$y = $x - $k;
|
||||
while ($x < $n && $y < $m && $compare($a[$x], $b[$y])) {
|
||||
$x++;
|
||||
$y++;
|
||||
}
|
||||
$v[$k] = $x;
|
||||
$v_save[$d] = $v;
|
||||
if ($x === $n && $y === $m) {
|
||||
break 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return self::formatCompact(self::extractSnakes($v_save, $n, $m), $b);
|
||||
}
|
||||
|
||||
public static function rebuildBFromCompact(string $textFrom, array $diff): string
|
||||
{
|
||||
$a = self::explode($textFrom);
|
||||
$b = [];
|
||||
$x = 0;
|
||||
|
||||
foreach ($diff as [$op, $data]) {
|
||||
switch ($op) {
|
||||
case DiffTypeEnum::KEEP->value:
|
||||
for ($i = 0; $i < $data; $i++) {
|
||||
$b[] = $a[$x++];
|
||||
}
|
||||
break;
|
||||
case DiffTypeEnum::DELETE->value:
|
||||
$x += $data; // skip deleted
|
||||
break;
|
||||
case DiffTypeEnum::INSERT->value:
|
||||
foreach ($data as $v) {
|
||||
$b[] = $v;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new \InvalidArgumentException('Invalid diff operation');
|
||||
}
|
||||
}
|
||||
|
||||
return self::implode($b);
|
||||
}
|
||||
|
||||
public static function buildDiffLines(string $textFrom, string $textTo): array
|
||||
{
|
||||
$a = self::explode($textFrom);
|
||||
$b = self::explode($textTo);
|
||||
$diff = MyersDiff::calculate($a, $b);
|
||||
|
||||
$lines = [];
|
||||
$x = 0;
|
||||
foreach ($diff as [$op, $data]) {
|
||||
switch ($op) {
|
||||
case DiffTypeEnum::KEEP->value:
|
||||
for ($i = 0; $i < $data; $i++) {
|
||||
$lines[] = [
|
||||
'line' => $x,
|
||||
'type' => 'keep',
|
||||
'from' => $a[$x],
|
||||
'to' => $a[$x],
|
||||
];
|
||||
$x++;
|
||||
}
|
||||
break;
|
||||
case DiffTypeEnum::DELETE->value:
|
||||
for ($i = 0; $i < $data; $i++) {
|
||||
$lines[] = [
|
||||
'line' => $x,
|
||||
'type' => 'delete',
|
||||
'from' => $a[$x],
|
||||
'to' => '',
|
||||
];
|
||||
$x++;
|
||||
}
|
||||
break;
|
||||
case DiffTypeEnum::INSERT->value:
|
||||
foreach ($data as $v) {
|
||||
$lines[] = [
|
||||
'line' => $x,
|
||||
'type' => 'insert',
|
||||
'from' => '',
|
||||
'to' => $v,
|
||||
];
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new \InvalidArgumentException('Invalid diff operation');
|
||||
}
|
||||
}
|
||||
|
||||
return $lines;
|
||||
}
|
||||
|
||||
private static function explode(string $text): array
|
||||
{
|
||||
return explode(self::NEWLINE, $text);
|
||||
}
|
||||
|
||||
private static function implode(array $text): string
|
||||
{
|
||||
return implode(self::NEWLINE, $text);
|
||||
}
|
||||
}
|
74
src/Service/SnipContent/SnipContentDB.php
Normal file
74
src/Service/SnipContent/SnipContentDB.php
Normal file
@ -0,0 +1,74 @@
|
||||
<?php
|
||||
|
||||
namespace App\Service\SnipContent;
|
||||
|
||||
use App\Entity\Snip;
|
||||
use App\Entity\SnipContent;
|
||||
use App\Entity\User;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
|
||||
readonly class SnipContentDB implements SnipContentInterface
|
||||
{
|
||||
public function __construct(
|
||||
private Snip $snip,
|
||||
private User $user,
|
||||
private EntityManagerInterface $em,
|
||||
) {}
|
||||
|
||||
public function update(string $snipContents): void
|
||||
{
|
||||
// Create new snipContent entity with previous one as parent
|
||||
$content = new SnipContent();
|
||||
$content
|
||||
->setText($snipContents)
|
||||
->setSnip($this->snip)
|
||||
;
|
||||
if ($this->snip->getSnipContents()->count() > 0) {
|
||||
$content->setParent($this->snip->getSnipContents()->last());
|
||||
}
|
||||
|
||||
$this->em->persist($content);
|
||||
$this->em->flush();
|
||||
|
||||
$this->snip->setActiveCommit($content->getId());
|
||||
$this->em->persist($this->snip);
|
||||
$this->em->flush();
|
||||
}
|
||||
|
||||
public function get(): string
|
||||
{
|
||||
$contentRepo = $this->em->getRepository(SnipContent::class);
|
||||
return $contentRepo->find($this->snip->getActiveCommit())->getText();
|
||||
}
|
||||
|
||||
public function getVersions(): array
|
||||
{
|
||||
// Return all snipContent entities (by parent)
|
||||
return array_map(fn(SnipContent $content) => [
|
||||
'id' => (string)$content->getId(),
|
||||
'name' => $content->getId()->getDateTime()->format('Y-m-d H:i:s'),
|
||||
], $this->snip->getSnipContents()->toArray());
|
||||
}
|
||||
|
||||
public function setVersion(string $version): void
|
||||
{
|
||||
$this->snip->setActiveCommit($version);
|
||||
$this->em->persist($this->snip);
|
||||
$this->em->flush();
|
||||
}
|
||||
|
||||
public function getCommit(): string
|
||||
{
|
||||
return $this->snip->getActiveCommit();
|
||||
}
|
||||
|
||||
public function delete(): void
|
||||
{
|
||||
// Cleanup the history
|
||||
}
|
||||
|
||||
public function getLatestVersion(): string
|
||||
{
|
||||
return $this->snip->getSnipContents()->last()->getId();
|
||||
}
|
||||
}
|
80
src/Service/SnipContent/SnipContentGit.php
Normal file
80
src/Service/SnipContent/SnipContentGit.php
Normal file
@ -0,0 +1,80 @@
|
||||
<?php
|
||||
|
||||
namespace App\Service\SnipContent;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\Git\CustomGitRepository;
|
||||
use App\Git\SimpleCommit;
|
||||
use Symfony\Component\Security\Core\User\UserInterface;
|
||||
|
||||
class SnipContentGit implements SnipContentInterface
|
||||
{
|
||||
private const string SNIP_FILE_NAME = 'snip.txt';
|
||||
private const string MASTER_BRANCH_NAME = 'master';
|
||||
|
||||
public function __construct(
|
||||
private readonly CustomGitRepository $repo,
|
||||
private readonly ?User $user,
|
||||
) {}
|
||||
|
||||
private function snipExists(): bool
|
||||
{
|
||||
return file_exists($this->getSnipPath());
|
||||
}
|
||||
|
||||
private function getSnipPath(): string
|
||||
{
|
||||
return sprintf('%s/snip.txt', $this->repo->getRepositoryPath());
|
||||
}
|
||||
|
||||
public function update(string $snipContents): void
|
||||
{
|
||||
if (!$this->user instanceof UserInterface) {
|
||||
return;
|
||||
}
|
||||
if ($this->repo->getCurrentBranchName() !== self::MASTER_BRANCH_NAME) {
|
||||
$this->repo->checkout(self::MASTER_BRANCH_NAME);
|
||||
}
|
||||
file_put_contents($this->getSnipPath(), $snipContents);
|
||||
$this->repo->addFile(self::SNIP_FILE_NAME);
|
||||
if ($this->repo->hasChanges()) {
|
||||
$this->repo->commit(sprintf('Updated snip at %s by %s', date('Y-m-d H:i:s'), $this->user));
|
||||
}
|
||||
}
|
||||
|
||||
public function get(): string
|
||||
{
|
||||
if (!$this->snipExists()) {
|
||||
return '';
|
||||
}
|
||||
return file_get_contents($this->getSnipPath());
|
||||
}
|
||||
|
||||
public function getVersions(): array
|
||||
{
|
||||
return array_map(fn(SimpleCommit $c) => [
|
||||
'id' => $c->getHash(),
|
||||
'name' => $c->getDate()->format('Y-m-d H:i:s'),
|
||||
], $this->repo->getAllCommits());
|
||||
}
|
||||
|
||||
public function setVersion(string $version): void
|
||||
{
|
||||
$this->repo->checkout($version);
|
||||
}
|
||||
|
||||
public function getCommit(): string
|
||||
{
|
||||
return $this->repo->getCurrentBranchName();
|
||||
}
|
||||
|
||||
public function delete(): void
|
||||
{
|
||||
system("rm -rf " . escapeshellarg($this->repo->getRepositoryPath()));
|
||||
}
|
||||
|
||||
public function getLatestVersion(): string
|
||||
{
|
||||
return self::MASTER_BRANCH_NAME;
|
||||
}
|
||||
}
|
21
src/Service/SnipContent/SnipContentInterface.php
Normal file
21
src/Service/SnipContent/SnipContentInterface.php
Normal file
@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace App\Service\SnipContent;
|
||||
|
||||
interface SnipContentInterface
|
||||
{
|
||||
public function update(string $snipContents): void;
|
||||
|
||||
public function get(): string;
|
||||
|
||||
/** @return array{id: string, name: string} */
|
||||
public function getVersions(): array;
|
||||
|
||||
public function setVersion(string $version): void;
|
||||
|
||||
public function getCommit(): string;
|
||||
|
||||
public function getLatestVersion(): string;
|
||||
|
||||
public function delete(): void;
|
||||
}
|
@ -1,108 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Service\SnipContent;
|
||||
|
||||
use App\Entity\Snip;
|
||||
use App\Entity\SnipContent;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
|
||||
readonly class SnipContentService
|
||||
{
|
||||
|
||||
public function __construct(
|
||||
private EntityManagerInterface $em,
|
||||
) {}
|
||||
|
||||
public function update(Snip $snip, string $snipContents): void
|
||||
{
|
||||
$parentContent = $snip->getActiveVersion();
|
||||
if ($this->rebuildText($parentContent) === $snipContents) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Create new snipContent entity with previous one as parent
|
||||
$content = new SnipContent();
|
||||
$content
|
||||
->setText($snipContents)
|
||||
->setSnip($snip)
|
||||
;
|
||||
if ($parentContent !== null) {
|
||||
$content->setParent($parentContent);
|
||||
$this->contentToRelative($parentContent);
|
||||
}
|
||||
|
||||
$this->em->persist($content);
|
||||
$this->em->flush();
|
||||
|
||||
$snip->setActiveVersion($content);
|
||||
$this->em->persist($snip);
|
||||
$this->em->flush();
|
||||
}
|
||||
|
||||
public function getActiveText(Snip $snip): string
|
||||
{
|
||||
return $this->rebuildText($snip->getActiveVersion());
|
||||
}
|
||||
|
||||
public function rebuildText(?SnipContent $snipContent): string
|
||||
{
|
||||
if ($snipContent === null) {
|
||||
return '';
|
||||
}
|
||||
if ($snipContent->getText()) {
|
||||
return $snipContent->getText();
|
||||
}
|
||||
|
||||
$parentContent = $snipContent->getParent();
|
||||
if ($parentContent === null && $snipContent->getDiff() === null) {
|
||||
return '---Something went very wrong, cant rebuild the text---';
|
||||
}
|
||||
|
||||
return MyersDiff::rebuildBFromCompact(
|
||||
$this->rebuildText($parentContent), $snipContent->getDiff()
|
||||
);
|
||||
}
|
||||
|
||||
public function setVersion(Snip $snip, SnipContent $version): void
|
||||
{
|
||||
$activeVersion = $snip->getActiveVersion();
|
||||
$this->contentToAbsolute($version);
|
||||
$this->contentToRelative($activeVersion);
|
||||
|
||||
$snip->setActiveVersion($version);
|
||||
$this->em->persist($snip);
|
||||
$this->em->flush();
|
||||
}
|
||||
|
||||
public function contentToRelative(SnipContent $content): void
|
||||
{
|
||||
if ($content->getText() === null || $content->getParent() === null) {
|
||||
return;
|
||||
}
|
||||
$contentText = $content->getText();
|
||||
$parentText = $this->rebuildText($content->getParent());
|
||||
$diff = MyersDiff::calculate($parentText, $contentText);
|
||||
$content->setDiff($diff);
|
||||
$content->setText(null);
|
||||
$this->em->persist($content);
|
||||
$this->em->flush();
|
||||
}
|
||||
|
||||
public function contentToAbsolute(SnipContent $content): void
|
||||
{
|
||||
if ($content->getDiff() === null) {
|
||||
return;
|
||||
}
|
||||
$content->setText($this->rebuildText($content));
|
||||
$content->setDiff(null);
|
||||
$this->em->persist($content);
|
||||
$this->em->flush();
|
||||
}
|
||||
|
||||
public function delete(Snip $snip): void
|
||||
{
|
||||
foreach ($snip->getSnipContents() as $snipContent) {
|
||||
$this->em->remove($snipContent);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,28 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Service\SnipParser;
|
||||
|
||||
abstract class AbstractParser implements ParserInterface
|
||||
{
|
||||
public static function getName(): string
|
||||
{
|
||||
$path = explode('\\', static::class);
|
||||
return strtolower(str_replace('Parser', '', array_pop($path)));
|
||||
}
|
||||
|
||||
public function parseRaw(string $content): string
|
||||
{
|
||||
return $content;
|
||||
}
|
||||
|
||||
public function parseView(string $content): string
|
||||
{
|
||||
try {
|
||||
return $this->safeParseView($content);
|
||||
} catch (\Exception $exception) {
|
||||
return sprintf('<pre><code class="hljs">%s</code></pre>', htmlspecialchars($exception->getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
abstract function safeParseView(string $content): string;
|
||||
}
|
@ -1,60 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Service\SnipParser\Generic;
|
||||
|
||||
use App\Repository\SnipContentRepository;
|
||||
use App\Repository\SnipRepository;
|
||||
use App\Security\Voter\SnipVoter;
|
||||
use App\Service\SnipContent\SnipContentService;
|
||||
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 SnipContentRepository $snipContentRepository,
|
||||
#[Autowire(lazy: true)] private readonly GenericParser $pipeline,
|
||||
#[Autowire(lazy: true)] private readonly SnipContentService $snipContentService,
|
||||
) {}
|
||||
|
||||
public function __invoke(mixed $payload): string
|
||||
{
|
||||
return $this->replaceReferences($payload);
|
||||
}
|
||||
|
||||
private function replaceReferences(mixed $payload): string
|
||||
{
|
||||
// replaces all references ({{ID}}) with the content of the snip
|
||||
$pattern = '/\{\{([A-Z0-9]+)\}\}/';
|
||||
|
||||
return preg_replace_callback($pattern, function ($matches) {
|
||||
$id = $matches[1];
|
||||
try {
|
||||
$content = $this->snipContentRepository->find($id);
|
||||
} catch (\Exception) {
|
||||
$content = null;
|
||||
}
|
||||
if ($content) {
|
||||
$snip = $content->getSnip();
|
||||
} else {
|
||||
$snip = $this->snipRepository->find($id);
|
||||
if ($snip) {
|
||||
$content = $this->snipContentRepository->find($snip->getActiveVersion());
|
||||
}
|
||||
}
|
||||
if ($content === null) {
|
||||
return sprintf('<span title="snip or content 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->parseView(
|
||||
$this->snipContentService->rebuildText($content)
|
||||
);
|
||||
}, $payload);
|
||||
}
|
||||
}
|
@ -1,13 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Service\SnipParser\Html;
|
||||
|
||||
use App\Service\SnipParser\AbstractParser;
|
||||
|
||||
class HtmlParser extends AbstractParser
|
||||
{
|
||||
public function safeParseView(string $content): string
|
||||
{
|
||||
return sprintf('<pre><code class="hljs">%s</code></pre>', htmlspecialchars($content));
|
||||
}
|
||||
}
|
@ -1,53 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Service\SnipParser\Markdown;
|
||||
|
||||
use App\Repository\SnipRepository;
|
||||
use App\Service\SnipParser\AbstractParser;
|
||||
use League\CommonMark\Event\DocumentParsedEvent;
|
||||
use League\CommonMark\Extension\CommonMark\Node\Inline\Link;
|
||||
use League\CommonMark\GithubFlavoredMarkdownConverter;
|
||||
use League\CommonMark\Node\Inline\Text;
|
||||
use League\CommonMark\Node\Query;
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||
use Symfony\Component\Routing\RouterInterface;
|
||||
|
||||
class MarkdownParser extends AbstractParser
|
||||
{
|
||||
public function __construct(
|
||||
#[Autowire(lazy: true)] private readonly RouterInterface $router,
|
||||
#[Autowire(lazy: true)] private readonly SnipRepository $snipRepo,
|
||||
) {}
|
||||
|
||||
public function safeParseView(string $content): string
|
||||
{
|
||||
$converter = new GithubFlavoredMarkdownConverter();
|
||||
$converter->getEnvironment()->addEventListener(DocumentParsedEvent::class, $this->documentParsed(...));
|
||||
return $converter->convert($content);
|
||||
}
|
||||
|
||||
private function documentParsed(DocumentParsedEvent $event): void
|
||||
{
|
||||
$document = $event->getDocument();
|
||||
|
||||
$linkNodes = new Query()
|
||||
->where(Query::type(Link::class))
|
||||
->findAll($document);
|
||||
|
||||
foreach ($linkNodes as $linkNode) {
|
||||
$url = $linkNode->getUrl();
|
||||
|
||||
$snip = $this->snipRepo->find($url);
|
||||
if ($snip === null) {
|
||||
continue;
|
||||
}
|
||||
$linkNode->setUrl($this->router->generate('snip_single', [
|
||||
'snip' => $url,
|
||||
]));
|
||||
$textNode = $linkNode->firstChild();
|
||||
if (!$textNode) {
|
||||
$linkNode->appendChild(new Text($snip));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,52 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Service\SnipParser;
|
||||
|
||||
use App\Entity\Snip;
|
||||
use Symfony\Component\DependencyInjection\Attribute\AutowireLocator;
|
||||
use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException;
|
||||
use Symfony\Component\DependencyInjection\ServiceLocator;
|
||||
|
||||
readonly class ParserFactory
|
||||
{
|
||||
public function __construct(
|
||||
#[AutowireLocator(ParserInterface::class, defaultIndexMethod: 'getName')]
|
||||
private ServiceLocator $locator
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @template T of ParserInterface
|
||||
*
|
||||
* @param class-string<T> $id
|
||||
*
|
||||
* @return T
|
||||
* @throws ServiceNotFoundException
|
||||
*/
|
||||
public function get(string $id): ParserInterface
|
||||
{
|
||||
return $this->locator->get($id);
|
||||
}
|
||||
|
||||
public function getBySnip(Snip $snip): ParserInterface
|
||||
{
|
||||
$parser = $snip->getParser();
|
||||
if (null === $parser) {
|
||||
throw new ServiceNotFoundException(sprintf('Unknown parser for snip "%s"', $snip->getParser()));
|
||||
}
|
||||
|
||||
return $this->get($parser);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return iterable<string, ParserInterface>
|
||||
*/
|
||||
public function getAll(): iterable
|
||||
{
|
||||
return $this->locator->getIterator();
|
||||
}
|
||||
|
||||
public function getChoices(): iterable
|
||||
{
|
||||
foreach ($this->getAll() as $parser) yield $parser::getName();
|
||||
}
|
||||
}
|
@ -1,15 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Service\SnipParser;
|
||||
|
||||
use Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag;
|
||||
|
||||
#[AutoconfigureTag]
|
||||
interface ParserInterface
|
||||
{
|
||||
public function parseView(string $content): string;
|
||||
|
||||
public function parseRaw(string $content): string;
|
||||
|
||||
public static function getName(): string;
|
||||
}
|
@ -1,18 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace App\Service\SnipParser\Generic;
|
||||
namespace App\Service\SnipParser;
|
||||
|
||||
use App\Service\SnipParser\AbstractParser;
|
||||
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 GenericParser extends AbstractParser
|
||||
class Pipeline
|
||||
{
|
||||
public function __construct(
|
||||
private readonly UrlReferenceStage $referenceStage,
|
||||
private readonly IncludeReferenceStage $includeStage,
|
||||
private readonly UrlReferenceStage $referenceStage,
|
||||
private readonly IncludeReferenceStage $includeStage,
|
||||
) {}
|
||||
|
||||
public function safeParseView(string $content): string
|
||||
public function parse(string $payload): string
|
||||
{
|
||||
$builder = new PipelineBuilder();
|
||||
$pipeline = $builder
|
||||
@ -25,15 +29,15 @@ class GenericParser extends AbstractParser
|
||||
->build()
|
||||
;
|
||||
|
||||
return $pipeline->process($content);
|
||||
return $pipeline->process($payload);
|
||||
}
|
||||
|
||||
public function parseRaw(string $content): string
|
||||
public function clean(string $payload): string
|
||||
{
|
||||
return str_replace(
|
||||
['```', '``'],
|
||||
'',
|
||||
$content
|
||||
$payload
|
||||
);
|
||||
}
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace App\Service\SnipParser\Generic;
|
||||
namespace App\Service\SnipParser\Stages;
|
||||
|
||||
use League\Pipeline\StageInterface;
|
||||
|
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);
|
||||
}
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace App\Service\SnipParser\Generic;
|
||||
namespace App\Service\SnipParser\Stages;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use League\Pipeline\StageInterface;
|
@ -1,15 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace App\Service\SnipParser\Generic;
|
||||
namespace App\Service\SnipParser\Stages;
|
||||
|
||||
use League\Pipeline\StageInterface;
|
||||
|
||||
readonly class ReplaceStage implements StageInterface
|
||||
class ReplaceStage implements StageInterface
|
||||
{
|
||||
// replaces a string with another string
|
||||
public function __construct(
|
||||
public string $search,
|
||||
public string $replace,
|
||||
public readonly string $search,
|
||||
public readonly string $replace,
|
||||
) {}
|
||||
|
||||
public function __invoke(mixed $payload): string
|
@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace App\Service\SnipParser\Generic;
|
||||
namespace App\Service\SnipParser\Stages;
|
||||
|
||||
use App\Repository\SnipRepository;
|
||||
use App\Security\Voter\SnipVoter;
|
||||
@ -9,12 +9,12 @@ use Symfony\Bundle\SecurityBundle\Security;
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||
|
||||
readonly class UrlReferenceStage implements StageInterface
|
||||
class UrlReferenceStage implements StageInterface
|
||||
{
|
||||
public function __construct(
|
||||
#[Autowire(lazy: true)] private UrlGeneratorInterface $router,
|
||||
#[Autowire(lazy: true)] private Security $security,
|
||||
#[Autowire(lazy: true)] private SnipRepository $snipRepository,
|
||||
#[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
|
||||
@ -37,7 +37,7 @@ readonly class UrlReferenceStage implements StageInterface
|
||||
}
|
||||
|
||||
$url = $this->router->generate('snip_single', ['snip' => $snip->getId()]);
|
||||
return sprintf('<a href="%s">%s</a>', $url, $snip);
|
||||
return sprintf('<a href="%s" title="Owner: %s">#%s</a>', $url, $snip->getCreatedBy(), $snip->getId());
|
||||
}, $payload);
|
||||
}
|
||||
}
|
@ -1,56 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Service\SnipParser\Twig;
|
||||
|
||||
use App\Entity\Snip;
|
||||
use App\Repository\SnipRepository;
|
||||
use App\Service\SnipContent\SnipContentService;
|
||||
use Twig\Error\LoaderError;
|
||||
use Twig\Loader\LoaderInterface;
|
||||
use Twig\Source;
|
||||
|
||||
class SnipLoader implements LoaderInterface
|
||||
{
|
||||
public function __construct(
|
||||
private readonly SnipRepository $repository,
|
||||
private readonly SnipContentService $contentService,
|
||||
) {}
|
||||
|
||||
public function getSourceContext(string $name): Source
|
||||
{
|
||||
return new Source($this->contentService->getActiveText($this->getFromKey($name)), $name);
|
||||
}
|
||||
|
||||
public function getCacheKey(string $name): string
|
||||
{
|
||||
return $this->getFromKey($name)->getActiveVersion()->getId();
|
||||
}
|
||||
|
||||
public function isFresh(string $name, int $time): bool
|
||||
{
|
||||
$this->getFromKey($name);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function exists(string $name): bool
|
||||
{
|
||||
try {
|
||||
$this->getFromKey($name);
|
||||
} catch (LoaderError) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private function getFromKey(string $key): Snip
|
||||
{
|
||||
$snip = $this->repository->find($key);
|
||||
if (!$snip) {
|
||||
throw new LoaderError(\sprintf('Template "%s" is not defined.', $key));
|
||||
}
|
||||
|
||||
return $snip;
|
||||
}
|
||||
}
|
@ -1,43 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Service\SnipParser\Twig;
|
||||
|
||||
use App\Repository\SnipRepository;
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||
use Symfony\Component\Routing\RouterInterface;
|
||||
use Twig\Extension\AbstractExtension;
|
||||
use Twig\TwigFunction;
|
||||
|
||||
class SnipTwigExtension extends AbstractExtension
|
||||
{
|
||||
public function __construct(
|
||||
#[Autowire(lazy: true)] private readonly RouterInterface $router,
|
||||
#[Autowire(lazy: true)] private readonly SnipRepository $snipRepo,
|
||||
) {}
|
||||
|
||||
public function getFunctions(): array
|
||||
{
|
||||
return [
|
||||
new TwigFunction('snipPath', $this->snipPath(...)),
|
||||
new TwigFunction('snipLink', $this->snipLink(...), [
|
||||
'is_safe' => ['html'],
|
||||
]),
|
||||
];
|
||||
}
|
||||
|
||||
private function snipPath(int $id): string
|
||||
{
|
||||
return $this->router->generate('snip_single', [
|
||||
'snip' => $id,
|
||||
]);
|
||||
}
|
||||
|
||||
private function snipLink(int $id): string
|
||||
{
|
||||
$snip = $this->snipRepo->find($id);
|
||||
if ($snip === null) {
|
||||
throw new \Exception(sprintf('Snip not found with id: %d', $id));
|
||||
}
|
||||
return sprintf('<a class="btn btn-sm btn-primary" href="%s">%s</a>', $this->snipPath($id), $snip);
|
||||
}
|
||||
}
|
@ -1,30 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Service\SnipParser\Twig;
|
||||
|
||||
use App\Service\SnipParser\AbstractParser;
|
||||
use Twig\Environment;
|
||||
use Twig\Loader\ArrayLoader;
|
||||
use Twig\Loader\ChainLoader;
|
||||
|
||||
class TwigParser extends AbstractParser
|
||||
{
|
||||
public function __construct(
|
||||
private readonly SnipLoader $snipLoader,
|
||||
private readonly SnipTwigExtension $snipTwigExtension,
|
||||
) {}
|
||||
|
||||
public function safeParseView(string $content): string
|
||||
{
|
||||
$loader = new ChainLoader([
|
||||
new ArrayLoader([
|
||||
'index' => $content,
|
||||
]),
|
||||
$this->snipLoader,
|
||||
]);
|
||||
$twig = new Environment($loader);
|
||||
$twig->addExtension($this->snipTwigExtension);
|
||||
|
||||
return $twig->render('index');
|
||||
}
|
||||
}
|
@ -3,17 +3,48 @@
|
||||
namespace App\Service;
|
||||
|
||||
use App\Entity\Snip;
|
||||
use App\Service\SnipContent\SnipContentService;
|
||||
use App\Git\CustomGit;
|
||||
use App\Service\SnipContent\SnipContentDB;
|
||||
use App\Service\SnipContent\SnipContentGit;
|
||||
use App\Service\SnipContent\SnipContentInterface;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Bundle\SecurityBundle\Security;
|
||||
|
||||
readonly class SnipServiceFactory
|
||||
class SnipServiceFactory
|
||||
{
|
||||
public function __construct(
|
||||
private EntityManagerInterface $em,
|
||||
private readonly string $gitStoragePath,
|
||||
private readonly string $storageType,
|
||||
private readonly Security $security,
|
||||
private readonly EntityManagerInterface $em,
|
||||
) {}
|
||||
|
||||
public function create(Snip $snip): SnipContentService
|
||||
public function create(Snip $snip): SnipContentInterface
|
||||
{
|
||||
return new SnipContentService($snip, $this->em);
|
||||
return match ($this->storageType) {
|
||||
'git' => $this->createGit($snip),
|
||||
'db' => $this->createDB($snip),
|
||||
default => throw new \Exception('Unknown storage type'),
|
||||
};
|
||||
}
|
||||
|
||||
private function createGit(Snip $snip): SnipContentGit
|
||||
{
|
||||
$git = new CustomGit();
|
||||
$repoPath = sprintf('%s/%s', $this->gitStoragePath, $snip->getId());
|
||||
if (!is_dir($repoPath)) {
|
||||
$repo = $git->init($repoPath);
|
||||
touch(sprintf('%s/.gitignore', $repoPath));
|
||||
$repo->addFile('.gitignore');
|
||||
$repo->commit('Initial commit');
|
||||
} else {
|
||||
$repo = $git->open($repoPath);
|
||||
}
|
||||
return new SnipContentGit($repo, $this->security->getUser());
|
||||
}
|
||||
|
||||
private function createDB(Snip $snip): SnipContentDB
|
||||
{
|
||||
return new SnipContentDB($snip, $this->security->getUser(), $this->em);
|
||||
}
|
||||
}
|
82
symfony.lock
82
symfony.lock
@ -1,11 +1,11 @@
|
||||
{
|
||||
"doctrine/doctrine-bundle": {
|
||||
"version": "2.14",
|
||||
"version": "2.9",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes",
|
||||
"branch": "main",
|
||||
"version": "2.13",
|
||||
"ref": "8d96c0b51591ffc26794d865ba3ee7d193438a83"
|
||||
"version": "2.8",
|
||||
"ref": "6b43b7b6ff6bf2551f2933ebeb66721fa3db8fbc"
|
||||
},
|
||||
"files": [
|
||||
"config/packages/doctrine.yaml",
|
||||
@ -27,12 +27,12 @@
|
||||
]
|
||||
},
|
||||
"symfony/console": {
|
||||
"version": "7.2",
|
||||
"version": "6.2",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes",
|
||||
"branch": "main",
|
||||
"version": "5.3",
|
||||
"ref": "1781ff40d8a17d87cf53f8d4cf0c8346ed2bb461"
|
||||
"ref": "da0c8be8157600ad34f10ff0c9cc91232522e047"
|
||||
},
|
||||
"files": [
|
||||
"bin/console"
|
||||
@ -51,37 +51,24 @@
|
||||
]
|
||||
},
|
||||
"symfony/flex": {
|
||||
"version": "2.5",
|
||||
"version": "2.2",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes",
|
||||
"branch": "main",
|
||||
"version": "2.4",
|
||||
"ref": "52e9754527a15e2b79d9a610f98185a1fe46622a"
|
||||
"version": "1.0",
|
||||
"ref": "146251ae39e06a95be0fe3d13c807bcf3938b172"
|
||||
},
|
||||
"files": [
|
||||
".env",
|
||||
".env.dev"
|
||||
]
|
||||
},
|
||||
"symfony/form": {
|
||||
"version": "7.2",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes",
|
||||
"branch": "main",
|
||||
"version": "7.2",
|
||||
"ref": "7d86a6723f4a623f59e2bf966b6aad2fc461d36b"
|
||||
},
|
||||
"files": [
|
||||
"config/packages/csrf.yaml"
|
||||
".env"
|
||||
]
|
||||
},
|
||||
"symfony/framework-bundle": {
|
||||
"version": "7.2",
|
||||
"version": "6.2",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes",
|
||||
"branch": "main",
|
||||
"version": "7.2",
|
||||
"ref": "87bcf6f7c55201f345d8895deda46d2adbdbaa89"
|
||||
"version": "6.2",
|
||||
"ref": "af47254c5e4cd543e6af3e4508298ffebbdaddd3"
|
||||
},
|
||||
"files": [
|
||||
"config/packages/cache.yaml",
|
||||
@ -104,24 +91,24 @@
|
||||
}
|
||||
},
|
||||
"symfony/monolog-bundle": {
|
||||
"version": "3.10",
|
||||
"version": "3.8",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes",
|
||||
"branch": "main",
|
||||
"version": "3.7",
|
||||
"ref": "aff23899c4440dd995907613c1dd709b6f59503f"
|
||||
"ref": "213676c4ec929f046dfde5ea8e97625b81bc0578"
|
||||
},
|
||||
"files": [
|
||||
"config/packages/monolog.yaml"
|
||||
]
|
||||
},
|
||||
"symfony/routing": {
|
||||
"version": "7.2",
|
||||
"version": "6.2",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes",
|
||||
"branch": "main",
|
||||
"version": "7.0",
|
||||
"ref": "21b72649d5622d8f7da329ffb5afb232a023619d"
|
||||
"version": "6.2",
|
||||
"ref": "e0a11b4ccb8c9e70b574ff5ad3dfdcd41dec5aa6"
|
||||
},
|
||||
"files": [
|
||||
"config/packages/routing.yaml",
|
||||
@ -129,25 +116,24 @@
|
||||
]
|
||||
},
|
||||
"symfony/security-bundle": {
|
||||
"version": "7.2",
|
||||
"version": "6.2",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes",
|
||||
"branch": "main",
|
||||
"version": "6.4",
|
||||
"ref": "2ae08430db28c8eb4476605894296c82a642028f"
|
||||
"version": "6.0",
|
||||
"ref": "8a5b112826f7d3d5b07027f93786ae11a1c7de48"
|
||||
},
|
||||
"files": [
|
||||
"config/packages/security.yaml",
|
||||
"config/routes/security.yaml"
|
||||
"config/packages/security.yaml"
|
||||
]
|
||||
},
|
||||
"symfony/twig-bundle": {
|
||||
"version": "7.2",
|
||||
"version": "6.2",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes",
|
||||
"branch": "main",
|
||||
"version": "6.4",
|
||||
"ref": "cab5fd2a13a45c266d45a7d9337e28dee6272877"
|
||||
"version": "5.4",
|
||||
"ref": "bb2178c57eee79e6be0b297aa96fc0c0def81387"
|
||||
},
|
||||
"files": [
|
||||
"config/packages/twig.yaml",
|
||||
@ -155,34 +141,36 @@
|
||||
]
|
||||
},
|
||||
"symfony/uid": {
|
||||
"version": "7.2",
|
||||
"version": "7.0",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes",
|
||||
"branch": "main",
|
||||
"version": "7.0",
|
||||
"ref": "0df5844274d871b37fc3816c57a768ffc60a43a5"
|
||||
"version": "6.2",
|
||||
"ref": "d294ad4add3e15d7eb1bae0221588ca89b38e558"
|
||||
},
|
||||
"files": []
|
||||
"files": [
|
||||
"config/packages/uid.yaml"
|
||||
]
|
||||
},
|
||||
"symfony/validator": {
|
||||
"version": "7.2",
|
||||
"version": "6.2",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes",
|
||||
"branch": "main",
|
||||
"version": "7.0",
|
||||
"ref": "8c1c4e28d26a124b0bb273f537ca8ce443472bfd"
|
||||
"version": "5.3",
|
||||
"ref": "c32cfd98f714894c4f128bb99aa2530c1227603c"
|
||||
},
|
||||
"files": [
|
||||
"config/packages/validator.yaml"
|
||||
]
|
||||
},
|
||||
"symfony/web-profiler-bundle": {
|
||||
"version": "7.2",
|
||||
"version": "6.2",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes",
|
||||
"branch": "main",
|
||||
"version": "6.1",
|
||||
"ref": "8b51135b84f4266e3b4c8a6dc23c9d1e32e543b7"
|
||||
"ref": "e42b3f0177df239add25373083a564e5ead4e13a"
|
||||
},
|
||||
"files": [
|
||||
"config/packages/web_profiler.yaml",
|
||||
|
@ -3,12 +3,12 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>{% if title is defined %}{{ title }}{% else %}SNIPS{% endif %}</title>
|
||||
<title>{% block title %}SNIPS{% endblock %}</title>
|
||||
<link rel="shortcut icon" type="image/jpg" href="/favicon.png">
|
||||
|
||||
{% block css %}
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css"
|
||||
integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH"
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet"
|
||||
integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3"
|
||||
crossorigin="anonymous">
|
||||
{% endblock %}
|
||||
</head>
|
||||
@ -37,7 +37,19 @@
|
||||
</div>
|
||||
|
||||
{# body blocks #}
|
||||
{% block content %}
|
||||
{% block bodyraw %}
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-sm mx-auto">
|
||||
{% block body %}{% endblock %}
|
||||
</div>
|
||||
{% if block('body2') is defined %}
|
||||
<div class="col-sm mx-auto">
|
||||
{{ block('body2') }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{# javascript block #}
|
||||
|
@ -1,8 +0,0 @@
|
||||
{% extends 'base/base.html.twig' %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
{% block container %}
|
||||
{% endblock %}
|
||||
</div>
|
||||
{% endblock %}
|
@ -1,6 +1,6 @@
|
||||
<nav class="navbar navbar-expand-md navbar-dark bg-dark" style="z-index: 1;">
|
||||
<div class="container-fluid">
|
||||
<a title="Snips" class="navbar-brand" href="{{ path('home') }}">SNIPS</a>
|
||||
<a title="BlueLinked Eco System" class="navbar-brand" href="{{ path('home') }}">SNIPS</a>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbar"
|
||||
aria-controls="navbar" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
@ -15,10 +15,10 @@
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ path('snip_new') }}">New snip</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ path('snip_public') }}">Public snips</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ path('snip_public') }}">Public snips</a>
|
||||
</li>
|
||||
</ul>
|
||||
<ul class="navbar-nav my-2 my-lg-0">
|
||||
{% if app.environment == 'dev' %}
|
||||
@ -35,9 +35,6 @@
|
||||
<a class="nav-link" href="{{ path('logout') }}">Logout</a>
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ path('login') }}">Login</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ path('register') }}">Register</a>
|
||||
</li>
|
||||
|
@ -1,10 +0,0 @@
|
||||
{% extends 'base/container.html.twig' %}
|
||||
|
||||
{% block container %}
|
||||
<div class="row">
|
||||
<div class="col-sm mx-auto">
|
||||
{% if title is defined %}<h3>{{ title }}</h3>{% endif %}
|
||||
{% block body %}{% endblock %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
@ -1,17 +0,0 @@
|
||||
{% extends 'base/container.html.twig' %}
|
||||
|
||||
{% block container %}
|
||||
<div class="row">
|
||||
<div class="col-sm mx-auto">
|
||||
{% if title is defined %}<h3>{{ title }}</h3>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-sm mx-auto">
|
||||
{% block column1 %}{% endblock %}
|
||||
</div>
|
||||
<div class="col-sm mx-auto">
|
||||
{% block column2 %}{% endblock %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
@ -1,32 +0,0 @@
|
||||
{% extends 'base/one.column.html.twig' %}
|
||||
|
||||
{% set title = 'Snip compare ' ~ snip %}
|
||||
|
||||
{% block body %}
|
||||
<a href="{{ path('snip_single', {snip: snip.id}) }}" class="btn btn-primary">
|
||||
<i class="fa fa-arrow-left"></i> Back
|
||||
</a>
|
||||
<br><br>
|
||||
<table class="table table-sm table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Line</th>
|
||||
<th>Type</th>
|
||||
<th>Old</th>
|
||||
<th>New</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for line in diff %}
|
||||
<tr>
|
||||
<td>{{ line.line }}.</td>
|
||||
<td class="table-{{ line.type == 'insert' ? 'success' : (line.type == 'delete' ? 'danger' : 'info') }}">
|
||||
{{ line.type }}
|
||||
</td>
|
||||
<td>{{ line.from }}</td>
|
||||
<td>{{ line.to }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endblock %}
|
6
templates/form.html.twig
Normal file
6
templates/form.html.twig
Normal file
@ -0,0 +1,6 @@
|
||||
{% extends 'base/base.html.twig' %}
|
||||
|
||||
{% block body %}
|
||||
<h3>{{ message }}</h3>
|
||||
{{ form(form) }}
|
||||
{% endblock %}
|
@ -1,7 +0,0 @@
|
||||
{% extends 'base/one.column.html.twig' %}
|
||||
|
||||
{% set title %}{{ message }}{% endset %}
|
||||
|
||||
{% block body %}
|
||||
{{ form(form) }}
|
||||
{% endblock %}
|
@ -1,5 +0,0 @@
|
||||
{% extends 'base/one.column.html.twig' %}
|
||||
|
||||
{% block body %}
|
||||
{{ text | nl2br }}
|
||||
{% endblock %}
|
20
templates/history/index.html.twig
Normal file
20
templates/history/index.html.twig
Normal file
@ -0,0 +1,20 @@
|
||||
{% extends 'base/base.html.twig' %}
|
||||
|
||||
{% block title %}Snip {{ snip }}{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<a href="{{ path('snip_single', {snip: snip.id}) }}" class="btn btn-primary">
|
||||
<i class="fa fa-arrow-left"></i> Back
|
||||
</a>
|
||||
<a href="{{ path('history_set', {version: latestVersion, snip: snip.id}) }}" class="btn btn-warning">
|
||||
<i class="fa fa-refresh"></i> Latest
|
||||
</a>
|
||||
<br><br>
|
||||
<div class="list-group">
|
||||
{% for version in versions %}
|
||||
<a class="list-group-item" href="{{ path('history_set', {version: version.id, snip: snip.id}) }}">
|
||||
{{ version.name }} - {{ version.id }}
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endblock %}
|
@ -1,6 +1,6 @@
|
||||
{% extends 'base/one.column.html.twig' %}
|
||||
{% extends 'base/base.html.twig' %}
|
||||
|
||||
{% set title %}Login{% endset %}
|
||||
{% block title %}Login{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<form action="{{ path('login') }}" method="post">
|
||||
@ -12,6 +12,7 @@
|
||||
You are already logged in as {{ app.user }}, <a href="{{ path('logout') }}">Logout</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
<h1 class="h3 mb-3 font-weight-normal">Please login</h1>
|
||||
<label for="inputUsername">Username</label>
|
||||
<input type="text" value="{{ last_username }}" name="_username" id="inputUsername" class="form-control" required
|
||||
autofocus>
|
||||
|
@ -1,7 +1,8 @@
|
||||
{% extends 'base/one.column.html.twig' %}
|
||||
{% extends 'base/base.html.twig' %}
|
||||
|
||||
{% set title %}Register{% endset %}
|
||||
{% block title %}Register{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<h1 class="h3 mb-3 font-weight-normal">Register new user</h1>
|
||||
{{ form(registrationForm) }}
|
||||
{% endblock %}
|
5
templates/simple.html.twig
Normal file
5
templates/simple.html.twig
Normal file
@ -0,0 +1,5 @@
|
||||
{% extends 'base/base.html.twig' %}
|
||||
|
||||
{% block body %}
|
||||
{{ text | nl2br }}
|
||||
{% endblock %}
|
@ -1,13 +1,3 @@
|
||||
{% if snip.visible %}
|
||||
<span class="badge bg-success">
|
||||
<i class="fa fa-eye"></i>
|
||||
</span>
|
||||
{% else %}
|
||||
<span class="badge bg-secondary">
|
||||
<i class="fa fa-eye-slash"></i>
|
||||
</span>
|
||||
{% endif %}
|
||||
|
||||
{% if snip.public %}
|
||||
<span class="badge bg-info">
|
||||
<i class="fa fa-lock-open"></i>
|
||||
|
@ -1,10 +1,6 @@
|
||||
{% extends 'base/one.column.html.twig' %}
|
||||
{% extends 'base/base.html.twig' %}
|
||||
|
||||
{% if snip.id %}
|
||||
{% set title = 'Edit Snip ' ~ snip %}
|
||||
{% else %}
|
||||
{% set title = 'Create Snip' %}
|
||||
{% endif %}
|
||||
{% block title %}Edit {{ snip }}{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
{% if snip.id %}
|
||||
@ -17,5 +13,6 @@
|
||||
<i class="fa fa-list"></i>
|
||||
Index
|
||||
</a><br><br>
|
||||
<h2>Editing {{ snip }}</h2>
|
||||
{{ form(form) }}
|
||||
{% endblock %}
|
@ -1,24 +1,22 @@
|
||||
{% extends 'base/one.column.html.twig' %}
|
||||
{% extends 'base/base.html.twig' %}
|
||||
|
||||
{% set title = 'My Snips' %}
|
||||
{% block title %}{{ title }}{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<h1>{{ title }}</h1>
|
||||
<a class="btn btn-success" href="{{ path('snip_new') }}">
|
||||
<i class="fa fa-plus"></i> Add
|
||||
</a>
|
||||
{% if request.onlyVisible %}
|
||||
<a class="btn btn-secondary" href="{{ path('snip_index', {onlyVisible: false}) }}">Show hidden</a>
|
||||
{% else %}
|
||||
<a class="btn btn-secondary" href="{{ path('snip_index', {onlyVisible: true}) }}">Hide hidden</a>
|
||||
{% endif %}
|
||||
<br><br>
|
||||
</a><br><br>
|
||||
<div class="list-group">
|
||||
{% for snip in snips %}
|
||||
<a class="list-group-item d-flex justify-content-between" href="{{ path('snip_single', {snip: snip.id}) }}">
|
||||
<span>
|
||||
<a class="list-group-item" href="{{ path('snip_single', {snip: snip.id}) }}">
|
||||
{% if snip.createdBy == app.user %}
|
||||
{{ include('snip/badge.html.twig', {snip: snip}) }}
|
||||
{{ snip }}
|
||||
</span>
|
||||
{% endif %}
|
||||
{{ snip }}
|
||||
{% if snip.createdBy != app.user %}
|
||||
{{ include('user/badge.html.twig', {user: snip.createdBy}) }}
|
||||
{% endif %}
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
@ -1,16 +0,0 @@
|
||||
{% extends 'base/one.column.html.twig' %}
|
||||
|
||||
{% set title = 'Public Snips' %}
|
||||
|
||||
{% block body %}
|
||||
<div class="list-group">
|
||||
{% for snip in snips %}
|
||||
<a class="list-group-item d-flex justify-content-between" href="{{ path('snip_single', {snip: snip.id}) }}">
|
||||
<span>
|
||||
{{ snip }}
|
||||
</span>
|
||||
{{ include('user/badge.html.twig', {user: snip.createdBy}) }}
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endblock %}
|
@ -1,23 +1,17 @@
|
||||
{% extends 'base/one.column.html.twig' %}
|
||||
{% extends 'base/base.html.twig' %}
|
||||
|
||||
{% set title %}Snip {{ snip }}{% endset %}
|
||||
{% block title %}Snip {{ snip }}{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
{% if app.user %}
|
||||
<a href="{{ path('snip_index') }}" class="btn btn-primary">
|
||||
<i class="fa fa-arrow-left"></i> Back
|
||||
</a>
|
||||
{% else %}
|
||||
<a href="{{ path('snip_public') }}" class="btn btn-primary">
|
||||
<i class="fa fa-arrow-left"></i> Index
|
||||
</a>
|
||||
{% endif %}
|
||||
<a href="{{ path('snip_index') }}" class="btn btn-primary">
|
||||
<i class="fa fa-arrow-left"></i> Back
|
||||
</a>
|
||||
{% if is_granted('edit', snip) %}
|
||||
<a class="btn btn-warning" href="{{ path('snip_edit', {snip: snip.id}) }}">
|
||||
<i class="fa fa-pencil" aria-hidden="true"></i> Edit
|
||||
</a>
|
||||
<a class="btn btn-info" href="{{ path('version_index', {snip: snip.id}) }}">
|
||||
<i class="fa fa-history" aria-hidden="true"></i> Versions
|
||||
<a class="btn btn-info" href="{{ path('history_index', {snip: snip.id}) }}">
|
||||
<i class="fa fa-history" aria-hidden="true"></i> History
|
||||
</a>
|
||||
<a href="{{ path('snip_delete', {snip: snip.id}) }}" class="btn btn-danger">
|
||||
<i class="fa fa-trash"></i> Delete
|
||||
@ -32,19 +26,12 @@
|
||||
{{ include('snip/badge.html.twig', {snip: snip}) }}
|
||||
{{ snip }} <small class="text-muted">#{{ snip.id }}</small>
|
||||
</h4>
|
||||
<div class="card-header">
|
||||
<p class="card-text">Current version: {{ branch }}</p>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{{ content|raw }}
|
||||
</div>
|
||||
<div class="card-footer">
|
||||
<p class="card-text text-muted">
|
||||
Current version: {{ snip.activeVersion.id }}
|
||||
{% if snip.activeVersion == snip.latestVersion %}(latest){% endif %}
|
||||
|
||||
Created at {{ snip.activeVersion.id.dateTime|date('Y-m-d H:i:s') }}
|
||||
|
||||
{{ include('user/badge.html.twig', {user: snip.createdBy}) }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
|
@ -1 +1 @@
|
||||
<span class="badge {% if user == app.user %}bg-success{% else %}bg-secondary{% endif %}">{{ user }}</span>
|
||||
<span class="badge bg-secondary">{{ user }}</span>
|
@ -1,51 +1,23 @@
|
||||
{% extends 'base/two.column.html.twig' %}
|
||||
{% extends "base/base.html.twig" %}
|
||||
|
||||
{% set title = app.user.name %}
|
||||
|
||||
{% block column2 %}
|
||||
<h5>Change profile</h5>
|
||||
{{ form(form) }}
|
||||
{% endblock %}
|
||||
|
||||
{% block column1 %}
|
||||
<h5>Api</h5>
|
||||
<div class="input-group mb-3">
|
||||
<span class="input-group-text" id="api-key">Api Key</span>
|
||||
<input type="text" class="form-control" aria-label="Username" aria-describedby="api-key"
|
||||
value="{{ app.user.apiKey }}" readonly>
|
||||
<a type="button" class="btn btn-outline-secondary" href="{{ path('user_apikey_generate') }}">Regenerate</a> <br/>
|
||||
{% block body %}
|
||||
<div class="row">
|
||||
<div class="col-sm">
|
||||
<h4>{{ app.user.name }}</h4>
|
||||
<br/>
|
||||
{% if is_granted('ROLE_ADMIN') %}
|
||||
<br/><br/>
|
||||
<h4>Latest release stats</h4>
|
||||
Branch: {{ release.branch }} <br/>
|
||||
Date: {{ release.date }} <br/>
|
||||
Hash short: {{ release.commitHashShort }} <br/>
|
||||
Hash long: {{ release.commitHashLong }} <br/>
|
||||
Commit date: {{ release.commitDate }} <br/>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-sm">
|
||||
<h4>Change profile</h4>
|
||||
{{ form(form) }}
|
||||
</div>
|
||||
</div>
|
||||
{% if is_granted('ROLE_ADMIN') %}
|
||||
<h5>Latest release stats</h5>
|
||||
<table class="table table-bordered table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Property</th>
|
||||
<th>Value</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Branch</td>
|
||||
<td>{{ release.branch }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Date</td>
|
||||
<td>{{ release.date }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Hash short</td>
|
||||
<td>{{ release.commitHashShort }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Hash long</td>
|
||||
<td>{{ release.commitHashLong }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Commit date</td>
|
||||
<td>{{ release.commitDate }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
{% endif %}
|
||||
{% endblock %}
|
@ -1,23 +0,0 @@
|
||||
{% extends 'base/one.column.html.twig' %}
|
||||
|
||||
{% set title = 'Snip ' ~ snip %}
|
||||
|
||||
{% block body %}
|
||||
<a href="{{ path('snip_single', {snip: snip.id}) }}" class="btn btn-primary">
|
||||
<i class="fa fa-arrow-left"></i> Back
|
||||
</a>
|
||||
<a href="{{ path('version_set', {version: snip.latestVersion.id, snip: snip.id}) }}" class="btn btn-warning">
|
||||
<i class="fa fa-refresh"></i> Latest
|
||||
</a>
|
||||
<a href="{{ path('content_compare', {to: snip.activeVersion.id}) }}" class="btn btn-warning">
|
||||
<i class="fa fa-left-right"></i> Compare
|
||||
</a>
|
||||
<br><br>
|
||||
<div class="list-group">
|
||||
{% for version in snip.snipContents|reverse %}
|
||||
<a class="list-group-item {% if version.id == snip.activeVersion.id %}list-group-item-success{% endif %}" href="{{ path('version_set', {version: version.id, snip: snip.id}) }}">
|
||||
{{ version.id.dateTime|date('Y-m-d H:i:s') }} - {{ version.id }}
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endblock %}
|
Loading…
x
Reference in New Issue
Block a user