├── .styleci ├── logs └── .gitignore ├── web ├── favicon.ico ├── .htaccess ├── index.php └── app.js ├── images └── website_look.png ├── .gitignore ├── src ├── Domain │ ├── Paste │ │ ├── NotExistsException.php │ │ ├── AlreadyExistsException.php │ │ └── Id.php │ ├── XkcdRepository.php │ ├── PasteRepository.php │ ├── Xkcd.php │ ├── File.php │ └── Paste.php ├── config.yml.example ├── Application │ ├── Crypter │ │ ├── CrypterException.php │ │ └── PasteCrypter.php │ ├── InvalidDataException.php │ ├── Generator │ │ └── RandomIdGenerator.php │ ├── Service │ │ ├── CreatePastePayload.php │ │ └── CreatePasteService.php │ ├── Base10And62Converter.php │ └── Form │ │ └── CreatePasteFormValidator.php ├── routes.yml ├── Infrastructure │ ├── HttpsXkcdRepository.php │ ├── AES256Crypter.php │ └── Dbal │ │ ├── DbalPasteMapper.php │ │ └── DbalPasteRepository.php ├── Twig │ ├── TransExtension.php │ └── SymfonyValidatorExtension.php ├── Slim │ ├── Middleware │ │ └── SymfonySessionMiddleware.php │ ├── Handler │ │ └── LoggingErrorHandler.php │ └── DecoratingCallableResolver.php ├── UserInterface │ ├── Controller │ │ ├── ControllerDecorator.php │ │ └── AbstractController.php │ └── Web │ │ └── Controller │ │ ├── ErrorController.php │ │ └── PasteController.php └── AppKernel.php ├── package.json ├── resources ├── translations │ ├── messages.php │ └── messages.pl.php ├── views │ ├── error.twig │ ├── paste.twig │ ├── layout.twig │ └── home.twig └── sass │ └── style.scss ├── tests ├── Domain │ ├── XkcdTest.php │ ├── Paste │ │ └── IdTest.php │ ├── FileTest.php │ └── PasteTest.php ├── Application │ ├── RandomIdGeneratorTest.php │ ├── Base10And62ConverterTest.php │ ├── Form │ │ └── CreatePasteFormValidatorTest.php │ └── Service │ │ └── CreatePasteServiceTest.php ├── Twig │ ├── TransExtensionTest.php │ └── SymfonyValidatorExtensionTest.php ├── Slim │ ├── Middleware │ │ └── SymfonySessionMiddlewareTest.php │ ├── Handler │ │ └── LoggingErrorHandlerTest.php │ └── DecoratingCallableResolverTest.php ├── UserInterface │ └── Controller │ │ ├── AbstractControllerTest.php │ │ └── ControllerDecoratorTest.php └── Infrastructure │ ├── Dbal │ └── DbalPasteMapperTest.php │ └── AES256CrypterTest.php ├── schema.sql ├── .scrutinizer.yml ├── .travis.yml ├── phpunit.xml ├── composer.json ├── gulpfile.js ├── LICENSE └── README.md /.styleci: -------------------------------------------------------------------------------- 1 | preset: psr2 2 | -------------------------------------------------------------------------------- /logs/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /web/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nastoletni/code/HEAD/web/favicon.ico -------------------------------------------------------------------------------- /images/website_look.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nastoletni/code/HEAD/images/website_look.png -------------------------------------------------------------------------------- /web/.htaccess: -------------------------------------------------------------------------------- 1 | RewriteEngine On 2 | RewriteCond %{REQUEST_FILENAME} !-f 3 | RewriteCond %{REQUEST_FILENAME} !-d 4 | RewriteRule ^ index.php [QSA,L] 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea/ 2 | .DS_STORE 3 | 4 | /node_modules/ 5 | /vendor/ 6 | /build/ 7 | /src/config.yml 8 | /web/style.css 9 | /web/*.css 10 | /web/*.min.js 11 | -------------------------------------------------------------------------------- /web/index.php: -------------------------------------------------------------------------------- 1 | handle(); 12 | -------------------------------------------------------------------------------- /src/Domain/Paste/NotExistsException.php: -------------------------------------------------------------------------------- 1 | [ 5 | 'title' => [ 6 | 'not_null' => 'Title is required' 7 | ], 8 | 'name' => [ 9 | 'not_null' => 'Name is required' 10 | ], 11 | 'content' => [ 12 | 'not_blank' => 'Content is required' 13 | ] 14 | ] 15 | ]; -------------------------------------------------------------------------------- /resources/translations/messages.pl.php: -------------------------------------------------------------------------------- 1 | [ 5 | 'title' => [ 6 | 'not_null' => 'Pole tytuł jest wymagane' 7 | ], 8 | 'name' => [ 9 | 'not_null' => 'Pole nazwa jest wymagane' 10 | ], 11 | 'content' => [ 12 | 'not_blank' => 'Pole treść jest wymagane' 13 | ] 14 | ] 15 | ]; -------------------------------------------------------------------------------- /resources/views/error.twig: -------------------------------------------------------------------------------- 1 | {% extends 'layout.twig' %} 2 | 3 | {% block main %} 4 |
5 |

Błąd {{ error }}

6 | 7 | Komiks z xkcd - {{ xkcdImage.alternateText }} 8 | 9 |
10 | {% endblock %} 11 | -------------------------------------------------------------------------------- /src/routes.yml: -------------------------------------------------------------------------------- 1 | home: 2 | method: GET 3 | path: / 4 | controller: 'Nastoletni\Code\UserInterface\Web\Controller\PasteController:home' 5 | 6 | create: 7 | method: POST 8 | path: / 9 | controller: 'Nastoletni\Code\UserInterface\Web\Controller\PasteController:create' 10 | 11 | paste: 12 | method: GET 13 | path: /{id}/{key} 14 | controller: 'Nastoletni\Code\UserInterface\Web\Controller\PasteController:paste' 15 | -------------------------------------------------------------------------------- /tests/Domain/XkcdTest.php: -------------------------------------------------------------------------------- 1 | assertEquals('url', $xkcd->getUrl()); 14 | $this->assertEquals('image', $xkcd->getImageUrl()); 15 | $this->assertEquals('alternate', $xkcd->getAlternateText()); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /tests/Application/RandomIdGeneratorTest.php: -------------------------------------------------------------------------------- 1 | getBase10Id(); 15 | 16 | $this->assertNotContains($id, $history); 17 | $history[] = $id; 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Application/Generator/RandomIdGenerator.php: -------------------------------------------------------------------------------- 1 | createMock(TranslatorInterface::class)); 14 | /** @var \Twig_SimpleFilter $filter */ 15 | $filter = $extension->getFilters()[0]; 16 | 17 | $this->assertInstanceOf(\Twig_SimpleFilter::class, $filter); 18 | $this->assertEquals('trans', $filter->getName()); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /.scrutinizer.yml: -------------------------------------------------------------------------------- 1 | filter: 2 | excluded_paths: [tests/*] 3 | 4 | checks: 5 | php: 6 | remove_extra_empty_lines: true 7 | remove_php_closing_tag: true 8 | remove_trailing_whitespace: true 9 | fix_use_statements: 10 | remove_unused: true 11 | preserve_multiple: false 12 | preserve_blanklines: true 13 | order_alphabetically: true 14 | fix_php_opening_tag: true 15 | fix_linefeed: true 16 | fix_line_ending: true 17 | fix_identation_4spaces: true 18 | fix_doc_comments: true 19 | 20 | tools: 21 | external_code_coverage: true 22 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | php: 4 | - 7.1 5 | 6 | # This triggers builds to run on the new TravisCI infrastructure. 7 | # See: http://docs.travis-ci.com/user/workers/container-based-infrastructure/ 8 | sudo: false 9 | 10 | ## Cache composer 11 | cache: 12 | directories: 13 | - $HOME/.composer/cache 14 | 15 | before_script: 16 | - travis_retry composer update ${COMPOSER_FLAGS} --no-interaction --prefer-dist 17 | 18 | script: 19 | - vendor/bin/phpunit --coverage-text --coverage-clover=coverage.clover 20 | 21 | after_script: 22 | - wget https://scrutinizer-ci.com/ocular.phar 23 | - php ocular.phar code-coverage:upload --format=php-clover coverage.clover 24 | -------------------------------------------------------------------------------- /tests/Domain/Paste/IdTest.php: -------------------------------------------------------------------------------- 1 | assertEquals(17, $id->getBase10Id()); 14 | $this->assertEquals('h', $id->getBase62Id()); 15 | } 16 | 17 | public function testBase62FabricMethodAndGetters() 18 | { 19 | $id = Id::createFromBase62('CO2'); 20 | 21 | $this->assertEquals(149174, $id->getBase10Id()); 22 | $this->assertEquals('CO2', $id->getBase62Id()); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Application/Crypter/PasteCrypter.php: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 14 | ./tests/ 15 | 16 | 17 | 18 | 19 | ./src/ 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/Domain/PasteRepository.php: -------------------------------------------------------------------------------- 1 | =7.1", 6 | "ext-openssl": "*", 7 | "slim/slim": "^3.8", 8 | "slim/twig-view": "^2.2", 9 | "doctrine/dbal": "^2.5", 10 | "monolog/monolog": "^1.22", 11 | "symfony/yaml": "^3.2", 12 | "symfony/validator": "^3.3", 13 | "symfony/http-foundation": "^3.3", 14 | "symfony/translation": "^3.3" 15 | }, 16 | "require-dev": { 17 | "phpunit/phpunit": "^6.1", 18 | "symfony/dom-crawler": "^3.2" 19 | }, 20 | "autoload": { 21 | "psr-4": { 22 | "Nastoletni\\Code\\": "src/" 23 | } 24 | }, 25 | "autoload-dev": { 26 | "psr-4": { 27 | "Nastoletni\\Code\\": "tests/" 28 | } 29 | }, 30 | "license": "MIT", 31 | "scripts": { 32 | "test": [ 33 | "\"vendor/bin/phpunit\"" 34 | ] 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /tests/Application/Base10And62ConverterTest.php: -------------------------------------------------------------------------------- 1 | assertEquals($base62, Base10And62Converter::base10To62($base10)); 15 | } 16 | 17 | /** 18 | * @dataProvider baseConverterProvider 19 | */ 20 | public function testBase62ToBase10($base10, $base62) 21 | { 22 | $this->assertEquals($base10, Base10And62Converter::base62To10($base62)); 23 | } 24 | 25 | public function baseConverterProvider() 26 | { 27 | return [ 28 | [1, '1'], 29 | [2, '2'], 30 | [10, 'a'], 31 | [36, 'A'], 32 | [62, '10'], 33 | [72, '1a'], 34 | [99, '1B'], 35 | ]; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /tests/Slim/Middleware/SymfonySessionMiddlewareTest.php: -------------------------------------------------------------------------------- 1 | createMock(Session::class); 15 | $sessionMock 16 | ->expects($this->once()) 17 | ->method('start'); 18 | 19 | $middleware = new SymfonySessionMiddleware($sessionMock); 20 | 21 | $middleware( 22 | $this->createMock(ServerRequestInterface::class), 23 | $this->createMock(ResponseInterface::class), 24 | function (ServerRequestInterface $request, ResponseInterface $response) { 25 | return $response; 26 | } 27 | ); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Application/Service/CreatePastePayload.php: -------------------------------------------------------------------------------- 1 | paste = $paste; 30 | $this->encryptionKey = $encryptionKey; 31 | } 32 | 33 | /** 34 | * @return Paste 35 | */ 36 | public function getPaste(): Paste 37 | { 38 | return $this->paste; 39 | } 40 | 41 | /** 42 | * @return string 43 | */ 44 | public function getEncryptionKey(): string 45 | { 46 | return $this->encryptionKey; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Twig/TransExtension.php: -------------------------------------------------------------------------------- 1 | translator = $translator; 24 | } 25 | 26 | /** 27 | * {@inheritdoc} 28 | */ 29 | public function getFilters() 30 | { 31 | return [ 32 | new \Twig_SimpleFilter('trans', [$this, 'trans']) 33 | ]; 34 | } 35 | 36 | /** 37 | * Translates given message using Translation component. 38 | * 39 | * @param $message 40 | * 41 | * @return string 42 | */ 43 | public function trans($message) 44 | { 45 | return $this->translator->trans($message); 46 | } 47 | } -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | const gulp = require('gulp'), 2 | sass = require('gulp-sass'), 3 | autoprefixer = require('gulp-autoprefixer'), 4 | csso = require('gulp-csso'); 5 | 6 | gulp.task('default', ['sass', 'css', 'js']); 7 | 8 | gulp.task('sass', () => { 9 | return gulp.src('./resources/sass/style.scss') 10 | .pipe(sass().on('error', sass.logError)) 11 | .pipe(autoprefixer()) 12 | .pipe(csso()) 13 | .pipe(gulp.dest('./web/')); 14 | }); 15 | 16 | gulp.task('sass:watch', () => { 17 | gulp.watch('./resources/sass/**/*.scss', ['sass']); 18 | }); 19 | 20 | gulp.task('css', () => { 21 | return gulp.src([ 22 | './node_modules/highlightjs/styles/mono-blue.css' 23 | ]) 24 | .pipe(csso()) 25 | .pipe(gulp.dest('./web/')); 26 | }); 27 | 28 | gulp.task('js', () => { 29 | return gulp.src([ 30 | './node_modules/timeago.js/dist/timeago.min.js', 31 | './node_modules/timeago.js/dist/timeago.locales.min.js', 32 | './node_modules/highlightjs/highlight.pack.min.js' 33 | ]) 34 | .pipe(gulp.dest('./web/')); 35 | }); 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Albert Wolszon 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/Slim/Middleware/SymfonySessionMiddleware.php: -------------------------------------------------------------------------------- 1 | session = $session; 26 | } 27 | 28 | /** 29 | * Starts session. 30 | * 31 | * @param ServerRequestInterface $request 32 | * @param ResponseInterface $response 33 | * @param callable $next 34 | * 35 | * @return ResponseInterface 36 | */ 37 | public function __invoke(ServerRequestInterface $request, ResponseInterface $response, callable $next): ResponseInterface 38 | { 39 | $this->session->start(); 40 | 41 | return $next($request, $response); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /tests/Domain/FileTest.php: -------------------------------------------------------------------------------- 1 | assertEquals($filename, $file->getFilename()); 17 | $this->assertEquals($content, $file->getContent()); 18 | } 19 | 20 | /** 21 | * @dataProvider parameterProvider 22 | */ 23 | public function testSettersAndGetters($filename, $content) 24 | { 25 | $file = new File('test.txt', 'test'); 26 | 27 | $file->setFilename($filename); 28 | $file->setContent($content); 29 | 30 | $this->assertEquals($filename, $file->getFilename()); 31 | $this->assertEquals($content, $file->getContent()); 32 | } 33 | 34 | public function parameterProvider() 35 | { 36 | return [ 37 | [null, 'test'], 38 | ['', 'test2'], 39 | ['test', 'test3'], 40 | ['test.txt', 'test4'], 41 | ['.test', 'test5'], 42 | ]; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Domain/Xkcd.php: -------------------------------------------------------------------------------- 1 | url = $url; 34 | $this->imageUrl = $imageUrl; 35 | $this->alternateText = $alternateText; 36 | } 37 | 38 | /** 39 | * @return string 40 | */ 41 | public function getUrl(): string 42 | { 43 | return $this->url; 44 | } 45 | 46 | /** 47 | * @return string 48 | */ 49 | public function getImageUrl(): string 50 | { 51 | return $this->imageUrl; 52 | } 53 | 54 | /** 55 | * @return string 56 | */ 57 | public function getAlternateText(): string 58 | { 59 | return $this->alternateText; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Twig/SymfonyValidatorExtension.php: -------------------------------------------------------------------------------- 1 | getPropertyPath() == $field) { 40 | yield $error->getMessage(); 41 | } 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Domain/File.php: -------------------------------------------------------------------------------- 1 | filename = $filename; 28 | $this->content = $content; 29 | } 30 | 31 | /** 32 | * @return null|string 33 | */ 34 | public function getFilename(): ?string 35 | { 36 | return $this->filename; 37 | } 38 | 39 | /** 40 | * @param null|string $filename 41 | */ 42 | public function setFilename($filename): void 43 | { 44 | $this->filename = $filename; 45 | } 46 | 47 | /** 48 | * @return string 49 | */ 50 | public function getContent(): string 51 | { 52 | return $this->content; 53 | } 54 | 55 | /** 56 | * @param string $content 57 | */ 58 | public function setContent(string $content): void 59 | { 60 | $this->content = $content; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /tests/UserInterface/Controller/AbstractControllerTest.php: -------------------------------------------------------------------------------- 1 | twig; 18 | } 19 | 20 | public function getRouter() 21 | { 22 | return $this->router; 23 | } 24 | 25 | public function getSession() 26 | { 27 | return $this->session; 28 | } 29 | }; 30 | 31 | $twig = $this->createMock(Twig::class); 32 | $router = $this->createMock(RouterInterface::class); 33 | $session = $this->createMock(Session::class); 34 | 35 | $controller->pseudoConstructor($twig, $router, $session); 36 | 37 | $this->assertEquals($twig, $controller->getTwig()); 38 | $this->assertEquals($router, $controller->getRouter()); 39 | $this->assertEquals($session, $controller->getSession()); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/UserInterface/Controller/ControllerDecorator.php: -------------------------------------------------------------------------------- 1 | twig = $twig; 38 | $this->router = $router; 39 | $this->session = $session; 40 | } 41 | 42 | /** 43 | * Decorates controller with common to all controllers dependencies. 44 | * 45 | * @param AbstractController $controller 46 | */ 47 | public function decorate(AbstractController $controller): void 48 | { 49 | $controller->pseudoConstructor($this->twig, $this->router, $this->session); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /tests/Twig/SymfonyValidatorExtensionTest.php: -------------------------------------------------------------------------------- 1 | getFunctions()[0]; 16 | 17 | $this->assertInstanceOf(\Twig_SimpleFunction::class, $function); 18 | $this->assertEquals('error', $function->getName()); 19 | } 20 | 21 | public function testErrorWithNull() 22 | { 23 | $extension = new SymfonyValidatorExtension(); 24 | 25 | $this->assertCount(0, $extension->error('test', null)); 26 | } 27 | 28 | public function testErrorWithViolationList() 29 | { 30 | $extension = new SymfonyValidatorExtension(); 31 | $violationList = new ConstraintViolationList([ 32 | new ConstraintViolation('test', 'test', [], null, 'foobar', ''), 33 | ]); 34 | 35 | $errors = $extension->error('foobar', $violationList); 36 | 37 | $this->assertContains('test', $errors); 38 | $this->assertCount(1, $errors); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /tests/UserInterface/Controller/ControllerDecoratorTest.php: -------------------------------------------------------------------------------- 1 | createMock(Twig::class); 15 | $router = $this->createMock(RouterInterface::class); 16 | $session = $this->createMock(Session::class); 17 | $decorator = new ControllerDecorator($twig, $router, $session); 18 | 19 | $controller = new class() extends AbstractController { 20 | public function getTwig() 21 | { 22 | return $this->twig; 23 | } 24 | 25 | public function getRouter() 26 | { 27 | return $this->router; 28 | } 29 | 30 | public function getSession() 31 | { 32 | return $this->session; 33 | } 34 | }; 35 | 36 | $decorator->decorate($controller); 37 | 38 | $this->assertEquals($twig, $controller->getTwig()); 39 | $this->assertEquals($router, $controller->getRouter()); 40 | $this->assertEquals($session, $controller->getSession()); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Domain/Paste/Id.php: -------------------------------------------------------------------------------- 1 | id = $id; 24 | } 25 | 26 | /** 27 | * Creates Id from ordinary, 10 base number [0-9]. 28 | * 29 | * @param int $id 30 | * 31 | * @return Id 32 | */ 33 | public static function createFromBase10(int $id): Id 34 | { 35 | return new static($id); 36 | } 37 | 38 | /** 39 | * Creates Id from base 62 number [0-9a-zA-Z]. 40 | * 41 | * @param string $id 42 | * 43 | * @return Id 44 | */ 45 | public static function createFromBase62(string $id): Id 46 | { 47 | return new static(Base10And62Converter::base62To10($id)); 48 | } 49 | 50 | /** 51 | * @return int 52 | */ 53 | public function getBase10Id(): int 54 | { 55 | return $this->id; 56 | } 57 | 58 | /** 59 | * @return string 60 | */ 61 | public function getBase62Id(): string 62 | { 63 | return Base10And62Converter::base10To62($this->id); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/Application/Base10And62Converter.php: -------------------------------------------------------------------------------- 1 | logger = $logger; 33 | $this->nextHandler = $nextHandler; 34 | } 35 | 36 | /** 37 | * Logs exceptions to logger. 38 | * 39 | * @param Request $request 40 | * @param Response $response 41 | * @param Throwable $exception 42 | * 43 | * @return Response 44 | */ 45 | public function __invoke(Request $request, Response $response, Throwable $exception): Response 46 | { 47 | $this->logger->critical($exception); 48 | 49 | if (!is_null($this->nextHandler)) { 50 | return ($this->nextHandler)($request, $response, $exception); 51 | } 52 | 53 | return $response; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Application/Form/CreatePasteFormValidator.php: -------------------------------------------------------------------------------- 1 | new Assert\NotNull(['message' => 'validator.title.not_null']), 43 | 'name' => new Assert\All([ 44 | new Assert\NotNull(['message' => 'validator.name.not_null']), 45 | ]), 46 | 'content' => new Assert\All([ 47 | new Assert\NotBlank(['message' => 'validator.content.not_blank']), 48 | ]), 49 | ]); 50 | 51 | return $validator->validate($data, $constraint); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/UserInterface/Controller/AbstractController.php: -------------------------------------------------------------------------------- 1 | twig = $twig; 38 | $this->router = $router; 39 | $this->session = $session; 40 | } 41 | 42 | /** 43 | * Adds given value to the flashed variables bag. 44 | * 45 | * @param string $name 46 | * @param $value 47 | */ 48 | protected function flash(string $name, $value): void 49 | { 50 | $this->session->getFlashBag()->add($name, $value); 51 | } 52 | 53 | /** 54 | * Returns flashed value with given name. 55 | * 56 | * @param string $name 57 | * 58 | * @return mixed|null 59 | */ 60 | protected function getFlash(string $name) 61 | { 62 | if (false == $this->session->getFlashBag()->has($name)) { 63 | return; 64 | } 65 | 66 | $flash = $this->session->getFlashBag()->get($name); 67 | 68 | return $flash[0] ?? null; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /tests/Slim/Handler/LoggingErrorHandlerTest.php: -------------------------------------------------------------------------------- 1 | createMock(LoggerInterface::class); 16 | $logger 17 | ->expects($this->once()) 18 | ->method('critical'); 19 | 20 | $loggingErrorHandler = new LoggingErrorHandler($logger); 21 | 22 | $invokeParams = $this->createInvokeParamsMocks(); 23 | $response = $loggingErrorHandler(...$invokeParams); 24 | 25 | $this->assertInstanceOf(ResponseInterface::class, $response); 26 | } 27 | 28 | public function testInvokeWithNextHandler() 29 | { 30 | $logger = $this->createMock(LoggerInterface::class); 31 | 32 | $invokeParams = $this->createInvokeParamsMocks(); 33 | 34 | $called = false; 35 | $nextHandler = function ( 36 | ServerRequestInterface $request, 37 | ResponseInterface $response, 38 | Exception $exception 39 | ) use (&$called) { 40 | $called = true; 41 | 42 | return $response; 43 | }; 44 | 45 | $loggingErrorHandler = new LoggingErrorHandler($logger, $nextHandler); 46 | $loggingErrorHandler(...$invokeParams); 47 | 48 | $this->assertTrue($called); 49 | } 50 | 51 | private function createInvokeParamsMocks() 52 | { 53 | $request = $this->createMock(ServerRequestInterface::class); 54 | $response = $this->createMock(ResponseInterface::class); 55 | $exception = new Exception(); 56 | 57 | return [$request, $response, $exception]; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /resources/sass/style.scss: -------------------------------------------------------------------------------- 1 | @import './../../node_modules/nastoletni-sass/src/style'; 2 | 3 | html { 4 | background: #fff; 5 | } 6 | 7 | body { 8 | display: flex; 9 | flex-direction: column; 10 | min-height: 100vh; 11 | } 12 | 13 | .page-navbar__inverse { 14 | color: $primaryColor; 15 | background: none; 16 | line-height: 80px; 17 | 18 | .page-navbar--title-link, 19 | .page-navbar-nav--link { 20 | color: $primaryColor; 21 | 22 | &:hover { 23 | background: none; 24 | } 25 | } 26 | 27 | @media (max-width: 800px) { 28 | .page-navbar-nav--link { 29 | color: #fff; 30 | } 31 | } 32 | } 33 | 34 | .lead-block { 35 | margin-bottom: 0; 36 | padding: 50px 15px; 37 | color: #fff; 38 | background: $primaryColor; 39 | 40 | &--container { 41 | @extend %container; 42 | } 43 | } 44 | 45 | .main { 46 | flex: 1; 47 | width: 100%; 48 | } 49 | 50 | .code-form { 51 | &--label { 52 | margin-bottom: 5px !important; 53 | } 54 | 55 | &--control-group { 56 | display: flex; 57 | flex-direction: row; 58 | } 59 | 60 | &--control-group-fill { 61 | flex: 1; 62 | margin-right: 15px; 63 | 64 | @media (max-width: $mobileWidth) { 65 | margin-right: 5px; 66 | } 67 | } 68 | 69 | &--control { 70 | border: 2px solid #ced9e4; 71 | } 72 | 73 | &--button { 74 | border: 0; 75 | padding: 12px 24px; 76 | 77 | &__delete { 78 | $red: #e74c3c; 79 | margin-left: 0; 80 | align-self: flex-start; 81 | flex: 0 0 auto; 82 | background: $red; 83 | color: #fff; 84 | 85 | &:hover, &:focus { 86 | background: darken($red, 15%); 87 | } 88 | } 89 | } 90 | } 91 | 92 | .fullwidth-ad { 93 | margin-bottom: 30px; 94 | } 95 | 96 | .footer { 97 | padding: 50px 0; 98 | background: #333; 99 | color: #fff; 100 | 101 | a { 102 | color: #fff; 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /resources/views/paste.twig: -------------------------------------------------------------------------------- 1 | {% extends 'layout.twig' %} 2 | 3 | {% set title = paste.title is not empty ? paste.title : 'Podgląd wklejki ' ~ paste.id.base62Id %} 4 | 5 | {% block title %}{{ title }} - {{ parent() }}{% endblock %} 6 | 7 | {% block head %} 8 | 9 | 10 | 11 | {{ parent() }} 12 | {% endblock %} 13 | 14 | {% block main %} 15 |
16 |

17 | {{ paste.title is not empty ? paste.title : 'Bez nazwy' }} 18 | 21 |

22 | {% for file in paste.files %} 23 |
24 |
25 |

{{ file.filename|default('Bez tytułu') }}

26 |
27 |
{{ file.content }}
28 |
29 | {% endfor %} 30 |
31 | {% endblock %} 32 | 33 | {% block js %} 34 | 35 | 36 | 37 | 42 | {% endblock %} 43 | -------------------------------------------------------------------------------- /src/Slim/DecoratingCallableResolver.php: -------------------------------------------------------------------------------- 1 | callableResolver = new CallableResolver($container); 36 | $this->controllerDecorator = $controllerDecorator; 37 | } 38 | 39 | /** 40 | * Invokes resolved callable and decorates it if is a subclass 41 | * of AbstractController. 42 | * 43 | * @param mixed $toResolve 44 | * 45 | * @return callable 46 | */ 47 | public function resolve($toResolve): callable 48 | { 49 | $resolved = $this->callableResolver->resolve($toResolve); 50 | 51 | // Check against resolved value for callable class or against first 52 | // value for callable array. 53 | if (($controller = $resolved) instanceof AbstractController 54 | || is_array($controller) && ($controller = $resolved[0]) instanceof AbstractController) { 55 | /* @var $controller AbstractController */ 56 | $this->controllerDecorator->decorate($controller); 57 | } 58 | 59 | return $resolved; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Domain/Paste.php: -------------------------------------------------------------------------------- 1 | id = $id; 41 | $this->title = $title; 42 | $this->createdAt = $createdAt; 43 | } 44 | 45 | /** 46 | * @return Paste\Id 47 | */ 48 | public function getId(): Paste\Id 49 | { 50 | return $this->id; 51 | } 52 | 53 | /** 54 | * @return null|string 55 | */ 56 | public function getTitle(): ?string 57 | { 58 | return $this->title; 59 | } 60 | 61 | /** 62 | * @param null|string $title 63 | */ 64 | public function setTitle($title): void 65 | { 66 | $this->title = $title; 67 | } 68 | 69 | /** 70 | * @return DateTime 71 | */ 72 | public function getCreatedAt(): DateTime 73 | { 74 | return $this->createdAt; 75 | } 76 | 77 | /** 78 | * @param DateTime $createdAt 79 | */ 80 | public function setCreatedAt(DateTime $createdAt): void 81 | { 82 | $this->createdAt = $createdAt; 83 | } 84 | 85 | /** 86 | * @return File[] 87 | */ 88 | public function getFiles(): array 89 | { 90 | return $this->files; 91 | } 92 | 93 | /** 94 | * @param File $file 95 | */ 96 | public function addFile(File $file): void 97 | { 98 | $this->files[] = $file; 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/UserInterface/Web/Controller/ErrorController.php: -------------------------------------------------------------------------------- 1 | xkcdRepository = $xkcdRepository; 28 | } 29 | 30 | /** 31 | * Handling 404 Not found. 32 | * 33 | * @param Request $request 34 | * @param Response $response 35 | * 36 | * @return Response 37 | */ 38 | public function notFound(Request $request, Response $response): Response 39 | { 40 | return $this->render($response, 404); 41 | } 42 | 43 | /** 44 | * Handling 500 Internal server error. 45 | * 46 | * @param Request $request 47 | * @param Response $response 48 | * @param Throwable $exception 49 | * 50 | * @return Response 51 | */ 52 | public function error(Request $request, Response $response, Throwable $exception): Response 53 | { 54 | return $this->render($response, 500); 55 | } 56 | 57 | /** 58 | * Takes care of all common things to these error pages, such as image from xkcd. 59 | * 60 | * @param Response $response 61 | * @param int $error 62 | * 63 | * @return Response 64 | */ 65 | private function render(Response $response, int $error): Response 66 | { 67 | return $this->twig->render($response, 'error.twig', [ 68 | 'error' => $error, 69 | 'xkcdImage' => $this->xkcdRepository->getRandom(), 70 | ]) 71 | ->withStatus($error); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /tests/Domain/PasteTest.php: -------------------------------------------------------------------------------- 1 | assertEquals($id, $paste->getId()); 17 | $this->assertEquals($title, $paste->getTitle()); 18 | $this->assertEquals($createdAt, $paste->getCreatedAt()); 19 | $this->assertEmpty($paste->getFiles()); 20 | } 21 | 22 | /** 23 | * @dataProvider parametersProvider 24 | */ 25 | public function testSettersAndGetters($id, $title, $createdAt) 26 | { 27 | $paste = new Paste( 28 | $this->createMock(Paste\Id::class), 29 | 'test', 30 | new \DateTime() 31 | ); 32 | 33 | $paste->setTitle($title); 34 | $paste->setCreatedAt($createdAt); 35 | 36 | $this->assertEquals($title, $paste->getTitle()); 37 | $this->assertEquals($createdAt, $paste->getCreatedAt()); 38 | } 39 | 40 | public function parametersProvider() 41 | { 42 | return [ 43 | [ 44 | $this->createMock(Paste\Id::class), 45 | 'Example', 46 | new \DateTime(), 47 | ], 48 | [ 49 | $this->createMock(Paste\Id::class), 50 | '', 51 | new \DateTime('+1 day'), 52 | ], 53 | [ 54 | $this->createMock(Paste\Id::class), 55 | null, 56 | new \DateTime('-1 year'), 57 | ], 58 | ]; 59 | } 60 | 61 | public function testAddingFile() 62 | { 63 | $paste = new Paste( 64 | $this->createMock(Paste\Id::class), 65 | 'test', 66 | new \DateTime() 67 | ); 68 | 69 | $file = $this->createMock(File::class); 70 | $paste->addFile($file); 71 | 72 | $this->assertContains($file, $paste->getFiles()); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /resources/views/layout.twig: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | {% block title %}Code{% endblock %} 7 | 8 | 9 | {% block head '' %} 10 | 11 | 12 | 30 | {% block main '' %} 31 | 42 | {% block js '' %} 43 | 44 | 45 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # nastoletni/code 2 | 3 | [![Software License][ico-license]](LICENSE.md) 4 | [![Build Status][ico-travis]][link-travis] 5 | [![Coverage Status][ico-scrutinizer]][link-scrutinizer] 6 | [![Quality Score][ico-code-quality]][link-code-quality] 7 | [![StyleCI][ico-styleci]][link-styleci] 8 | 9 | #### Visit [code.nastoletni.pl](https://code.nastoletni.pl/) and create your own paste! 10 | 11 | This is open source pastebin-like website with features like multifile pastes and drag'n'drop. It keeps privacy of pastes, each file is being encrypted using AES-256-CBC and the only person (*or computer*) that knows the key is you. 12 | 13 | We aim with this tool mainly for developers in mind and we want to keep it as usable and simple in use as we can. If you have any idea that could be implemented, [create a new issue](https://github.com/nastoletni/code/issues/new) and describe it. Any kind of feedback is welcome! 14 | 15 | ![Website look](/images/website_look.png) 16 | 17 | ## Install 18 | 19 | 1. Download Composer dependencies `composer install` 20 | 2. Install npm dependencies and compile sass `npm install && gulp` 21 | 3. Create config file with `cp src/config.yml.example src/config.yml` and fill it. 22 | 3. Populate database with schema from *schema.sql* 23 | 24 | That's it. 25 | 26 | ## Security 27 | 28 | If you discover any security related issues, please email [w.albert221@gmail.com](mailto:w.albert221@gmail.com) instead of using the issue tracker. 29 | 30 | ## License 31 | 32 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 33 | 34 | [ico-license]: https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square 35 | [ico-travis]: https://img.shields.io/travis/nastoletni/code/master.svg?style=flat-square 36 | [ico-scrutinizer]: https://img.shields.io/scrutinizer/coverage/g/nastoletni/code.svg?style=flat-square 37 | [ico-code-quality]: https://img.shields.io/scrutinizer/g/nastoletni/code.svg?style=flat-square 38 | [ico-styleci]: https://styleci.io/repos/92681743/shield?branch=master 39 | 40 | [link-travis]: https://travis-ci.org/nastoletni/code 41 | [link-scrutinizer]: https://scrutinizer-ci.com/g/nastoletni/code/code-structure 42 | [link-code-quality]: https://scrutinizer-ci.com/g/nastoletni/code 43 | [link-styleci]: https://styleci.io/repos/92681743 44 | -------------------------------------------------------------------------------- /src/Application/Service/CreatePasteService.php: -------------------------------------------------------------------------------- 1 | pasteRepository = $pasteRepository; 34 | $this->pasteCrypter = $pasteCrypter; 35 | } 36 | 37 | /** 38 | * Creates Paste and File entities populating it with unique id and then 39 | * saves it to repository. 40 | * 41 | * @param array $data 42 | * 43 | * @return CreatePastePayload 44 | */ 45 | public function handle(array $data): CreatePastePayload 46 | { 47 | // Generate pretty for eye key that will be lately used for encrypting. 48 | $key = dechex(random_int(0x10000000, 0xFFFFFFFF)); 49 | 50 | do { 51 | $paste = new Paste( 52 | RandomIdGenerator::nextId(), 53 | $data['title'], 54 | new \DateTime() 55 | ); 56 | 57 | foreach ($data['content'] as $i => $content) { 58 | $name = $data['name'][$i]; 59 | 60 | $paste->addFile(new File($name, $content)); 61 | } 62 | 63 | $this->pasteCrypter->encrypt($paste, $key); 64 | 65 | $alreadyUsed = false; 66 | try { 67 | $this->pasteRepository->save($paste); 68 | } catch (Paste\AlreadyExistsException $e) { 69 | $alreadyUsed = true; 70 | } 71 | } while ($alreadyUsed); 72 | 73 | return new CreatePastePayload($paste, $key); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/Infrastructure/AES256Crypter.php: -------------------------------------------------------------------------------- 1 | keyToEncryptionKey($key); 21 | 22 | foreach ($paste->getFiles() as $file) { 23 | $iv = openssl_random_pseudo_bytes(openssl_cipher_iv_length(static::CIPHER)); 24 | 25 | $encrypted = openssl_encrypt( 26 | $file->getContent(), 27 | static::CIPHER, 28 | $key, 29 | 0, 30 | $iv 31 | ); 32 | $iv = base64_encode($iv); 33 | 34 | if (false === $encrypted) { 35 | throw new CrypterException('[OpenSSL] '.openssl_error_string()); 36 | } 37 | 38 | $file->setContent($encrypted.':'.$iv); 39 | } 40 | } 41 | 42 | /** 43 | * {@inheritdoc} 44 | */ 45 | public function decrypt(Paste &$paste, string $key): void 46 | { 47 | $key = $this->keyToEncryptionKey($key); 48 | 49 | foreach ($paste->getFiles() as $file) { 50 | [$encrypted, $iv] = explode(':', $file->getContent()); 51 | 52 | $iv = base64_decode($iv); 53 | $decrypted = openssl_decrypt( 54 | $encrypted, 55 | static::CIPHER, 56 | $key, 57 | 0, 58 | $iv 59 | ); 60 | 61 | if (false === $decrypted) { 62 | throw new CrypterException('[OpenSSL] '.openssl_error_string()); 63 | } 64 | 65 | $file->setContent($decrypted); 66 | } 67 | } 68 | 69 | /** 70 | * Translates key that varies in length to 256bit (32 bytes) encryption key. 71 | * 72 | * @param string $key 73 | * 74 | * @return string 75 | */ 76 | private function keyToEncryptionKey(string $key): string 77 | { 78 | return mb_substr(sha1($key, true), 0, 32); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/Infrastructure/Dbal/DbalPasteMapper.php: -------------------------------------------------------------------------------- 1 | validateData($data); 24 | 25 | $paste = new Paste( 26 | Paste\Id::createFromBase10((int) $data[0]['id']), 27 | empty($data[0]['title']) ? null : $data[0]['title'], 28 | new DateTime($data[0]['created_at']) 29 | ); 30 | 31 | foreach ($data as $file) { 32 | $file = new File( 33 | empty($file['filename']) ? null : $file['filename'], 34 | $file['content'] 35 | ); 36 | 37 | $paste->addFile($file); 38 | } 39 | 40 | return $paste; 41 | } 42 | 43 | /** 44 | * Checks whether given data is valid. 45 | * 46 | * @param array $data 47 | * 48 | * @throws InvalidDataException when data is invalid. 49 | */ 50 | private function validateData(array $data): void 51 | { 52 | $pasteRequiredFields = ['id', 'title', 'created_at']; 53 | $fileRequiredFields = ['filename', 'content']; 54 | 55 | foreach ($pasteRequiredFields as $pasteRequiredField) { 56 | if (!array_key_exists($pasteRequiredField, $data[0])) { 57 | throw new InvalidDataException(sprintf( 58 | 'Given data has to have \'%s\' key.', 59 | $pasteRequiredField 60 | )); 61 | } 62 | } 63 | 64 | foreach ($data as $file) { 65 | foreach ($fileRequiredFields as $fileRequiredField) { 66 | if (!array_key_exists($fileRequiredField, $file)) { 67 | throw new InvalidDataException(sprintf( 68 | 'Given data\'s files have to have \'%s\' key.', 69 | $fileRequiredField 70 | )); 71 | } 72 | } 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /tests/Infrastructure/Dbal/DbalPasteMapperTest.php: -------------------------------------------------------------------------------- 1 | 438643, 17 | 'title' => null, 18 | 'created_at' => '2017-05-25 20:33:54', 19 | 'filename' => 'foo.php', 20 | 'content' => '', 21 | ], 22 | [ 23 | 'id' => 438643, 24 | 'title' => null, 25 | 'created_at' => '2017-05-25 20:33:54', 26 | 'filename' => '', 27 | 'content' => 'test', 28 | ], 29 | ]; 30 | 31 | $paste = $mapper->map($validData); 32 | 33 | $this->assertEquals(438643, $paste->getId()->getBase10Id()); 34 | $this->assertNull($paste->getTitle()); 35 | $this->assertEquals('2017-05-25 20:33:54', $paste->getCreatedAt()->format('Y-m-d H:i:s')); 36 | $this->assertNotEmpty($paste->getFiles()); 37 | $this->assertEquals('foo.php', $paste->getFiles()[0]->getFilename()); 38 | $this->assertEquals('', $paste->getFiles()[0]->getContent()); 39 | $this->assertNull($paste->getFiles()[1]->getFilename()); 40 | } 41 | 42 | /** 43 | * @dataProvider invalidDataProvider 44 | * @expectedException \Nastoletni\Code\Application\InvalidDataException 45 | */ 46 | public function testMappingThrowsExceptionWithInvalidData($data) 47 | { 48 | $mapper = new DbalPasteMapper(); 49 | 50 | $mapper->map($data); 51 | } 52 | 53 | public function invalidDataProvider() 54 | { 55 | return [ 56 | [[[ 57 | ]]], 58 | [[[ 59 | 'id' => 1, 60 | ]]], 61 | [[[ 62 | 'id' => 1, 63 | 'title' => 'test', 64 | ]]], 65 | [[[ 66 | 'id' => 1, 67 | 'title' => 'test', 68 | 'created_at' => '', 69 | ]]], 70 | [[[ 71 | 'id' => 1, 72 | 'title' => 'test', 73 | 'created_at' => '', 74 | 'filename' => 'test', 75 | ]]], 76 | ]; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /tests/Application/Form/CreatePasteFormValidatorTest.php: -------------------------------------------------------------------------------- 1 | assertInstanceOf(CreatePasteFormValidator::class, CreatePasteFormValidator::create()); 12 | } 13 | 14 | /** 15 | * @dataProvider validationCasesProvider 16 | */ 17 | public function testValidation($data, $errorsCount) 18 | { 19 | $validator = CreatePasteFormValidator::create(); 20 | 21 | $errors = $validator->validate($data); 22 | 23 | $this->assertCount($errorsCount, $errors); 24 | } 25 | 26 | public function validationCasesProvider() 27 | { 28 | return [ 29 | [ 30 | [ 31 | 'title' => 'Foobar', 32 | ], 33 | 2, 34 | ], 35 | [ 36 | [ 37 | 'title' => 'Foobar', 38 | 'content' => [ 39 | 'test', 40 | ], 41 | ], 42 | 1, 43 | ], 44 | [ 45 | [ 46 | 'title' => 'Foobar', 47 | 'name' => [ 48 | '', 49 | ], 50 | 'content' => [ 51 | 'Test content', 52 | ], 53 | ], 54 | 0, 55 | ], 56 | [ 57 | [ 58 | 'title' => 'Test', 59 | 'name' => [ 60 | '', 61 | '', 62 | ], 63 | 'content' => [ 64 | 'Test', 65 | 'Another test', 66 | ], 67 | ], 68 | 0, 69 | ], 70 | [ 71 | [ 72 | 'title' => '', 73 | 'name' => [ 74 | '', 75 | ], 76 | 'content' => [ 77 | 'Test', 78 | ], 79 | ], 80 | 0, 81 | ], 82 | [ 83 | [ 84 | 'title' => '', 85 | 'name' => [ 86 | '', 87 | ], 88 | 'content' => [ 89 | '', 90 | ], 91 | ], 92 | 1, 93 | ], 94 | [ 95 | [ 96 | 'title' => 'Title', 97 | 'name' => [ 98 | '', 99 | '', 100 | ], 101 | 'content' => [ 102 | 'Test', 103 | 'Foobar', 104 | ], 105 | ], 106 | 0, 107 | ], 108 | ]; 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/Infrastructure/Dbal/DbalPasteRepository.php: -------------------------------------------------------------------------------- 1 | dbal = $dbal; 35 | $this->mapper = $mapper; 36 | } 37 | 38 | /** 39 | * {@inheritdoc} 40 | */ 41 | public function getById(Paste\Id $id): Paste 42 | { 43 | $qb = $this->dbal->createQueryBuilder() 44 | ->select('p.id', 'p.title', 'p.created_at', 'f.filename', 'f.content') 45 | ->from('pastes', 'p') 46 | ->innerJoin('p', 'files', 'f', 'f.paste_id = p.id') 47 | ->where('p.id = :id') 48 | ->setParameter(':id', $id->getBase10Id()); 49 | 50 | $query = $this->dbal->executeQuery($qb->getSQL(), $qb->getParameters()); 51 | 52 | if ($query->rowCount() < 1) { 53 | throw new Paste\NotExistsException(sprintf('Paste with id %s does not exist.', $id->getBase62Id())); 54 | } 55 | 56 | $data = $query->fetchAll(); 57 | 58 | return $this->mapper->map($data); 59 | } 60 | 61 | /** 62 | * {@inheritdoc} 63 | * 64 | * @throws ConnectionException when there is something wrong with transaction. 65 | */ 66 | public function save(Paste $paste): void 67 | { 68 | $this->dbal->beginTransaction(); 69 | 70 | try { 71 | $this->dbal->insert('pastes', [ 72 | 'id' => $paste->getId()->getBase10Id(), 73 | 'title' => $paste->getTitle(), 74 | 'created_at' => $paste->getCreatedAt()->format('Y-m-d H:i:s'), 75 | ], [PDO::PARAM_INT, PDO::PARAM_STR, PDO::PARAM_STR]); 76 | } catch (DBALException $e) { 77 | /* @see https://dev.mysql.com/doc/refman/5.7/en/error-messages-server.html#error_er_dup_entry */ 78 | if (1062 == $e->getCode()) { 79 | throw new Paste\AlreadyExistsException(sprintf( 80 | 'Paste with id %s already exists.', 81 | $paste->getId()->getBase62Id() 82 | )); 83 | } 84 | 85 | throw $e; 86 | } 87 | 88 | // Insertion of files. 89 | foreach ($paste->getFiles() as $file) { 90 | $this->dbal->insert('files', [ 91 | 'paste_id' => $paste->getId()->getBase10Id(), 92 | 'filename' => $file->getFilename(), 93 | 'content' => $file->getContent(), 94 | ], [PDO::PARAM_INT, PDO::PARAM_STR, PDO::PARAM_STR]); 95 | } 96 | 97 | try { 98 | $this->dbal->commit(); 99 | } catch (ConnectionException $e) { 100 | $this->dbal->rollBack(); 101 | throw $e; 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /tests/Application/Service/CreatePasteServiceTest.php: -------------------------------------------------------------------------------- 1 | createMock(PasteRepository::class); 15 | $pasteRepositoryMock 16 | ->expects($this->once()) 17 | ->method('save'); 18 | 19 | $createPasteService = new CreatePasteService( 20 | $pasteRepositoryMock, 21 | $this->createMock(PasteCrypter::class) 22 | ); 23 | 24 | $createPasteService->handle($this->constructorParameters()); 25 | } 26 | 27 | public function testHandlingEncryptsPaste() 28 | { 29 | $pasteCrypterMock = $this->createMock(PasteCrypter::class); 30 | $pasteCrypterMock 31 | ->expects($this->once()) 32 | ->method('encrypt'); 33 | 34 | $createPasteService = new CreatePasteService( 35 | $this->createMock(PasteRepository::class), 36 | $pasteCrypterMock 37 | ); 38 | 39 | $createPasteService->handle($this->constructorParameters()); 40 | } 41 | 42 | public function testHandlingPassesOnAlreadyExistsException() 43 | { 44 | $pasteRepository = new class() implements PasteRepository { 45 | private $thrown = false; 46 | 47 | public function getById(Paste\Id $id): Paste 48 | { 49 | } 50 | 51 | public function save(Paste $paste): void 52 | { 53 | if (false === $this->thrown) { 54 | $this->thrown = true; 55 | 56 | throw new Paste\AlreadyExistsException(); 57 | } 58 | } 59 | }; 60 | 61 | $createPasteService = new CreatePasteService( 62 | $pasteRepository, 63 | $this->createMock(PasteCrypter::class) 64 | ); 65 | $createPasteService->handle($this->constructorParameters()); 66 | $createPasteService->handle($this->constructorParameters()); 67 | 68 | // Service did not throw any exception, it's ok then 69 | $this->assertTrue(true); 70 | } 71 | 72 | public function testResultMatchesInput() 73 | { 74 | $createPasteService = new CreatePasteService( 75 | $this->createMock(PasteRepository::class), 76 | $this->createMock(PasteCrypter::class) 77 | ); 78 | 79 | $params = $this->constructorParameters(); 80 | 81 | $payload = $createPasteService->handle($params); 82 | $paste = $payload->getPaste(); 83 | 84 | $this->assertEquals($params['title'], $paste->getTitle()); 85 | $this->assertEquals($params['name'][0], $paste->getFiles()[0]->getFilename()); 86 | $this->assertEquals($params['content'][0], $paste->getFiles()[0]->getContent()); 87 | 88 | $this->assertNotEmpty($payload->getEncryptionKey()); 89 | } 90 | 91 | private function constructorParameters() 92 | { 93 | return [ 94 | 'title' => 'Test', 95 | 'name' => [ 96 | 'test.txt', 97 | ], 98 | 'content' => [ 99 | 'Lorem ipsum dolor', 100 | ], 101 | ]; 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/UserInterface/Web/Controller/PasteController.php: -------------------------------------------------------------------------------- 1 | pasteRepository = $pasteRepository; 40 | $this->pasteCrypter = $pasteCrypter; 41 | } 42 | 43 | /** 44 | * home: GET /. 45 | * 46 | * @param Request $request 47 | * @param Response $response 48 | * 49 | * @return Response 50 | */ 51 | public function home(Request $request, Response $response): Response 52 | { 53 | return $this->twig->render($response, 'home.twig', [ 54 | 'errors' => $this->getFlash('errors'), 55 | 'old' => $this->getFlash('old'), 56 | ]); 57 | } 58 | 59 | /** 60 | * create: POST /. 61 | * 62 | * @param Request $request 63 | * @param Response $response 64 | * 65 | * @return Response 66 | */ 67 | public function create(Request $request, Response $response): Response 68 | { 69 | $data = $request->getParsedBody(); 70 | 71 | $validator = CreatePasteFormValidator::create(); 72 | 73 | $errors = $validator->validate($data); 74 | 75 | if (count($errors) > 0) { 76 | $this->flash('errors', $errors); 77 | $this->flash('old', $data); 78 | 79 | return $response 80 | ->withStatus(302) 81 | ->withHeader('Location', $this->router->relativePathFor('home')); 82 | } 83 | 84 | $createPasteService = new CreatePasteService($this->pasteRepository, $this->pasteCrypter); 85 | $payload = $createPasteService->handle($data); 86 | 87 | $paste = $payload->getPaste(); 88 | 89 | return $response 90 | ->withStatus(302) 91 | ->withHeader('Location', $this->router->relativePathFor('paste', [ 92 | 'id' => $paste->getId()->getBase62Id(), 93 | 'key' => $payload->getEncryptionKey(), 94 | ])); 95 | } 96 | 97 | /** 98 | * paste: GET /{id}/{key}. 99 | * 100 | * @param Request $request 101 | * @param Response $response 102 | * @param string $id 103 | * @param string $key 104 | * 105 | * @throws NotFoundException 106 | * 107 | * @return Response 108 | */ 109 | public function paste(Request $request, Response $response, string $id, string $key): Response 110 | { 111 | try { 112 | $paste = $this->pasteRepository->getById(Id::createFromBase62($id)); 113 | } catch (NotExistsException $e) { 114 | throw new NotFoundException($request, $response); 115 | } 116 | 117 | try { 118 | $this->pasteCrypter->decrypt($paste, $key); 119 | } catch (CrypterException $e) { 120 | // CrypterException is almost always when user has modified 121 | // encryption key in the URL and that is considered as not found. 122 | throw new NotFoundException($request, $response); 123 | } 124 | 125 | return $this->twig->render($response, 'paste.twig', [ 126 | 'paste' => $paste, 127 | ]); 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /resources/views/home.twig: -------------------------------------------------------------------------------- 1 | {% extends 'layout.twig' %} 2 | 3 | {% block head %} 4 | 5 | 6 | 7 | 8 | {{ parent() }} 9 | {% endblock %} 10 | 11 | {% block main %} 12 |
13 |
Code jest otwarto-źródłową wklejką z funkcjonalnościami takimi jak drag 'n drop i obsługa więcej niż jednego pliku. Zachowuje prywatność wklejek, każdy plik jest szyfrowany przy użyciu algorytmu AES-256-CBC i jedyną osobą, która zna klucz, jesteś ty i każdy, komu wyślesz linka.
14 |
15 |
16 |
17 |

Dodaj nowy

18 |
19 | 20 | 21 | {% for error in error('[title]', errors) %} 22 |

{{ error|trans }}

23 | {% endfor %} 24 | {% for i, input in old.content %} 25 |
26 | 27 |
28 | 29 | {% if i != 0 %} 30 | 31 | {% endif %} 32 |
33 | {% for error in error('[name][' ~ i ~ ']', errors) %} 34 |

{{ error|trans }}

35 | {% endfor %} 36 | 37 | 38 | {% for error in error('[content][' ~ i ~ ']', errors) %} 39 |

{{ error|trans }}

40 | {% endfor %} 41 |
42 | {% else %} 43 |
44 | 45 | 46 | 47 | 48 |
49 | {% endfor %} 50 |
51 | 52 | 53 |
54 |
55 |
56 |
57 | 58 | {% endblock %} 59 | 60 | {% block js %} 61 | 62 | 63 | {% endblock %} 64 | -------------------------------------------------------------------------------- /web/app.js: -------------------------------------------------------------------------------- 1 | const Fieldset = { 2 | SPECIFIED_FORM_QUERY: '.code-form--file[data-index="%d"]', 3 | LAST_FORM_QUERY: '.code-form--file:last-of-type', 4 | 5 | addNew() { 6 | const lastForm = document.querySelector(this.LAST_FORM_QUERY); 7 | const newForm = this.getElement(); 8 | const formsWrapper = lastForm.parentNode; 9 | 10 | newForm.querySelector('.code-form--button__delete').addEventListener('click', this.remove.bind(this)); 11 | formsWrapper.insertBefore(newForm, lastForm.nextSibling); 12 | }, 13 | 14 | remove(event) { 15 | const index = parseInt(event.target.parentNode.parentNode.getAttribute('data-index')); 16 | 17 | if (index === 0) { 18 | return; 19 | } 20 | 21 | // Add 3 because: +1 because [data-index] is 0-indexed and +2 because we've got two elements before. 22 | const form = document.querySelector(this.SPECIFIED_FORM_QUERY.replace(/%d/, index)); 23 | form.parentNode.removeChild(form); 24 | }, 25 | 26 | getElement() { 27 | const index = this.getNextIndex(); 28 | const fieldset = document.createElement('fieldset'); 29 | fieldset.setAttribute('class', 'code-form--file'); 30 | fieldset.setAttribute('data-index', index); 31 | 32 | fieldset.innerHTML = 33 | ` 34 |
35 | 36 | 37 |
38 | 39 | `; 40 | 41 | return fieldset; 42 | }, 43 | 44 | getNextIndex() { 45 | const currentIndex = parseInt(document.querySelector(this.LAST_FORM_QUERY).getAttribute('data-index'), 10); 46 | 47 | return currentIndex + 1; 48 | }, 49 | }; 50 | 51 | const NewFormButton = { 52 | BUTTON_QUERY: '#new-file', 53 | 54 | init() { 55 | const button = document.querySelector(this.BUTTON_QUERY); 56 | button.addEventListener('click', () => this.onClick()); 57 | }, 58 | 59 | onClick() { 60 | Fieldset.addNew(); 61 | }, 62 | }; 63 | 64 | const DragAndDrop = { 65 | FILE_FORM_QUERY: '.code-form--file', 66 | OVERLAY_WRAPPER_QUERY: 'body', 67 | 68 | init() { 69 | this.lastTarget = null; 70 | 71 | this.appendOverlay(); 72 | this.handleDragEnter(); 73 | this.handleDragLeave(); 74 | this.handleDragOver(); 75 | this.handleDrop(); 76 | }, 77 | 78 | handleDragEnter() { 79 | window.addEventListener('dragenter', (event) => { 80 | if (this.isFile(event)) { 81 | this.lastTarget = event.target; 82 | this.showOverlay(); 83 | } 84 | }); 85 | }, 86 | 87 | handleDragLeave() { 88 | window.addEventListener('dragleave', (event) => { 89 | event.preventDefault(); 90 | 91 | if (event.target === this.lastTarget) { 92 | this.hideOverlay(); 93 | } 94 | }); 95 | }, 96 | 97 | handleDragOver() { 98 | window.addEventListener('dragover', (event) => { 99 | event.preventDefault(); 100 | }); 101 | }, 102 | 103 | handleDrop() { 104 | window.addEventListener('drop', (event) => { 105 | event.preventDefault(); 106 | this.hideOverlay(); 107 | 108 | const files = [...event.dataTransfer.files] 109 | .filter(file => this.isReadable(file)); 110 | 111 | if (files.length === 0) { 112 | return; 113 | } 114 | 115 | const fieldsetsToCreateNumber = files.length - this.getEmptyFieldsets().length; 116 | this.createNewFieldsets(fieldsetsToCreateNumber); 117 | 118 | const emptyFieldsets = this.getEmptyFieldsets(); 119 | emptyFieldsets.forEach((fieldset, index) => this.fillFieldset(fieldset, files[index])) 120 | }); 121 | }, 122 | 123 | createNewFieldsets(number) { 124 | for(let i = 0; i < number; i++) { 125 | Fieldset.addNew(); 126 | } 127 | }, 128 | 129 | fillFieldset(fieldset, file) { 130 | const input = fieldset.querySelector('input'); 131 | const textarea = fieldset.querySelector('textarea'); 132 | 133 | input.value = this.getFileName(file); 134 | 135 | this.getFileContent(file).then(content => { 136 | textarea.value = content; 137 | }); 138 | }, 139 | 140 | getEmptyFieldsets() { 141 | const fieldsets = [...document.querySelectorAll(this.FILE_FORM_QUERY)]; 142 | 143 | return fieldsets.filter(fieldset => this.isFieldsetEmpty(fieldset)); 144 | }, 145 | 146 | isFieldsetEmpty(fieldset) { 147 | const inputValue = fieldset.querySelector('input').value; 148 | const textareaValue = fieldset.querySelector('textarea').value; 149 | 150 | return inputValue + textareaValue === ''; 151 | }, 152 | 153 | isFile(event) { 154 | return event.dataTransfer.types.some(type => type === 'Files'); 155 | }, 156 | 157 | isReadable(file) { 158 | // Is it empty string, application/* or text/*? 159 | return /(^$|application\/.+|text\/.+)/.exec(file.type); 160 | }, 161 | 162 | getFileName(file) { 163 | return file.name; 164 | }, 165 | 166 | getFileContent(file) { 167 | return new Promise(resolve => { 168 | const fileReader = new FileReader(); 169 | 170 | fileReader.readAsText(file); 171 | fileReader.onload = (event) => { 172 | resolve(event.target.result); 173 | }; 174 | }); 175 | }, 176 | 177 | getOverlayElement() { 178 | const overlay = document.createElement('div'); 179 | overlay.classList.add('drop-overlay'); 180 | overlay.innerHTML = `

Upuść pliki tutaj

`; 181 | 182 | return overlay; 183 | }, 184 | 185 | appendOverlay() { 186 | const overlayWrapper = document.querySelector(this.OVERLAY_WRAPPER_QUERY); 187 | this.overlay = this.getOverlayElement(); 188 | 189 | overlayWrapper.appendChild(this.overlay); 190 | }, 191 | 192 | showOverlay() { 193 | this.overlay.classList.add('drop-overlay__active'); 194 | }, 195 | 196 | hideOverlay() { 197 | this.overlay.classList.remove('drop-overlay__active'); 198 | }, 199 | }; 200 | 201 | DragAndDrop.init(); 202 | NewFormButton.init(); 203 | -------------------------------------------------------------------------------- /tests/Slim/DecoratingCallableResolverTest.php: -------------------------------------------------------------------------------- 1 | getContainerWithClassMethod(); 17 | /** @var AbstractController $controller */ 18 | $controller = $container->get('controller'); 19 | 20 | $controllerDecorator = $this->createMock(ControllerDecorator::class); 21 | $controllerDecorator 22 | ->expects($this->once()) 23 | ->method('decorate'); 24 | 25 | $decoratingCallableResolver = new DecoratingCallableResolver( 26 | $container, 27 | $controllerDecorator 28 | ); 29 | 30 | $resolved = $decoratingCallableResolver->resolve('controller:home'); 31 | $this->assertEquals($controller, $resolved[0]); 32 | $this->assertEquals('home', $resolved[1]); 33 | } 34 | 35 | public function testResolveWithInvokableChildOfAbstractControllerInContainer() 36 | { 37 | $container = $this->getContainerWithInvokableClass(); 38 | /** @var AbstractController $controller */ 39 | $controller = $container->get('controller'); 40 | 41 | $controllerDecorator = $this->createMock(ControllerDecorator::class); 42 | $controllerDecorator 43 | ->expects($this->once()) 44 | ->method('decorate'); 45 | 46 | $decoratingCallableResolver = new DecoratingCallableResolver( 47 | $container, 48 | $controllerDecorator 49 | ); 50 | 51 | $resolved = $decoratingCallableResolver->resolve('controller'); 52 | $this->assertEquals($controller, $resolved[0]); 53 | $this->assertEquals('__invoke', $resolved[1]); 54 | } 55 | 56 | public function testResolveWithChildOfAbstractControllerWithMethod() 57 | { 58 | $controller = new class() extends AbstractController { 59 | public function home( 60 | ServerRequestInterface $request, 61 | ResponseInterface $response 62 | ) { 63 | return $response; 64 | } 65 | }; 66 | 67 | $controllerDecorator = $this->createMock(ControllerDecorator::class); 68 | $controllerDecorator 69 | ->expects($this->once()) 70 | ->method('decorate'); 71 | 72 | $decoratingCallableResolver = new DecoratingCallableResolver( 73 | $this->createMock(ContainerInterface::class), 74 | $controllerDecorator 75 | ); 76 | 77 | $resolved = $decoratingCallableResolver->resolve([$controller, 'home']); 78 | $this->assertEquals($controller, $resolved[0]); 79 | $this->assertEquals('home', $resolved[1]); 80 | } 81 | 82 | public function testResolveWithInvokableChildOfAbstractController() 83 | { 84 | $controller = new class() extends AbstractController { 85 | public function __invoke( 86 | ServerRequestInterface $request, 87 | ResponseInterface $response 88 | ) { 89 | return $response; 90 | } 91 | }; 92 | 93 | $controllerDecorator = $this->createMock(ControllerDecorator::class); 94 | $controllerDecorator 95 | ->expects($this->once()) 96 | ->method('decorate'); 97 | 98 | $decoratingCallableResolver = new DecoratingCallableResolver( 99 | $this->createMock(ContainerInterface::class), 100 | $controllerDecorator 101 | ); 102 | 103 | $resolved = $decoratingCallableResolver->resolve($controller); 104 | $this->assertEquals($controller, $resolved); 105 | } 106 | 107 | public function testResolveWithNotChildOfAbstractController() 108 | { 109 | $controller = new class() { 110 | public function home( 111 | ServerRequestInterface $request, 112 | ResponseInterface $response 113 | ) { 114 | return $response; 115 | } 116 | }; 117 | 118 | $controllerDecorator = $this->createMock(ControllerDecorator::class); 119 | $controllerDecorator 120 | ->expects($this->never()) 121 | ->method('decorate'); 122 | 123 | $decoratingCallableResolver = new DecoratingCallableResolver( 124 | $this->createMock(ContainerInterface::class), 125 | $controllerDecorator 126 | ); 127 | 128 | $resolved = $decoratingCallableResolver->resolve([$controller, 'home']); 129 | $this->assertEquals($controller, $resolved[0]); 130 | $this->assertEquals('home', $resolved[1]); 131 | } 132 | 133 | private function getContainerWithClassMethod() 134 | { 135 | return new class() implements ContainerInterface { 136 | public function get($id) 137 | { 138 | return new class() extends AbstractController { 139 | public function home( 140 | ServerRequestInterface $request, 141 | ResponseInterface $response 142 | ) { 143 | return $response; 144 | } 145 | }; 146 | } 147 | 148 | public function has($id) 149 | { 150 | return 'controller' == $id; 151 | } 152 | }; 153 | } 154 | 155 | public function getContainerWithInvokableClass() 156 | { 157 | return new class() implements ContainerInterface { 158 | public function get($id) 159 | { 160 | return new class() extends AbstractController { 161 | public function __invoke( 162 | ServerRequestInterface $request, 163 | ResponseInterface $response 164 | ) { 165 | return $response; 166 | } 167 | }; 168 | } 169 | 170 | public function has($id) 171 | { 172 | return 'controller' == $id; 173 | } 174 | }; 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /src/AppKernel.php: -------------------------------------------------------------------------------- 1 | slim = new App(); 47 | 48 | $this->setupConfig(); 49 | $this->setupServices(); 50 | $this->setupRoutes(); 51 | 52 | // Middlewares 53 | $this->slim->add(new SymfonySessionMiddleware($this->slim->getContainer()['session'])); 54 | } 55 | 56 | /** 57 | * Sets up config in container. 58 | */ 59 | private function setupConfig(): void 60 | { 61 | $config = Yaml::parse(file_get_contents(__DIR__.'/config.yml')); 62 | 63 | $this->slim->getContainer()['config'] = $config; 64 | } 65 | 66 | /** 67 | * Sets up dependencies in container. 68 | */ 69 | private function setupServices(): void 70 | { 71 | $container = $this->slim->getContainer(); 72 | $container['settings']['displayErrorDetails'] = $container['config']['debug']; 73 | $container['logger'] = function () { 74 | return new Logger('application', [ 75 | new StreamHandler(__DIR__.'/../logs/logs.log'), 76 | ]); 77 | }; 78 | $container['foundHandler'] = function () { 79 | return new RequestResponseArgs(); 80 | }; 81 | $container['notFoundHandler'] = function (Container $container) { 82 | return [$container[ErrorController::class], 'notFound']; 83 | }; 84 | $container['errorHandler'] = function (Container $container) { 85 | // Show pretty error page on production and Slim debug info on development. 86 | $next = $container['config']['debug'] ? 87 | new Error($container['config']['debug']) : 88 | [$container[ErrorController::class], 'error']; 89 | 90 | return new Slim\Handler\LoggingErrorHandler( 91 | $container->get('logger'), 92 | $next 93 | ); 94 | }; 95 | $container['phpErrorHandler'] = function (Container $container) { 96 | // Show pretty error page on production and Slim debug info on development. 97 | $next = $container['config']['debug'] ? 98 | new PhpError($container['config']['debug']) : 99 | [$container[ErrorController::class], 'error']; 100 | 101 | return new Slim\Handler\LoggingErrorHandler( 102 | $container->get('logger'), 103 | $next 104 | ); 105 | }; 106 | $container['translator'] = function (Container $container) { 107 | $translator = new Translator($container['config']['locale']); 108 | $translator->setFallbackLocales(['en']); 109 | 110 | $translator->addLoader('php', new PhpFileLoader()); 111 | $translator->addResource('php', __DIR__.'/../resources/translations/messages.php', 'en'); 112 | $translator->addResource('php', __DIR__.'/../resources/translations/messages.pl.php', 'pl'); 113 | 114 | return $translator; 115 | }; 116 | $container['twig'] = function (Container $container) { 117 | $twig = new Twig(__DIR__.'/../resources/views/', [ 118 | 'debug' => $container['config']['debug'], 119 | ]); 120 | $twig->addExtension(new TwigExtension($container['router'], $container['config']['base_url'])); 121 | $twig->addExtension(new SymfonyValidatorExtension()); 122 | $twig->addExtension(new TransExtension($container['translator'])); 123 | 124 | return $twig; 125 | }; 126 | $container['session'] = function () { 127 | return new Session(); 128 | }; 129 | $container['controllerDecorator'] = function (Container $container) { 130 | return new ControllerDecorator( 131 | $container['twig'], 132 | $container['router'], 133 | $container['session'] 134 | ); 135 | }; 136 | $container['callableResolver'] = function (Container $container) { 137 | return new DecoratingCallableResolver( 138 | $container, 139 | $container['controllerDecorator'] 140 | ); 141 | }; 142 | $container['dbal'] = function (Container $container) { 143 | $config = new Configuration(); 144 | 145 | return DriverManager::getConnection([ 146 | 'driver' => 'pdo_mysql', 147 | 'host' => $container['config']['database']['host'], 148 | 'port' => $container['config']['database']['port'], 149 | 'dbname' => $container['config']['database']['name'], 150 | 'user' => $container['config']['database']['user'], 151 | 'password' => $container['config']['database']['password'], 152 | 'charset' => $container['config']['database']['charset'], 153 | ], $config); 154 | }; 155 | 156 | // Controllers 157 | $container[PasteController::class] = function (Container $container) { 158 | $pasteRepository = new DbalPasteRepository($container['dbal'], new DbalPasteMapper()); 159 | 160 | return new PasteController($pasteRepository, new AES256Crypter()); 161 | }; 162 | $container[ErrorController::class] = function (Container $container) { 163 | /** @var ControllerDecorator $controllerDecorator */ 164 | $controllerDecorator = $container['controllerDecorator']; 165 | 166 | $errorController = new ErrorController( 167 | new HttpsXkcdRepository() 168 | ); 169 | $controllerDecorator->decorate($errorController); 170 | 171 | return $errorController; 172 | }; 173 | } 174 | 175 | /** 176 | * Sets up routes. 177 | */ 178 | private function setupRoutes(): void 179 | { 180 | $routes = Yaml::parse(file_get_contents(__DIR__.'/routes.yml')); 181 | 182 | foreach ($routes as $routeName => $route) { 183 | $this->slim->map([$route['method']], $route['path'], $route['controller'])->setName($routeName); 184 | } 185 | } 186 | 187 | /** 188 | * Sends response to the client. 189 | */ 190 | public function handle(): void 191 | { 192 | $this->slim->run(); 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /tests/Infrastructure/AES256CrypterTest.php: -------------------------------------------------------------------------------- 1 | createMock(Paste\Id::class), 21 | 'title', 22 | new \DateTime() 23 | ); 24 | $paste->addFile(new File('', $content)); 25 | 26 | $encryptedPaste = clone $paste; 27 | 28 | $crypter->encrypt($encryptedPaste, $key); 29 | 30 | // FIXME: Why the fuck does this not pass 31 | // $this->assertNotEquals( 32 | // $paste->getFiles()[0]->getContent(), 33 | // $encryptedPaste->getFiles()[0]->getContent() 34 | // ); 35 | 36 | $crypter->decrypt($encryptedPaste, $key); 37 | 38 | $this->assertEquals( 39 | $paste->getFiles()[0]->getContent(), 40 | $encryptedPaste->getFiles()[0]->getContent() 41 | ); 42 | } 43 | 44 | public function crypterContentProvider() 45 | { 46 | return [ 47 | ['Foobar'], 48 | [<<<'CONTENT' 49 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Duis a facilisis est. Donec bibendum metus ac ex eleifend, eu maximus dolor imperdiet. Donec eget metus magna. Duis et metus erat. Aliquam posuere nisi a orci rutrum, sed molestie dui gravida. Aliquam erat volutpat. Nam a quam in nulla efficitur pulvinar. Phasellus molestie, mauris feugiat volutpat rutrum, erat odio viverra magna, vitae bibendum nisi lectus id sapien. 50 | 51 | In blandit urna nibh. Aenean dictum nulla non dapibus aliquam. In non pulvinar urna. Cras eget venenatis massa. Cras rhoncus nisi at ante egestas suscipit. Suspendisse potenti. Suspendisse potenti. Sed vel magna quam. 52 | 53 | Sed eu est gravida, suscipit ex a, euismod nibh. In nunc magna, consequat vitae orci et, rutrum malesuada turpis. Cras scelerisque at est ut congue. Cras quis aliquet dolor, et pellentesque nisi. Sed venenatis dapibus ornare. Cras non placerat velit, non sollicitudin justo. Aenean eleifend et dui quis efficitur. In hac habitasse platea dictumst. Integer pulvinar, sapien a facilisis laoreet, tellus ipsum gravida tortor, eu semper libero neque at tellus. Aliquam eleifend nibh vel erat lacinia placerat. Duis quis felis elit. Duis sollicitudin sagittis risus, et consequat enim dictum non. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur dui mauris, tincidunt non purus molestie, porta semper mi. Donec placerat tempus nibh, vehicula vulputate turpis commodo eget. 54 | 55 | Vivamus finibus gravida lorem, eu sagittis orci tempor in. Quisque vestibulum, arcu quis mollis consectetur, risus mi placerat dolor, at sagittis erat est sit amet lacus. Nulla facilisi. Morbi a ante purus. Suspendisse ultricies dignissim diam, vel egestas nibh eleifend vel. Nulla tempus commodo nunc, at euismod risus pulvinar eget. Cras faucibus nisl porta cursus condimentum. 56 | 57 | Nullam sed sodales lectus, vitae porta purus. Phasellus porttitor accumsan diam eget feugiat. Duis sit amet ultricies dui, ut tincidunt ante. Phasellus elit arcu, ullamcorper sed lorem eget, blandit tempus quam. Etiam aliquet mollis nisl, et fringilla magna auctor sit amet. Phasellus iaculis lectus vitae lectus volutpat interdum. Sed condimentum pulvinar tempus. Curabitur eu sagittis neque, ac pharetra neque. 58 | 59 | Proin sed accumsan velit, vel dignissim erat. Donec pretium tincidunt metus laoreet semper. Praesent quam dui, lacinia id posuere sed, dictum id arcu. Nunc odio ligula, lacinia a condimentum at, pretium et neque. Ut rhoncus, ante et tristique finibus, magna nunc ornare erat, id ultricies augue lacus eget massa. Morbi massa tortor, tincidunt sit amet erat quis, varius iaculis nisi. Proin lobortis odio eu nisl accumsan posuere. Nam enim enim, sollicitudin vitae odio non, bibendum eleifend erat. Phasellus ligula quam, luctus sagittis lacinia eget, mollis at enim. Nam pulvinar quam est, vitae bibendum odio iaculis pulvinar. Vivamus id justo nibh. Curabitur elementum justo ac venenatis auctor. Vivamus rutrum, nunc nec dapibus bibendum, orci nibh efficitur nulla, ac posuere leo elit quis risus. 60 | 61 | Nullam vulputate tortor nec erat condimentum volutpat. Nulla a congue erat, ut bibendum neque. Etiam sem est, porta vitae faucibus quis, tempor nec ligula. Phasellus semper a massa quis lacinia. In mattis ullamcorper lectus vel placerat. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Phasellus maximus porta aliquam. Aliquam aliquam molestie posuere. Maecenas eu dolor nec felis molestie pulvinar. 62 | 63 | Vivamus mi ex, finibus vel molestie eu, cursus at velit. Morbi consequat turpis lectus, tincidunt hendrerit odio viverra vitae. Donec metus ante, interdum a ullamcorper vitae, molestie sed urna. Proin et viverra neque, vitae faucibus urna. Cras accumsan ac massa eu fringilla. Donec efficitur rutrum diam a tempus. Phasellus at dictum tortor. Duis tempus orci lacus, vehicula finibus tortor cursus a. Nullam mollis tempus tincidunt. Integer fringilla sem in metus dictum sollicitudin. 64 | 65 | Nam consectetur euismod mi. Mauris mi leo, lacinia ac quam nec, pretium volutpat turpis. Cras suscipit ullamcorper tincidunt. Curabitur erat elit, tincidunt ut fermentum eget, facilisis ut lorem. Curabitur egestas commodo erat, eget volutpat arcu tincidunt accumsan. Sed lectus erat, finibus vitae vehicula a, posuere eu nisl. Morbi eget ornare massa. Duis vel egestas risus. Maecenas blandit augue quis libero semper cursus. In vitae diam maximus, faucibus risus id, dignissim nisl. Maecenas et erat vel enim efficitur fringilla ut eu augue. Nam viverra, tellus nec sollicitudin rhoncus, ex lacus bibendum justo, et tincidunt ex libero sed eros. Donec tortor diam, blandit sed lobortis ut, blandit sed sapien. 66 | 67 | Phasellus blandit cursus lobortis. Vivamus ac cursus lacus. Proin nec felis et lacus tincidunt commodo vel sit amet mauris. Morbi accumsan bibendum mauris. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Fusce odio lacus, pharetra quis augue a, rutrum mollis nisi. In id euismod metus. Morbi sit amet vestibulum lectus. Vivamus quis mi in elit aliquet convallis. 68 | 69 | Suspendisse eget volutpat felis. Praesent vel nisl vitae ipsum vulputate blandit. Vivamus ornare sit amet arcu in hendrerit. Nunc vulputate purus sit amet nisi imperdiet, non laoreet sem tempus. Aenean eu quam tristique, elementum orci a, placerat magna. Maecenas et dictum massa. Vestibulum viverra, purus pharetra dapibus euismod, libero tortor lobortis sem, non sagittis urna tortor ac nisi. In ligula nisl, faucibus ut nulla vitae, pulvinar ullamcorper mi. Ut vel risus diam. Nulla vel placerat elit. 70 | 71 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin auctor, velit eget varius finibus, ligula massa dignissim nisl, eu ornare nunc sem hendrerit arcu. Proin venenatis interdum neque, id fermentum justo aliquam at. Vivamus nec nisl viverra, tempor mi ut, rhoncus libero. Aliquam sed facilisis urna. Quisque vel odio id metus porta vulputate non in libero. Etiam non suscipit ligula. 72 | 73 | Suspendisse vehicula turpis semper risus dictum, eget tempus ligula fermentum. Aliquam erat volutpat. Suspendisse sed massa quis velit egestas pretium. Vestibulum rutrum, ligula at accumsan hendrerit, quam nunc fermentum magna, vitae vestibulum diam nulla non nulla. Suspendisse et dui velit. Fusce risus felis, mollis at nulla ac, consectetur venenatis diam. Sed lobortis lorem id vestibulum malesuada. 74 | 75 | Aliquam neque urna, interdum vitae tortor nec, dictum sodales risus. Cras risus magna, ullamcorper vitae libero non, pellentesque bibendum nisl. Vivamus aliquet nulla in augue consequat, at porta augue euismod. Curabitur finibus tincidunt bibendum. Suspendisse potenti. In hac habitasse platea dictumst. Suspendisse quam enim, dignissim eget dolor eu, consequat lobortis est. 76 | 77 | Etiam rutrum arcu ac dapibus egestas. Vestibulum a mi vel purus faucibus tempor. Maecenas varius elit vel elementum bibendum. Ut eget lectus eu neque dictum vehicula eget sit amet magna. Proin at erat tincidunt, lacinia velit a, auctor mi. Mauris a sem eu eros mattis suscipit porta nec odio. Nam at ligula pharetra, elementum magna vitae, fringilla sapien. Nunc consequat enim in massa consectetur efficitur. Phasellus scelerisque eros at dui suscipit, nec bibendum lectus congue. Fusce bibendum enim at diam posuere, quis laoreet dolor pulvinar. Quisque enim enim, placerat auctor dui non, scelerisque auctor turpis. Quisque iaculis felis vitae mauris dictum, eget lobortis nisl tincidunt. Nulla dapibus felis quis eros euismod porttitor. 78 | 79 | Donec gravida augue in ipsum convallis viverra. Nunc suscipit nunc et diam euismod aliquam viverra non lacus. Aliquam erat volutpat. Mauris semper justo a tincidunt dapibus. Nam eget eleifend nisl. Nulla feugiat, risus vel tempus eleifend, elit nisi convallis est, sit amet feugiat dui lectus sit amet dolor. Nunc eget eros erat. 80 | 81 | Suspendisse potenti. Duis consequat dolor facilisis arcu facilisis, sed maximus erat pulvinar. Nullam faucibus sem nec justo efficitur fringilla. Ut rhoncus vitae metus et blandit. Phasellus tempor mauris sit amet laoreet elementum. Vivamus eu arcu aliquam, aliquet dolor a, aliquam libero. Donec nec lorem velit. Nullam hendrerit turpis et mauris rhoncus hendrerit sit amet at dolor. Curabitur et urna imperdiet, laoreet libero at, tristique dolor. 82 | 83 | Quisque aliquam dignissim cursus. Sed eget tincidunt justo. Praesent pharetra facilisis sem in ullamcorper. Praesent mollis orci in lorem consequat convallis. Nulla ultrices sit amet lorem vitae fringilla. Phasellus dignissim auctor enim, vitae iaculis justo pretium non. Donec tempor diam commodo metus vulputate, ac volutpat purus efficitur. Etiam sit amet orci ut elit lobortis semper vel vel tortor. Phasellus molestie, nunc id euismod cursus, arcu turpis varius enim, eu tincidunt nisl massa et massa. Maecenas pulvinar odio dui, ut laoreet nisl porta non. 84 | 85 | Vivamus lobortis, odio vitae fringilla posuere, odio purus pharetra magna, ornare dapibus ipsum lectus sit amet ligula. Sed placerat urna non orci volutpat pellentesque. Aenean non tempus orci. Mauris sodales laoreet risus, vitae aliquet elit bibendum pellentesque. Sed ullamcorper laoreet venenatis. Fusce tortor dolor, egestas sit amet ullamcorper ut, malesuada in est. Proin lorem nibh, interdum et diam vitae, elementum feugiat lectus. Vivamus venenatis augue id risus mattis mollis. Mauris sit amet quam felis. Suspendisse potenti. Etiam euismod quam sit amet sapien ornare, a imperdiet massa eleifend. Nulla laoreet libero massa, eget vestibulum justo egestas eu. Nulla facilisi. Nam eu lorem vel orci maximus elementum sit amet auctor massa. Donec ut laoreet libero. Sed a velit ipsum. 86 | 87 | Nam urna mauris, elementum sit amet sapien eu, euismod pharetra dolor. Etiam sed sem sapien. Mauris eu scelerisque orci. Curabitur lobortis magna a ligula malesuada, eu aliquam justo euismod. Phasellus vel blandit est. Morbi pretium, nisi non condimentum gravida, orci elit mollis neque, in condimentum leo quam at nunc. Nulla aliquam pretium mauris eu condimentum. Nulla convallis sed dui sed facilisis. Morbi at metus viverra, consequat erat quis, euismod mi. Praesent in bibendum ante. In in purus tellus. Nunc est enim, volutpat sed mauris sit amet, cursus molestie urna. Donec dictum nisi id massa porttitor scelerisque sed. 88 | CONTENT 89 | ], 90 | ]; 91 | } 92 | } 93 | --------------------------------------------------------------------------------