diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..14846f83 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,10 @@ +# See https://php.watch/articles/composer-gitattributes +/.github export-ignore +/demo export-ignore +/tests export-ignore + +/.gitignore export-ignore +/.gitattributes export-ignore +/.travis.yml export-ignore +/phpstan.neon.dist export-ignore +/phpunit.xml.dist export-ignore \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..ae5cfc73 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,113 @@ +# This file was heavily based on the ci-file from SymfonyCasts/verify-email-bundle +# +# See https://github.com/SymfonyCasts/verify-email-bundle +# https://github.com/SymfonyCasts/verify-email-bundle/blob/main/.github/workflows/ci.yml + +name: CI +on: + push: + branches: ['main','master'] + pull_request: + +jobs: + static-analysis: + name: Static Analysis + runs-on: ubuntu-18.04 + + steps: + - name: "Checkout code" + uses: "actions/checkout@v2" + + - name: "Install PHP" + uses: "shivammathur/setup-php@v2" + with: + coverage: "none" + php-version: "8.0" + + - name: "Validate composer.json" + run: "composer validate --strict --no-check-lock" + + - name: "Validate php-files" + run: "php -l src && php -l tests" + + - name: "Composer install" + uses: "ramsey/composer-install@v2" + with: + composer-options: "--prefer-stable" + dependency-versions: 'highest' + + - name: "PHPStan" + run: "vendor/bin/phpstan analyze" + + - name: "PHPCompatibility" + run: "vendor/bin/phpcs src/ tests/ --standard=PHPCompatibility --runtime-set testVersion 7.2-8.1" + + tests: + name: "Tests ${{ matrix.php-version }} ${{ matrix.dependency-versions }}" + runs-on: ubuntu-18.04 + needs: static-analysis + + strategy: + fail-fast: false + matrix: + # normal, highest, non-dev installs + php-version: ['7.2', '7.3', '7.4', '8.0', '8.1'] + composer-options: ['--prefer-stable'] + dependency-versions: ['highest'] + include: + # testing lowest PHP version with lowest dependencies + - php-version: '7.2.5' + dependency-versions: 'lowest' + composer-options: '--prefer-lowest' + + steps: + - name: "Checkout code" + uses: "actions/checkout@v2" + + - name: "Install PHP" + uses: "shivammathur/setup-php@v2" + with: + coverage: "none" + php-version: "${{ matrix.php-version }}" + +# - name: Install Global Dependencies +# run: | +# composer global require --no-progress --no-scripts --no-plugins symfony/flex >=1.x + + - name: "Composer install" + uses: "ramsey/composer-install@v2" + with: + dependency-versions: "${{ matrix.dependency-versions }}" + composer-options: "--prefer-dist --no-progress" + + - name: Unit Tests + run: vendor/bin/phpunit + + - name: Install symlinks for demo's + uses: "ramsey/composer-install@v2" + with: + dependency-versions: "${{ matrix.dependency-versions }}" + composer-options: "--prefer-dist --no-progress" + working-directory: "demo" + + - name: Demo symfony4.4 - Install dependencies + uses: "ramsey/composer-install@v2" + with: + dependency-versions: "${{ matrix.dependency-versions }}" + composer-options: "--prefer-dist --no-progress" + working-directory: "demo/symfony4.4" + + - name: Demo symfony4.4 - Unit Tests + run: demo/symfony4.4/vendor/bin/simple-phpunit -c demo/symfony4.4/phpunit.xml.dist + + - name: Demo symfony6.x - Install dependencies + uses: "ramsey/composer-install@v2" + with: + dependency-versions: "${{ matrix.dependency-versions }}" + composer-options: "--prefer-dist --no-progress" + working-directory: "demo/symfony6.x" + if: ${{ startsWith(matrix.php-version , '8.') }} + + - name: Demo symfony6.x - Unit Tests + run: demo/symfony6.x/vendor/bin/simple-phpunit -c demo/symfony6.x/phpunit.xml.dist + if: ${{ startsWith(matrix.php-version , '8.') }} \ No newline at end of file diff --git a/.gitignore b/.gitignore index bd757d68..74552b2c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ .idea/ /composer.lock /vendor +.phpunit.result.cache +.phpstan-cache/ \ No newline at end of file diff --git a/.phpunit.result.cache b/.phpunit.result.cache deleted file mode 100644 index 2ea5639a..00000000 --- a/.phpunit.result.cache +++ /dev/null @@ -1 +0,0 @@ -{"version":1,"defects":{"Ambta\\DoctrineEncryptBundle\\Tests\\Functional\\BasicQueryTest\\BasicQueryDefuseTest::testPersistEntity":4,"Ambta\\DoctrineEncryptBundle\\Tests\\Functional\\BasicQueryTest\\BasicQueryDefuseTest::testNoUpdateOnReadEncrypted":4,"Ambta\\DoctrineEncryptBundle\\Tests\\Functional\\BasicQueryTest\\BasicQueryDefuseTest::testStoredDataIsEncrypted":4,"Ambta\\DoctrineEncryptBundle\\Tests\\Functional\\BasicQueryTest\\BasicQueryHaliteTest::testPersistEntity":1,"Ambta\\DoctrineEncryptBundle\\Tests\\Functional\\BasicQueryTest\\BasicQueryHaliteTest::testNoUpdateOnReadEncrypted":1,"Ambta\\DoctrineEncryptBundle\\Tests\\Functional\\BasicQueryTest\\BasicQueryHaliteTest::testStoredDataIsEncrypted":1,"Ambta\\DoctrineEncryptBundle\\Tests\\Unit\\DependencyInjection\\DoctrineEncryptExtensionTest::testConfigLoadCustom":1,"Ambta\\DoctrineEncryptBundle\\Tests\\Unit\\Encryptors\\HaliteEncryptorTest::testEncryptWithoutExtensionThrowsException":1,"Ambta\\DoctrineEncryptBundle\\Tests\\Unit\\Encryptors\\DefuseEncryptorTest::testEncrypt":4,"Ambta\\DoctrineEncryptBundle\\Tests\\Unit\\Encryptors\\DefuseEncryptorTest::testGenerateKey":4,"Ambta\\DoctrineEncryptBundle\\Tests\\Unit\\Encryptors\\HaliteEncryptorTest::testEncryptExtension":1,"Ambta\\DoctrineEncryptBundle\\Tests\\Unit\\Encryptors\\HaliteEncryptorTest::testGenerateKey":1,"Ambta\\DoctrineEncryptBundle\\Tests\\Unit\\Subscribers\\DoctrineEncryptSubscriberTest::testProcessFieldsNoEncryptor":4},"times":{"Ambta\\DoctrineEncryptBundle\\Tests\\Functional\\BasicQueryTest\\BasicQueryDefuseTest::testPersistEntity":0.456,"Ambta\\DoctrineEncryptBundle\\Tests\\Functional\\BasicQueryTest\\BasicQueryDefuseTest::testNoUpdateOnReadEncrypted":1.046,"Ambta\\DoctrineEncryptBundle\\Tests\\Functional\\BasicQueryTest\\BasicQueryDefuseTest::testStoredDataIsEncrypted":0.654,"Ambta\\DoctrineEncryptBundle\\Tests\\Functional\\BasicQueryTest\\BasicQueryHaliteTest::testPersistEntity":0.106,"Ambta\\DoctrineEncryptBundle\\Tests\\Functional\\BasicQueryTest\\BasicQueryHaliteTest::testNoUpdateOnReadEncrypted":0.142,"Ambta\\DoctrineEncryptBundle\\Tests\\Functional\\BasicQueryTest\\BasicQueryHaliteTest::testStoredDataIsEncrypted":0.126,"Ambta\\DoctrineEncryptBundle\\Tests\\Unit\\DependencyInjection\\DoctrineEncryptExtensionTest::testConfigLoadHalite":0.031,"Ambta\\DoctrineEncryptBundle\\Tests\\Unit\\DependencyInjection\\DoctrineEncryptExtensionTest::testConfigLoadDefuse":0.012,"Ambta\\DoctrineEncryptBundle\\Tests\\Unit\\DependencyInjection\\DoctrineEncryptExtensionTest::testConfigLoadCustom":0.013,"Ambta\\DoctrineEncryptBundle\\Tests\\Unit\\Encryptors\\DefuseEncryptorTest::testEncrypt":0.252,"Ambta\\DoctrineEncryptBundle\\Tests\\Unit\\Encryptors\\DefuseEncryptorTest::testGenerateKey":0.132,"Ambta\\DoctrineEncryptBundle\\Tests\\Unit\\Encryptors\\HaliteEncryptorTest::testEncryptExtension":0.005,"Ambta\\DoctrineEncryptBundle\\Tests\\Unit\\Encryptors\\HaliteEncryptorTest::testGenerateKey":0.008,"Ambta\\DoctrineEncryptBundle\\Tests\\Unit\\Encryptors\\HaliteEncryptorTest::testEncryptWithoutExtensionThrowsException":0.001,"Ambta\\DoctrineEncryptBundle\\Tests\\Unit\\Subscribers\\DoctrineEncryptSubscriberTest::testSetRestorEncryptor":0.01,"Ambta\\DoctrineEncryptBundle\\Tests\\Unit\\Subscribers\\DoctrineEncryptSubscriberTest::testProcessFieldsEncrypt":0.003,"Ambta\\DoctrineEncryptBundle\\Tests\\Unit\\Subscribers\\DoctrineEncryptSubscriberTest::testProcessFieldsEncryptExtend":0.003,"Ambta\\DoctrineEncryptBundle\\Tests\\Unit\\Subscribers\\DoctrineEncryptSubscriberTest::testProcessFieldsEncryptEmbedded":0.004,"Ambta\\DoctrineEncryptBundle\\Tests\\Unit\\Subscribers\\DoctrineEncryptSubscriberTest::testProcessFieldsEncryptNull":0.002,"Ambta\\DoctrineEncryptBundle\\Tests\\Unit\\Subscribers\\DoctrineEncryptSubscriberTest::testProcessFieldsNoEncryptor":0.001,"Ambta\\DoctrineEncryptBundle\\Tests\\Unit\\Subscribers\\DoctrineEncryptSubscriberTest::testProcessFieldsDecrypt":0.002,"Ambta\\DoctrineEncryptBundle\\Tests\\Unit\\Subscribers\\DoctrineEncryptSubscriberTest::testProcessFieldsDecryptExtended":0.003,"Ambta\\DoctrineEncryptBundle\\Tests\\Unit\\Subscribers\\DoctrineEncryptSubscriberTest::testProcessFieldsDecryptEmbedded":0.003,"Ambta\\DoctrineEncryptBundle\\Tests\\Unit\\Subscribers\\DoctrineEncryptSubscriberTest::testProcessFieldsDecryptNull":0.002,"Ambta\\DoctrineEncryptBundle\\Tests\\Unit\\Subscribers\\DoctrineEncryptSubscriberTest::testProcessFieldsDecryptNonEncrypted":0.002,"Ambta\\DoctrineEncryptBundle\\Tests\\Unit\\Subscribers\\DoctrineEncryptSubscriberTest::testOnFlush":0.052,"Ambta\\DoctrineEncryptBundle\\Tests\\Unit\\Subscribers\\DoctrineEncryptSubscriberTest::testPostFlush":0.006}} \ No newline at end of file diff --git a/README.md b/README.md index 5e7d22d0..c3ca35f8 100644 --- a/README.md +++ b/README.md @@ -62,3 +62,8 @@ Filename example: `.DefuseEncryptor.key` or `.HaliteEncryptor.key` * [Usage](src/Resources/doc/usage.md) * [Console commands](src/Resources/doc/commands.md) * [Custom encryption class](src/Resources/doc/custom_encryptor.md) + +### Demo + +Two demo-installations, one using symfony 4.4 and one using symfony 6.x, can be found in this repository in [`demo`](demo). This demonstrates how to use +the application using both annotations and, when using php > 8.0, attributes. diff --git a/composer.json b/composer.json index fc3a8a89..699dd29c 100644 --- a/composer.json +++ b/composer.json @@ -5,19 +5,25 @@ "license": "MIT", "description": "Encrypted symfony entity's by verified and standardized libraries", "require": { - "php": "^8.0", + "php": "^7.2|^8.0", "paragonie/halite": "^4.6", "paragonie/sodium_compat": "^1.5", "doctrine/orm": "^2.5", + "doctrine/doctrine-bundle": "^2.0", "symfony/property-access": "^4.1|^5.0|^6.0", "symfony/dependency-injection": "^4.1|^5.0|^6.0", "symfony/yaml": "^4.1|^5.0|^6.0", "symfony/http-kernel": "^4.1|^5.0|^6.0", - "symfony/config": "^4.1|^5.0|^6.0" + "symfony/config": "^4.1|^5.0|^6.0", + "doctrine/annotations": "^1.13" }, "require-dev": { "phpunit/phpunit": "^8.0|^9.0", - "defuse/php-encryption": "^2.1" + "defuse/php-encryption": "^2.1", + "doctrine/cache": "^1.11", + "phpstan/phpstan": "^1.4", + "jetbrains/phpstorm-attributes": "^1.0", + "phpcompatibility/php-compatibility": "^9.3" }, "suggest": { "defuse/php-encryption": "Alternative for halite for use with older php-versions", @@ -28,9 +34,14 @@ "Ambta\\DoctrineEncryptBundle\\": "src/" } }, + "scripts": { + "post-install-cmd": "\"vendor/bin/phpcs\" --config-set installed_paths vendor/phpcompatibility/php-compatibility", + "post-update-cmd" : "\"vendor/bin/phpcs\" --config-set installed_paths vendor/phpcompatibility/php-compatibility", + "phpcs-compatibility-test" : "vendor/bin/phpcs src/ tests/ --standard=PHPCompatibility --runtime-set testVersion 7.2-8.1" + }, "autoload-dev": { "psr-4": { - "Ambta\\DoctrineEncryptBundle\\Tests\\": "Tests/" + "Ambta\\DoctrineEncryptBundle\\Tests\\": "tests/" } }, "config": { diff --git a/demo/.gitignore b/demo/.gitignore new file mode 100644 index 00000000..74552b2c --- /dev/null +++ b/demo/.gitignore @@ -0,0 +1,5 @@ +.idea/ +/composer.lock +/vendor +.phpunit.result.cache +.phpstan-cache/ \ No newline at end of file diff --git a/demo/README.md b/demo/README.md new file mode 100644 index 00000000..6bb37caf --- /dev/null +++ b/demo/README.md @@ -0,0 +1,3 @@ +Run `composer.install` in this directory to create symlinks for the shared files to the demo-projects + +If you prefer a copy, use `composer install --no-dev`. diff --git a/demo/composer.json b/demo/composer.json new file mode 100644 index 00000000..a7844a22 --- /dev/null +++ b/demo/composer.json @@ -0,0 +1,26 @@ +{ + "name": "absolute-quantum/doctrine-encrypt-bundle-demo", + "description": "Symlinks shared directories", + "type": "metapackage", + "require": { + "kporras07/composer-symlinks": "^1.1" + }, + "scripts": { + "post-install-cmd": [ + "Kporras07\\ComposerSymlinks\\ScriptHandler::createSymlinks" + ], + "post-update-cmd": [ + "Kporras07\\ComposerSymlinks\\ScriptHandler::createSymlinks" + ] + }, + "extra": { + "symlinks": { + "shared/templates": "symfony4.4/templates", + "./shared/templates": "symfony6.x/templates", + "shared/var/data.db": "symfony4.4/var/data.db", + "./shared/var/data.db": "symfony6.x/var/data.db", + "shared/.Halite.key": "symfony4.4/.Halite.key", + "./shared/.Halite.key": "symfony6.x/.Halite.key" + } + } +} diff --git a/demo/shared/.Halite.key b/demo/shared/.Halite.key new file mode 100644 index 00000000..352aa20d --- /dev/null +++ b/demo/shared/.Halite.key @@ -0,0 +1 @@ +31400400ce550006e6a149298ed2a9d78d94d9a5aaac68e4b2d86ee0174c4e25ad03983e03d6d958d8f51636cbe221dd21ae6c186058af6b998001a7a04954a14ffc675595ccbdb04602b76dabe84b33d023974e8b8e689a1d03a9affe5abadad18a8861 \ No newline at end of file diff --git a/demo/shared/templates/index.html.twig b/demo/shared/templates/index.html.twig new file mode 100644 index 00000000..65279859 --- /dev/null +++ b/demo/shared/templates/index.html.twig @@ -0,0 +1,51 @@ + + + + + {% block title %}Welcome!{% endblock %} + + + +

Hello World!

+

Symfony {{ appVersion }} using php{{ constant('PHP_MAJOR_VERSION')~'.'~constant('PHP_MINOR_VERSION') }}

+ + + + + + + + {% for secret in secrets %} + + + + + + + {% endfor %} +
TypeNameSecretRaw Secret (as stored in DB)
{{ secret.type }}{{ secret.name }}{{ secret.secret }}{{ secret.rawSecret }}
+ + diff --git a/demo/shared/var/data.db b/demo/shared/var/data.db new file mode 100644 index 00000000..760eeee8 Binary files /dev/null and b/demo/shared/var/data.db differ diff --git a/demo/symfony4.4/.env b/demo/symfony4.4/.env new file mode 100644 index 00000000..4f0cd85f --- /dev/null +++ b/demo/symfony4.4/.env @@ -0,0 +1,31 @@ +# In all environments, the following files are loaded if they exist, +# the latter taking precedence over the former: +# +# * .env contains default values for the environment variables needed by the app +# * .env.local uncommitted file with local overrides +# * .env.$APP_ENV committed environment-specific defaults +# * .env.$APP_ENV.local uncommitted environment-specific overrides +# +# Real environment variables win over .env files. +# +# DO NOT DEFINE PRODUCTION SECRETS IN THIS FILE NOR IN ANY OTHER COMMITTED FILES. +# +# Run "composer dump-env prod" to compile .env files for production use (requires symfony/flex >=1.2). +# https://symfony.com/doc/current/best_practices.html#use-environment-variables-for-infrastructure-configuration + +###> symfony/framework-bundle ### +APP_ENV=dev +APP_SECRET=50710977b1177353fb0c574b9ce67051 +#TRUSTED_PROXIES=127.0.0.0/8,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16 +#TRUSTED_HOSTS='^(localhost|example\.com)$' +###< symfony/framework-bundle ### + +###> doctrine/doctrine-bundle ### +# Format described at https://www.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/configuration.html#connecting-using-a-url +# IMPORTANT: You MUST configure your server version, either here or in config/packages/doctrine.yaml +# +# DATABASE_URL="sqlite:///%kernel.project_dir%/var/data.db" +# DATABASE_URL="mysql://db_user:db_password@127.0.0.1:3306/db_name?serverVersion=5.7" +DATABASE_PATH="var/data.db" +DATABASE_URL="sqlite:///%kernel.project_dir%/var/data.db" +###< doctrine/doctrine-bundle ### diff --git a/demo/symfony4.4/.gitignore b/demo/symfony4.4/.gitignore new file mode 100644 index 00000000..36eed7b0 --- /dev/null +++ b/demo/symfony4.4/.gitignore @@ -0,0 +1,16 @@ +# Ignore symlinks +/templates +/var/data.db +/.Halite.key + +# Ignore locked versions of composer +composer.lock +###> symfony/framework-bundle ### +/.env.local +/.env.local.php +/.env.*.local +/config/secrets/prod/prod.decrypt.private.php +/public/bundles/ +/var/ +/vendor/ +###< symfony/framework-bundle ### diff --git a/demo/symfony4.4/.phpversion b/demo/symfony4.4/.phpversion new file mode 100644 index 00000000..ea70ce01 --- /dev/null +++ b/demo/symfony4.4/.phpversion @@ -0,0 +1 @@ +72 diff --git a/demo/symfony4.4/README.md b/demo/symfony4.4/README.md new file mode 100644 index 00000000..ba4ace44 --- /dev/null +++ b/demo/symfony4.4/README.md @@ -0,0 +1,26 @@ +This demo-installation demonstrates a simple symfony 4.4-application using both annotations and, when using php > 8.0, attributes. + +# How to use +```shell +# run `composer install` in parent-directory to create symlinks for shared files +cd ../ +composer install +cd - + +# Install packages +composer install + +# Serve the application +## Using symfony-cli +symfony serve +## Using php-builtin-server () +php -S localhost:8000 -t public +## Other possibility +### Use a webserver like apache or nginx + + +# Run unittests +vendor/bin/simple-phpunit +``` + + diff --git a/demo/symfony4.4/bin/console b/demo/symfony4.4/bin/console new file mode 100755 index 00000000..5de0e1c5 --- /dev/null +++ b/demo/symfony4.4/bin/console @@ -0,0 +1,42 @@ +#!/usr/bin/env php +getParameterOption(['--env', '-e'], null, true)) { + putenv('APP_ENV='.$_SERVER['APP_ENV'] = $_ENV['APP_ENV'] = $env); +} + +if ($input->hasParameterOption('--no-debug', true)) { + putenv('APP_DEBUG='.$_SERVER['APP_DEBUG'] = $_ENV['APP_DEBUG'] = '0'); +} + +require dirname(__DIR__).'/config/bootstrap.php'; + +if ($_SERVER['APP_DEBUG']) { + umask(0000); + + if (class_exists(Debug::class)) { + Debug::enable(); + } +} + +$kernel = new Kernel($_SERVER['APP_ENV'], (bool) $_SERVER['APP_DEBUG']); +$application = new Application($kernel); +$application->run($input); diff --git a/demo/symfony4.4/composer.json b/demo/symfony4.4/composer.json new file mode 100644 index 00000000..88824244 --- /dev/null +++ b/demo/symfony4.4/composer.json @@ -0,0 +1,72 @@ +{ + "type": "project", + "license": "proprietary", + "require": { + "php": ">=7.1.3", + "ext-ctype": "*", + "ext-iconv": "*", + "absolute-quantum/doctrine-encrypt-bundle": "@dev", + "doctrine/persistence": "^1.3|^2.0", + "symfony/console": "4.4.*", + "symfony/dotenv": "4.4.*", + "symfony/flex": "^1.3.1", + "symfony/framework-bundle": "4.4.*", + "symfony/twig-bundle": "4.4.*", + "symfony/yaml": "4.4.*" + }, + "require-dev": { + "symfony/phpunit-bridge": "^6.0" + }, + "config": { + "allow-plugins": { + "composer/package-versions-deprecated": true, + "symfony/flex": true, + "ajgl/composer-symlinker": true + }, + "preferred-install": { + "*": "dist" + }, + "sort-packages": true + }, + "autoload": { + "psr-4": { + "App\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "App\\Tests\\": "tests/" + } + }, + "replace": { + "paragonie/random_compat": "2.*", + "symfony/polyfill-ctype": "*", + "symfony/polyfill-iconv": "*", + "symfony/polyfill-php71": "*", + "symfony/polyfill-php70": "*", + "symfony/polyfill-php56": "*" + }, + "scripts": { + "post-install-cmd": [ + "bin/console cache:clear" + ], + "post-update-cmd": [ + "bin/console cache:clear" + ] + }, + "conflict": { + "symfony/symfony": "*" + }, + "extra": { + "symfony": { + "allow-contrib": false, + "require": "4.4.*" + } + }, + "repositories": [ + { + "type": "path", + "url": "../../" + } + ] +} diff --git a/demo/symfony4.4/config/bootstrap.php b/demo/symfony4.4/config/bootstrap.php new file mode 100644 index 00000000..55560fb8 --- /dev/null +++ b/demo/symfony4.4/config/bootstrap.php @@ -0,0 +1,23 @@ +=1.2) +if (is_array($env = @include dirname(__DIR__).'/.env.local.php') && (!isset($env['APP_ENV']) || ($_SERVER['APP_ENV'] ?? $_ENV['APP_ENV'] ?? $env['APP_ENV']) === $env['APP_ENV'])) { + (new Dotenv(false))->populate($env); +} else { + // load all the .env files + (new Dotenv(false))->loadEnv(dirname(__DIR__).'/.env'); +} + +$_SERVER += $_ENV; +$_SERVER['APP_ENV'] = $_ENV['APP_ENV'] = ($_SERVER['APP_ENV'] ?? $_ENV['APP_ENV'] ?? null) ?: 'dev'; +$_SERVER['APP_DEBUG'] = $_SERVER['APP_DEBUG'] ?? $_ENV['APP_DEBUG'] ?? 'prod' !== $_SERVER['APP_ENV']; +$_SERVER['APP_DEBUG'] = $_ENV['APP_DEBUG'] = (int) $_SERVER['APP_DEBUG'] || filter_var($_SERVER['APP_DEBUG'], FILTER_VALIDATE_BOOLEAN) ? '1' : '0'; diff --git a/demo/symfony4.4/config/bundles.php b/demo/symfony4.4/config/bundles.php new file mode 100644 index 00000000..05746c3a --- /dev/null +++ b/demo/symfony4.4/config/bundles.php @@ -0,0 +1,8 @@ + ['all' => true], + Ambta\DoctrineEncryptBundle\AmbtaDoctrineEncryptBundle::class => ['all' => true], + Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true], + Symfony\Bundle\TwigBundle\TwigBundle::class => ['all' => true], +]; diff --git a/demo/symfony4.4/config/packages/cache.yaml b/demo/symfony4.4/config/packages/cache.yaml new file mode 100644 index 00000000..6899b720 --- /dev/null +++ b/demo/symfony4.4/config/packages/cache.yaml @@ -0,0 +1,19 @@ +framework: + cache: + # Unique name of your app: used to compute stable namespaces for cache keys. + #prefix_seed: your_vendor_name/app_name + + # The "app" cache stores to the filesystem by default. + # The data in this cache should persist between deploys. + # Other options include: + + # Redis + #app: cache.adapter.redis + #default_redis_provider: redis://localhost + + # APCu (not recommended with heavy random-write workloads as memory fragmentation can cause perf issues) + #app: cache.adapter.apcu + + # Namespaced pools use the above "app" backend by default + #pools: + #my.dedicated.cache: null diff --git a/demo/symfony4.4/config/packages/doctrine.yaml b/demo/symfony4.4/config/packages/doctrine.yaml new file mode 100644 index 00000000..3ddaa9f6 --- /dev/null +++ b/demo/symfony4.4/config/packages/doctrine.yaml @@ -0,0 +1,18 @@ +doctrine: + dbal: + url: '%env(resolve:DATABASE_URL)%' + + # IMPORTANT: You MUST configure your server version, + # either here or in the DATABASE_URL env var (see .env file) + #server_version: '13' + orm: + auto_generate_proxy_classes: true + naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware + auto_mapping: true + mappings: + Annotation: + type: 'annotation' + is_bundle: false + dir: '%kernel.project_dir%/src/Entity/Annotation' + prefix: 'App\Entity\Annotation' + alias: App diff --git a/demo/symfony4.4/config/packages/framework.yaml b/demo/symfony4.4/config/packages/framework.yaml new file mode 100644 index 00000000..cad7f780 --- /dev/null +++ b/demo/symfony4.4/config/packages/framework.yaml @@ -0,0 +1,17 @@ +# see https://symfony.com/doc/current/reference/configuration/framework.html +framework: + secret: '%env(APP_SECRET)%' + #csrf_protection: true + #http_method_override: 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 + + #esi: true + #fragments: true + php_errors: + log: true diff --git a/demo/symfony4.4/config/packages/php8/doctrine.yaml b/demo/symfony4.4/config/packages/php8/doctrine.yaml new file mode 100644 index 00000000..371d4934 --- /dev/null +++ b/demo/symfony4.4/config/packages/php8/doctrine.yaml @@ -0,0 +1,15 @@ +doctrine: + orm: + mappings: + Annotation: + type: 'annotation' + is_bundle: false + dir: '%kernel.project_dir%/src/Entity/Annotation' + prefix: 'App\Entity\Annotation' + alias: App + Attributes: + type: 'attribute' + is_bundle: false + dir: '%kernel.project_dir%/src/Entity/Attribute' + prefix: 'App\Entity\Attribute' + alias: App diff --git a/demo/symfony4.4/config/packages/prod/doctrine.yaml b/demo/symfony4.4/config/packages/prod/doctrine.yaml new file mode 100644 index 00000000..17299e28 --- /dev/null +++ b/demo/symfony4.4/config/packages/prod/doctrine.yaml @@ -0,0 +1,17 @@ +doctrine: + orm: + auto_generate_proxy_classes: false + query_cache_driver: + type: pool + pool: doctrine.system_cache_pool + result_cache_driver: + type: pool + pool: doctrine.result_cache_pool + +framework: + cache: + pools: + doctrine.result_cache_pool: + adapter: cache.app + doctrine.system_cache_pool: + adapter: cache.system diff --git a/demo/symfony4.4/config/packages/prod/routing.yaml b/demo/symfony4.4/config/packages/prod/routing.yaml new file mode 100644 index 00000000..b3e6a0af --- /dev/null +++ b/demo/symfony4.4/config/packages/prod/routing.yaml @@ -0,0 +1,3 @@ +framework: + router: + strict_requirements: null diff --git a/demo/symfony4.4/config/packages/routing.yaml b/demo/symfony4.4/config/packages/routing.yaml new file mode 100644 index 00000000..7e977620 --- /dev/null +++ b/demo/symfony4.4/config/packages/routing.yaml @@ -0,0 +1,3 @@ +framework: + router: + utf8: true diff --git a/demo/symfony4.4/config/packages/test/doctrine.yaml b/demo/symfony4.4/config/packages/test/doctrine.yaml new file mode 100644 index 00000000..e00e38f5 --- /dev/null +++ b/demo/symfony4.4/config/packages/test/doctrine.yaml @@ -0,0 +1,4 @@ +doctrine: + dbal: + # "TEST_TOKEN" is typically set by ParaTest + #dbname_suffix: '_test%env(default::TEST_TOKEN)%' diff --git a/demo/symfony4.4/config/packages/test/framework.yaml b/demo/symfony4.4/config/packages/test/framework.yaml new file mode 100644 index 00000000..d051c840 --- /dev/null +++ b/demo/symfony4.4/config/packages/test/framework.yaml @@ -0,0 +1,4 @@ +framework: + test: true + session: + storage_id: session.storage.mock_file diff --git a/demo/symfony4.4/config/packages/test/twig.yaml b/demo/symfony4.4/config/packages/test/twig.yaml new file mode 100644 index 00000000..8c6e0b40 --- /dev/null +++ b/demo/symfony4.4/config/packages/test/twig.yaml @@ -0,0 +1,2 @@ +twig: + strict_variables: true diff --git a/demo/symfony4.4/config/packages/twig.yaml b/demo/symfony4.4/config/packages/twig.yaml new file mode 100644 index 00000000..773160cf --- /dev/null +++ b/demo/symfony4.4/config/packages/twig.yaml @@ -0,0 +1,7 @@ +twig: + default_path: '%kernel.project_dir%/templates' + debug: '%kernel.debug%' + strict_variables: '%kernel.debug%' + exception_controller: null + globals: + appVersion: "4.4" diff --git a/demo/symfony4.4/config/preload.php b/demo/symfony4.4/config/preload.php new file mode 100644 index 00000000..064bdcd6 --- /dev/null +++ b/demo/symfony4.4/config/preload.php @@ -0,0 +1,9 @@ + doctrine/doctrine-bundle ### + database: + ports: + - "5432" +###< doctrine/doctrine-bundle ### diff --git a/demo/symfony4.4/docker-compose.yml b/demo/symfony4.4/docker-compose.yml new file mode 100644 index 00000000..a80a8cf9 --- /dev/null +++ b/demo/symfony4.4/docker-compose.yml @@ -0,0 +1,21 @@ +version: '3' + +services: +###> doctrine/doctrine-bundle ### + database: + image: postgres:${POSTGRES_VERSION:-13}-alpine + environment: + POSTGRES_DB: ${POSTGRES_DB:-app} + # You should definitely change the password in production + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-ChangeMe} + POSTGRES_USER: ${POSTGRES_USER:-symfony} + volumes: + - db-data:/var/lib/postgresql/data:rw + # You may use a bind-mounted host directory instead, so that it is harder to accidentally remove the volume and lose all your data! + # - ./docker/db/data:/var/lib/postgresql/data:rw +###< doctrine/doctrine-bundle ### + +volumes: +###> doctrine/doctrine-bundle ### + db-data: +###< doctrine/doctrine-bundle ### diff --git a/demo/symfony4.4/phpunit.xml.dist b/demo/symfony4.4/phpunit.xml.dist new file mode 100644 index 00000000..ddc7f24a --- /dev/null +++ b/demo/symfony4.4/phpunit.xml.dist @@ -0,0 +1,29 @@ + + + + + tests + + + + + + src + + + + + + + + + diff --git a/demo/symfony4.4/public/index.php b/demo/symfony4.4/public/index.php new file mode 100644 index 00000000..d0b6e020 --- /dev/null +++ b/demo/symfony4.4/public/index.php @@ -0,0 +1,27 @@ +handle($request); +$response->send(); +$kernel->terminate($request, $response); diff --git a/demo/symfony4.4/src/Controller/.gitignore b/demo/symfony4.4/src/Controller/.gitignore new file mode 100644 index 00000000..e69de29b diff --git a/demo/symfony4.4/src/Controller/DefaultController.php b/demo/symfony4.4/src/Controller/DefaultController.php new file mode 100644 index 00000000..0d4b0c5d --- /dev/null +++ b/demo/symfony4.4/src/Controller/DefaultController.php @@ -0,0 +1,55 @@ +findAll(), + $secretUsingAttributesRepository->findAll() + ); + + return $this->render('index.html.twig',['secrets' => $secrets]); + } + + /** + * @Route(name="create", path="/create") + */ + public function create(Request $request, EntityManagerInterface $em): Response + { + if (!$request->query->has('name') || !$request->query->has('secret') || !$request->query->has('type')) { + return new Response('Please specify name, secret and type in url-query'); + } + + $type = $request->query->get('type'); + if ($type === 'annotation') { + $secret = new \App\Entity\Annotation\Secret(); + } elseif($type === 'attribute') { + $secret = new \App\Entity\Attribute\Secret(); + } else { + return new Response('Type is only allowed to be "annotation" or "attribute"'); + } + + $secret + ->setName($request->query->getAlnum('name')) + ->setSecret($request->query->getAlnum('secret')); + + $em->persist($secret); + $em->flush(); + + return new Response(sprintf('OK - secret %s stored',$secret->getName())); + } +} \ No newline at end of file diff --git a/demo/symfony4.4/src/Entity/.gitignore b/demo/symfony4.4/src/Entity/.gitignore new file mode 100644 index 00000000..e69de29b diff --git a/demo/symfony4.4/src/Entity/Annotation/Secret.php b/demo/symfony4.4/src/Entity/Annotation/Secret.php new file mode 100644 index 00000000..49dce58a --- /dev/null +++ b/demo/symfony4.4/src/Entity/Annotation/Secret.php @@ -0,0 +1,98 @@ +name; + } + + /** + * @param string $name + * + * @return $this + */ + public function setName($name): self + { + $this->name = $name; + + return $this; + } + + /** + * @return string + */ + public function getSecret() + { + return $this->secret; + } + + /** + * @param string $secret + * + * @return $this + */ + public function setSecret($secret): self + { + $this->secret = $secret; + + return $this; + } + + /** + * @return string + */ + public function getRawSecret() + { + return $this->rawSecret; + } + + /** + * @param mixed $rawSecret + * + * @return $this + */ + public function setRawSecret($rawSecret) + { + $this->rawSecret = $rawSecret; + + return $this; + } +} \ No newline at end of file diff --git a/demo/symfony4.4/src/Entity/Attribute/Secret.php b/demo/symfony4.4/src/Entity/Attribute/Secret.php new file mode 100644 index 00000000..c76a80db --- /dev/null +++ b/demo/symfony4.4/src/Entity/Attribute/Secret.php @@ -0,0 +1,94 @@ +name; + } + + /** + * @param string $name + * + * @return $this + */ + public function setName($name): self + { + $this->name = $name; + + return $this; + } + + /** + * @return string + */ + public function getSecret() + { + return $this->secret; + } + + /** + * @param string $secret + * + * @return $this + */ + public function setSecret($secret): self + { + $this->secret = $secret; + + return $this; + } + + /** + * @return string + */ + public function getRawSecret() + { + return $this->rawSecret; + } + + /** + * @param mixed $rawSecret + * + * @return $this + */ + public function setRawSecret($rawSecret) + { + $this->rawSecret = $rawSecret; + + return $this; + } +} \ No newline at end of file diff --git a/demo/symfony4.4/src/Entity/SecretInterface.php b/demo/symfony4.4/src/Entity/SecretInterface.php new file mode 100644 index 00000000..cd234732 --- /dev/null +++ b/demo/symfony4.4/src/Entity/SecretInterface.php @@ -0,0 +1,11 @@ +getProjectDir().'/config/bundles.php'; + foreach ($contents as $class => $envs) { + if ($envs[$this->environment] ?? $envs['all'] ?? false) { + yield new $class(); + } + } + } + + public function getProjectDir(): string + { + return \dirname(__DIR__); + } + + protected function configureContainer(ContainerBuilder $container, LoaderInterface $loader): void + { + $container->addResource(new FileResource($this->getProjectDir().'/config/bundles.php')); + $container->setParameter('container.dumper.inline_class_loader', \PHP_VERSION_ID < 70400 || $this->debug); + $container->setParameter('container.dumper.inline_factories', true); + $confDir = $this->getProjectDir().'/config'; + + $loader->load($confDir.'/{packages}/*'.self::CONFIG_EXTS, 'glob'); + $loader->load($confDir.'/{packages}/'.$this->environment.'/*'.self::CONFIG_EXTS, 'glob'); + $loader->load($confDir.'/{services}'.self::CONFIG_EXTS, 'glob'); + $loader->load($confDir.'/{services}_'.$this->environment.self::CONFIG_EXTS, 'glob'); + + if (PHP_VERSION_ID >= 80000) { + $loader->load($confDir.'/{packages}/php8/*'.self::CONFIG_EXTS, 'glob'); + } + } + + protected function configureRoutes(RouteCollectionBuilder $routes): void + { + $confDir = $this->getProjectDir().'/config'; + + $routes->import($confDir.'/{routes}/'.$this->environment.'/*'.self::CONFIG_EXTS, '/', 'glob'); + $routes->import($confDir.'/{routes}/*'.self::CONFIG_EXTS, '/', 'glob'); + $routes->import($confDir.'/{routes}'.self::CONFIG_EXTS, '/', 'glob'); + } +} diff --git a/demo/symfony4.4/src/Repository/.gitignore b/demo/symfony4.4/src/Repository/.gitignore new file mode 100644 index 00000000..e69de29b diff --git a/demo/symfony4.4/src/Repository/AbstractSecretRepository.php b/demo/symfony4.4/src/Repository/AbstractSecretRepository.php new file mode 100644 index 00000000..13510ec2 --- /dev/null +++ b/demo/symfony4.4/src/Repository/AbstractSecretRepository.php @@ -0,0 +1,26 @@ +createQueryBuilder('s'); + $qb->select('s') + ->addSelect('(s.secret) as rawSecret') + ->orderBy('s.name','ASC'); + $rawResult = $qb->getQuery()->getResult(); + + $result = []; + foreach ($rawResult as $row) { + $secret = $row[0]; + $secret->setRawSecret($row['rawSecret']); + $result[] = $secret; + } + + return $result; + } +} \ No newline at end of file diff --git a/demo/symfony4.4/src/Repository/Annotation/SecretRepository.php b/demo/symfony4.4/src/Repository/Annotation/SecretRepository.php new file mode 100644 index 00000000..fc1427a5 --- /dev/null +++ b/demo/symfony4.4/src/Repository/Annotation/SecretRepository.php @@ -0,0 +1,27 @@ += 80000) { + /** + * @method Secret|null find($id, $lockMode = null, $lockVersion = null) + * @method Secret|null findOneBy(array $criteria, array $orderBy = null) + * @method Secret[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) + */ + class SecretRepository extends AbstractSecretRepository + { + public function __construct(\Doctrine\Common\Persistence\ManagerRegistry $registry) + { + parent::__construct($registry, Secret::class); + } + } +} else { + /** + * Dummy-repository for php < 8.0 + */ + class SecretRepository + { + public function findAll() + { + return []; + } + + public function find($id, $lockMode = null, $lockVersion = null) + { + return null; + } + + public function findOneBy(array $criteria, array $orderBy = null) + { + return null; + } + + public function findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) + { + return []; + } + } +} \ No newline at end of file diff --git a/demo/symfony4.4/symfony.lock b/demo/symfony4.4/symfony.lock new file mode 100644 index 00000000..2a2aaeab --- /dev/null +++ b/demo/symfony4.4/symfony.lock @@ -0,0 +1,266 @@ +{ + "absolute-quantum/doctrine-encrypt-bundle": { + "version": "dev-master-fixes" + }, + "doctrine/annotations": { + "version": "1.13", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "master", + "version": "1.0", + "ref": "a2759dd6123694c8d901d0ec80006e044c2e6457" + }, + "files": [ + "config/routes/annotations.yaml" + ] + }, + "doctrine/cache": { + "version": "2.1.1" + }, + "doctrine/collections": { + "version": "1.6.8" + }, + "doctrine/common": { + "version": "3.2.2" + }, + "doctrine/dbal": { + "version": "3.3.2" + }, + "doctrine/deprecations": { + "version": "v0.5.3" + }, + "doctrine/doctrine-bundle": { + "version": "2.5", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "master", + "version": "2.4", + "ref": "f98f1affe028f8153a459d15f220ada3826b5aa2" + }, + "files": [ + "config/packages/doctrine.yaml", + "config/packages/prod/doctrine.yaml", + "config/packages/test/doctrine.yaml", + "src/Entity/.gitignore", + "src/Repository/.gitignore" + ] + }, + "doctrine/event-manager": { + "version": "1.1.1" + }, + "doctrine/inflector": { + "version": "2.0.4" + }, + "doctrine/instantiator": { + "version": "1.4.0" + }, + "doctrine/lexer": { + "version": "1.2.2" + }, + "doctrine/orm": { + "version": "2.11.1" + }, + "doctrine/persistence": { + "version": "2.3.0" + }, + "doctrine/sql-formatter": { + "version": "1.1.2" + }, + "kporras07/composer-symlinks": { + "version": "v1.1" + }, + "paragonie/constant_time_encoding": { + "version": "v2.5.0" + }, + "paragonie/halite": { + "version": "v4.8.0" + }, + "paragonie/hidden-string": { + "version": "v2.0.0" + }, + "paragonie/sodium_compat": { + "version": "v1.17.0" + }, + "psr/cache": { + "version": "2.0.0" + }, + "psr/container": { + "version": "1.1.2" + }, + "psr/log": { + "version": "2.0.0" + }, + "symfony/cache": { + "version": "v4.4.37" + }, + "symfony/cache-contracts": { + "version": "v2.5.0" + }, + "symfony/config": { + "version": "v4.4.37" + }, + "symfony/console": { + "version": "4.4", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "master", + "version": "4.4", + "ref": "fd5340d07d4c90504843b53da41525cf42e31f5c" + }, + "files": [ + "bin/console", + "config/bootstrap.php" + ] + }, + "symfony/debug": { + "version": "v4.4.37" + }, + "symfony/dependency-injection": { + "version": "v4.4.37" + }, + "symfony/deprecation-contracts": { + "version": "v2.5.0" + }, + "symfony/doctrine-bridge": { + "version": "v4.4.37" + }, + "symfony/dotenv": { + "version": "v4.4.37" + }, + "symfony/error-handler": { + "version": "v4.4.37" + }, + "symfony/event-dispatcher": { + "version": "v4.4.37" + }, + "symfony/event-dispatcher-contracts": { + "version": "v1.1.11" + }, + "symfony/filesystem": { + "version": "v4.4.37" + }, + "symfony/finder": { + "version": "v4.4.37" + }, + "symfony/flex": { + "version": "1.18", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "master", + "version": "1.0", + "ref": "c0eeb50665f0f77226616b6038a9b06c03752d8e" + }, + "files": [ + ".env" + ] + }, + "symfony/framework-bundle": { + "version": "4.4", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "master", + "version": "4.4", + "ref": "24eb45d1355810154890460e6a05c0ca27318fe7" + }, + "files": [ + "config/bootstrap.php", + "config/packages/cache.yaml", + "config/packages/framework.yaml", + "config/packages/test/framework.yaml", + "config/preload.php", + "config/routes/dev/framework.yaml", + "config/services.yaml", + "public/index.php", + "src/Controller/.gitignore", + "src/Kernel.php" + ] + }, + "symfony/http-client-contracts": { + "version": "v2.5.0" + }, + "symfony/http-foundation": { + "version": "v4.4.37" + }, + "symfony/http-kernel": { + "version": "v4.4.37" + }, + "symfony/inflector": { + "version": "v4.4.37" + }, + "symfony/mime": { + "version": "v4.4.37" + }, + "symfony/polyfill-intl-idn": { + "version": "v1.24.0" + }, + "symfony/polyfill-intl-normalizer": { + "version": "v1.24.0" + }, + "symfony/polyfill-mbstring": { + "version": "v1.24.0" + }, + "symfony/polyfill-php72": { + "version": "v1.24.0" + }, + "symfony/polyfill-php73": { + "version": "v1.24.0" + }, + "symfony/polyfill-php80": { + "version": "v1.24.0" + }, + "symfony/polyfill-php81": { + "version": "v1.24.0" + }, + "symfony/property-access": { + "version": "v4.4.37" + }, + "symfony/routing": { + "version": "4.4", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "master", + "version": "4.2", + "ref": "683dcb08707ba8d41b7e34adb0344bfd68d248a7" + }, + "files": [ + "config/packages/prod/routing.yaml", + "config/packages/routing.yaml", + "config/routes.yaml" + ] + }, + "symfony/service-contracts": { + "version": "v2.5.0" + }, + "symfony/translation-contracts": { + "version": "v2.5.0" + }, + "symfony/twig-bridge": { + "version": "v4.4.37" + }, + "symfony/twig-bundle": { + "version": "4.4", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "master", + "version": "4.4", + "ref": "73baff3f7b3cea12a73812a7cfd2c0924a9e250f" + }, + "files": [ + "config/packages/test/twig.yaml", + "config/packages/twig.yaml", + "templates/base.html.twig" + ] + }, + "symfony/var-dumper": { + "version": "v4.4.37" + }, + "symfony/var-exporter": { + "version": "v4.4.37" + }, + "symfony/yaml": { + "version": "v4.4.37" + }, + "twig/twig": { + "version": "v3.3.8" + } +} diff --git a/demo/symfony4.4/templates b/demo/symfony4.4/templates new file mode 120000 index 00000000..e6b9ddfe --- /dev/null +++ b/demo/symfony4.4/templates @@ -0,0 +1 @@ +../shared/templates \ No newline at end of file diff --git a/demo/symfony4.4/tests/SecretTest.php b/demo/symfony4.4/tests/SecretTest.php new file mode 100644 index 00000000..d66ffacf --- /dev/null +++ b/demo/symfony4.4/tests/SecretTest.php @@ -0,0 +1,78 @@ +get('doctrine.orm.entity_manager'); + + // Make sure we do not store testdata + $entityManager->beginTransaction(); + + $name = 'test123'; + $secretString = 'i am a secret string'; + + // Create entity to test with + $newSecretObject = (new $className) + ->setName($name) + ->setSecret($secretString); + + $entityManager->persist($newSecretObject); + $entityManager->flush(); + + // Fetch the actual data + $secretRepository = $entityManager->getRepository($className); + $qb = $secretRepository->createQueryBuilder('s'); + $qb->select('s') + ->addSelect('(s.secret) as rawSecret') + ->where('s.name = :name') + ->setParameter('name',$name) + ->orderBy('s.name','ASC'); + $result = $qb->getQuery()->getSingleResult(); + + $actualSecretObject = $result[0]; + $actualRawSecret = $result['rawSecret']; + + self::assertInstanceOf($className,$actualSecretObject); + self::assertEquals($newSecretObject->getSecret(), $actualSecretObject->getSecret()); + self::assertEquals($newSecretObject->getName(), $actualSecretObject->getName()); + // Make sure it is encrypted + self::assertNotEquals($newSecretObject->getSecret(),$actualRawSecret); + self::assertStringEndsWith(DoctrineEncryptSubscriber::ENCRYPTION_MARKER,$actualRawSecret); + } + + /** + * @covers Entity\Annotation\Secret::getSecret + * @covers Entity\Annotation\Secret::getName + */ + public function testAnnotationSecretsAreEncryptedInDatabase(): void + { + $this->testSecretsAreEncryptedInDatabase(Entity\Annotation\Secret::class); + } + + /** + * @covers Entity\Attribute\Secret::getSecret + * @covers Entity\Attribute\Secret::getName + * @requires PHP 8.0 + */ + public function testAttributeSecretsAreEncryptedInDatabase(): void + { + $this->testSecretsAreEncryptedInDatabase(Entity\Attribute\Secret::class); + } +} diff --git a/demo/symfony6.x/.env b/demo/symfony6.x/.env new file mode 100644 index 00000000..54ba0e01 --- /dev/null +++ b/demo/symfony6.x/.env @@ -0,0 +1,28 @@ +# In all environments, the following files are loaded if they exist, +# the latter taking precedence over the former: +# +# * .env contains default values for the environment variables needed by the app +# * .env.local uncommitted file with local overrides +# * .env.$APP_ENV committed environment-specific defaults +# * .env.$APP_ENV.local uncommitted environment-specific overrides +# +# Real environment variables win over .env files. +# +# DO NOT DEFINE PRODUCTION SECRETS IN THIS FILE NOR IN ANY OTHER COMMITTED FILES. +# +# Run "composer dump-env prod" to compile .env files for production use (requires symfony/flex >=1.2). +# https://symfony.com/doc/current/best_practices.html#use-environment-variables-for-infrastructure-configuration + +###> symfony/framework-bundle ### +APP_ENV=dev +APP_SECRET=6962c9529b154a9ca68b705e88c4d677 +###< symfony/framework-bundle ### +###> doctrine/doctrine-bundle ### +# Format described at https://www.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/configuration.html#connecting-using-a-url +# IMPORTANT: You MUST configure your server version, either here or in config/packages/doctrine.yaml +# +# DATABASE_URL="sqlite:///%kernel.project_dir%/var/data.db" +# DATABASE_URL="mysql://db_user:db_password@127.0.0.1:3306/db_name?serverVersion=5.7" +DATABASE_PATH="var/data.db" +DATABASE_URL="sqlite:///%kernel.project_dir%/var/data.db" +###< doctrine/doctrine-bundle ### diff --git a/demo/symfony6.x/.env.test b/demo/symfony6.x/.env.test new file mode 100644 index 00000000..9e7162f0 --- /dev/null +++ b/demo/symfony6.x/.env.test @@ -0,0 +1,6 @@ +# define your env variables for the test env here +KERNEL_CLASS='App\Kernel' +APP_SECRET='$ecretf0rt3st' +SYMFONY_DEPRECATIONS_HELPER=999999 +PANTHER_APP_ENV=panther +PANTHER_ERROR_SCREENSHOT_DIR=./var/error-screenshots diff --git a/demo/symfony6.x/.gitignore b/demo/symfony6.x/.gitignore new file mode 100644 index 00000000..9daab39b --- /dev/null +++ b/demo/symfony6.x/.gitignore @@ -0,0 +1,27 @@ +# Ignore symlinks +/templates +/var/data.db +/.Halite.key + +# Ignore locked versions of composer +composer.lock + +###> symfony/framework-bundle ### +/.env.local +/.env.local.php +/.env.*.local +/config/secrets/prod/prod.decrypt.private.php +/public/bundles/ +/var/ +/vendor/ +###< symfony/framework-bundle ### + +###> symfony/phpunit-bridge ### +.phpunit.result.cache +/phpunit.xml +###< symfony/phpunit-bridge ### + +###> phpunit/phpunit ### +/phpunit.xml +.phpunit.result.cache +###< phpunit/phpunit ### diff --git a/demo/symfony6.x/README.md b/demo/symfony6.x/README.md new file mode 100644 index 00000000..ba4ace44 --- /dev/null +++ b/demo/symfony6.x/README.md @@ -0,0 +1,26 @@ +This demo-installation demonstrates a simple symfony 4.4-application using both annotations and, when using php > 8.0, attributes. + +# How to use +```shell +# run `composer install` in parent-directory to create symlinks for shared files +cd ../ +composer install +cd - + +# Install packages +composer install + +# Serve the application +## Using symfony-cli +symfony serve +## Using php-builtin-server () +php -S localhost:8000 -t public +## Other possibility +### Use a webserver like apache or nginx + + +# Run unittests +vendor/bin/simple-phpunit +``` + + diff --git a/demo/symfony6.x/bin/console b/demo/symfony6.x/bin/console new file mode 100755 index 00000000..c933dc53 --- /dev/null +++ b/demo/symfony6.x/bin/console @@ -0,0 +1,17 @@ +#!/usr/bin/env php +=8.0.2", + "ext-ctype": "*", + "ext-iconv": "*", + "absolute-quantum/doctrine-encrypt-bundle": "@dev", + "symfony/console": "6.0.*", + "symfony/dotenv": "6.0.*", + "symfony/flex": "^2", + "symfony/framework-bundle": "6.0.*", + "symfony/runtime": "6.0.*", + "symfony/twig-bundle": "6.0.*" + }, + "require-dev": { + "phpunit/phpunit": "^9.5", + "symfony/maker-bundle": "^1.0", + "symfony/phpunit-bridge": "^6.0" + }, + "config": { + "allow-plugins": { + "composer/package-versions-deprecated": true, + "symfony/flex": true, + "symfony/runtime": true + }, + "optimize-autoloader": true, + "preferred-install": { + "*": "dist" + }, + "sort-packages": true + }, + "autoload": { + "psr-4": { + "App\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "App\\Tests\\": "tests/" + } + }, + "replace": { + "symfony/polyfill-ctype": "*", + "symfony/polyfill-iconv": "*", + "symfony/polyfill-php72": "*", + "symfony/polyfill-php73": "*", + "symfony/polyfill-php74": "*", + "symfony/polyfill-php80": "*" + }, + "scripts": { + "auto-scripts": { + "cache:clear": "symfony-cmd", + "assets:install %PUBLIC_DIR%": "symfony-cmd" + }, + "post-install-cmd": [ + "@auto-scripts" + ], + "post-update-cmd": [ + "@auto-scripts" + ] + }, + "conflict": { + "symfony/symfony": "*" + }, + "extra": { + "symfony": { + "allow-contrib": false, + "require": "6.0.*" + } + }, + "repositories": [ + { + "type": "path", + "url": "../../" + } + ] +} diff --git a/demo/symfony6.x/config/bundles.php b/demo/symfony6.x/config/bundles.php new file mode 100644 index 00000000..b7e7b92c --- /dev/null +++ b/demo/symfony6.x/config/bundles.php @@ -0,0 +1,9 @@ + ['all' => true], + Symfony\Bundle\TwigBundle\TwigBundle::class => ['all' => true], + Symfony\Bundle\MakerBundle\MakerBundle::class => ['dev' => true], + Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true], + Ambta\DoctrineEncryptBundle\AmbtaDoctrineEncryptBundle::class => ['all' => true], +]; diff --git a/demo/symfony6.x/config/packages/cache.yaml b/demo/symfony6.x/config/packages/cache.yaml new file mode 100644 index 00000000..6899b720 --- /dev/null +++ b/demo/symfony6.x/config/packages/cache.yaml @@ -0,0 +1,19 @@ +framework: + cache: + # Unique name of your app: used to compute stable namespaces for cache keys. + #prefix_seed: your_vendor_name/app_name + + # The "app" cache stores to the filesystem by default. + # The data in this cache should persist between deploys. + # Other options include: + + # Redis + #app: cache.adapter.redis + #default_redis_provider: redis://localhost + + # APCu (not recommended with heavy random-write workloads as memory fragmentation can cause perf issues) + #app: cache.adapter.apcu + + # Namespaced pools use the above "app" backend by default + #pools: + #my.dedicated.cache: null diff --git a/demo/symfony6.x/config/packages/doctrine.yaml b/demo/symfony6.x/config/packages/doctrine.yaml new file mode 100644 index 00000000..bff6abbf --- /dev/null +++ b/demo/symfony6.x/config/packages/doctrine.yaml @@ -0,0 +1,49 @@ +doctrine: + dbal: + url: '%env(resolve:DATABASE_URL)%' + + # IMPORTANT: You MUST configure your server version, + # either here or in the DATABASE_URL env var (see .env file) + #server_version: '13' + orm: + auto_generate_proxy_classes: true + naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware + auto_mapping: true + mappings: + Annotation: + type: 'annotation' + is_bundle: false + dir: '%kernel.project_dir%/src/Entity/Annotation' + prefix: 'App\Entity\Annotation' + alias: App + Attributes: + type: 'attribute' + is_bundle: false + dir: '%kernel.project_dir%/src/Entity/Attribute' + prefix: 'App\Entity\Attribute' + alias: App + +when@test: + doctrine: + dbal: + # "TEST_TOKEN" is typically set by ParaTest + dbname_suffix: '_test%env(default::TEST_TOKEN)%' + +when@prod: + doctrine: + orm: + auto_generate_proxy_classes: false + query_cache_driver: + type: pool + pool: doctrine.system_cache_pool + result_cache_driver: + type: pool + pool: doctrine.result_cache_pool + + framework: + cache: + pools: + doctrine.result_cache_pool: + adapter: cache.app + doctrine.system_cache_pool: + adapter: cache.system diff --git a/demo/symfony6.x/config/packages/framework.yaml b/demo/symfony6.x/config/packages/framework.yaml new file mode 100644 index 00000000..7853e9ed --- /dev/null +++ b/demo/symfony6.x/config/packages/framework.yaml @@ -0,0 +1,24 @@ +# see https://symfony.com/doc/current/reference/configuration/framework.html +framework: + secret: '%env(APP_SECRET)%' + #csrf_protection: true + http_method_override: false + + # 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: + test: true + session: + storage_factory_id: session.storage.factory.mock_file diff --git a/demo/symfony6.x/config/packages/routing.yaml b/demo/symfony6.x/config/packages/routing.yaml new file mode 100644 index 00000000..4b766ce5 --- /dev/null +++ b/demo/symfony6.x/config/packages/routing.yaml @@ -0,0 +1,12 @@ +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 + +when@prod: + framework: + router: + strict_requirements: null diff --git a/demo/symfony6.x/config/packages/twig.yaml b/demo/symfony6.x/config/packages/twig.yaml new file mode 100644 index 00000000..d863e8ec --- /dev/null +++ b/demo/symfony6.x/config/packages/twig.yaml @@ -0,0 +1,8 @@ +twig: + default_path: '%kernel.project_dir%/templates' + globals: + appVersion: "6.x" + +when@test: + twig: + strict_variables: true diff --git a/demo/symfony6.x/config/preload.php b/demo/symfony6.x/config/preload.php new file mode 100644 index 00000000..5ebcdb21 --- /dev/null +++ b/demo/symfony6.x/config/preload.php @@ -0,0 +1,5 @@ + + + + + + + + + + + + + + + + tests + + + + + + src + + + + + + + + + + diff --git a/demo/symfony6.x/public/index.php b/demo/symfony6.x/public/index.php new file mode 100644 index 00000000..9982c218 --- /dev/null +++ b/demo/symfony6.x/public/index.php @@ -0,0 +1,9 @@ +findAll(), + $secretUsingAttributesRepository->findAll() + ); + + return $this->render('index.html.twig',['secrets' => $secrets]); + } + + /** + * @Route(name="create", path="/create") + */ + public function create(Request $request, EntityManagerInterface $em): Response + { + if (!$request->query->has('name') || !$request->query->has('secret') || !$request->query->has('type')) { + return new Response('Please specify name, secret and type in url-query'); + } + + $type = $request->query->get('type'); + if ($type === 'annotation') { + $secret = new \App\Entity\Annotation\Secret(); + } elseif($type === 'attribute') { + $secret = new \App\Entity\Attribute\Secret(); + } else { + return new Response('Type is only allowed to be "annotation" or "attribute"'); + } + + $secret + ->setName($request->query->getAlnum('name')) + ->setSecret($request->query->getAlnum('secret')); + + $em->persist($secret); + $em->flush(); + + return new Response(sprintf('OK - secret %s stored',$secret->getName())); + } +} \ No newline at end of file diff --git a/demo/symfony6.x/src/Entity/.gitignore b/demo/symfony6.x/src/Entity/.gitignore new file mode 100644 index 00000000..e69de29b diff --git a/demo/symfony6.x/src/Entity/Annotation/Secret.php b/demo/symfony6.x/src/Entity/Annotation/Secret.php new file mode 100644 index 00000000..49dce58a --- /dev/null +++ b/demo/symfony6.x/src/Entity/Annotation/Secret.php @@ -0,0 +1,98 @@ +name; + } + + /** + * @param string $name + * + * @return $this + */ + public function setName($name): self + { + $this->name = $name; + + return $this; + } + + /** + * @return string + */ + public function getSecret() + { + return $this->secret; + } + + /** + * @param string $secret + * + * @return $this + */ + public function setSecret($secret): self + { + $this->secret = $secret; + + return $this; + } + + /** + * @return string + */ + public function getRawSecret() + { + return $this->rawSecret; + } + + /** + * @param mixed $rawSecret + * + * @return $this + */ + public function setRawSecret($rawSecret) + { + $this->rawSecret = $rawSecret; + + return $this; + } +} \ No newline at end of file diff --git a/demo/symfony6.x/src/Entity/Attribute/Secret.php b/demo/symfony6.x/src/Entity/Attribute/Secret.php new file mode 100644 index 00000000..c76a80db --- /dev/null +++ b/demo/symfony6.x/src/Entity/Attribute/Secret.php @@ -0,0 +1,94 @@ +name; + } + + /** + * @param string $name + * + * @return $this + */ + public function setName($name): self + { + $this->name = $name; + + return $this; + } + + /** + * @return string + */ + public function getSecret() + { + return $this->secret; + } + + /** + * @param string $secret + * + * @return $this + */ + public function setSecret($secret): self + { + $this->secret = $secret; + + return $this; + } + + /** + * @return string + */ + public function getRawSecret() + { + return $this->rawSecret; + } + + /** + * @param mixed $rawSecret + * + * @return $this + */ + public function setRawSecret($rawSecret) + { + $this->rawSecret = $rawSecret; + + return $this; + } +} \ No newline at end of file diff --git a/demo/symfony6.x/src/Entity/SecretInterface.php b/demo/symfony6.x/src/Entity/SecretInterface.php new file mode 100644 index 00000000..cd234732 --- /dev/null +++ b/demo/symfony6.x/src/Entity/SecretInterface.php @@ -0,0 +1,11 @@ + The objects. + */ + public function findAll() + { + $qb = $this->createQueryBuilder('s'); + $qb->select('s') + ->addSelect('(s.secret) as rawSecret') + ->orderBy('s.name','ASC'); + $rawResult = $qb->getQuery()->getResult(); + + $result = []; + foreach ($rawResult as $row) { + $secret = $row[0]; + $secret->setRawSecret($row['rawSecret']); + $result[] = $secret; + } + + return $result; + } +} \ No newline at end of file diff --git a/demo/symfony6.x/src/Repository/Annotation/SecretRepository.php b/demo/symfony6.x/src/Repository/Annotation/SecretRepository.php new file mode 100644 index 00000000..fc1427a5 --- /dev/null +++ b/demo/symfony6.x/src/Repository/Annotation/SecretRepository.php @@ -0,0 +1,27 @@ += 80000) { + /** + * @method Secret|null find($id, $lockMode = null, $lockVersion = null) + * @method Secret|null findOneBy(array $criteria, array $orderBy = null) + * @method Secret[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) + */ + class SecretRepository extends AbstractSecretRepository + { + public function __construct(\Doctrine\Common\Persistence\ManagerRegistry $registry) + { + parent::__construct($registry, Secret::class); + } + } +} else { + /** + * Dummy-repository for php < 8.0 + */ + class SecretRepository + { + public function findAll() + { + return []; + } + + public function find($id, $lockMode = null, $lockVersion = null) + { + return null; + } + + public function findOneBy(array $criteria, array $orderBy = null) + { + return null; + } + + public function findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) + { + return []; + } + } +} \ No newline at end of file diff --git a/demo/symfony6.x/symfony.lock b/demo/symfony6.x/symfony.lock new file mode 100644 index 00000000..aaf4fff0 --- /dev/null +++ b/demo/symfony6.x/symfony.lock @@ -0,0 +1,381 @@ +{ + "absolute-quantum/doctrine-encrypt-bundle": { + "version": "dev-master-fixes" + }, + "doctrine/annotations": { + "version": "1.13", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "master", + "version": "1.0", + "ref": "a2759dd6123694c8d901d0ec80006e044c2e6457" + }, + "files": [ + "config/routes/annotations.yaml" + ] + }, + "doctrine/cache": { + "version": "2.1.1" + }, + "doctrine/collections": { + "version": "1.6.8" + }, + "doctrine/common": { + "version": "3.2.2" + }, + "doctrine/dbal": { + "version": "3.3.2" + }, + "doctrine/deprecations": { + "version": "v0.5.3" + }, + "doctrine/doctrine-bundle": { + "version": "2.5", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "master", + "version": "2.4", + "ref": "06a5dfe6c1b12da89276b72ed281be757d24c8a0" + }, + "files": [ + "config/packages/doctrine.yaml", + "src/Entity/.gitignore", + "src/Repository/.gitignore" + ] + }, + "doctrine/event-manager": { + "version": "1.1.1" + }, + "doctrine/inflector": { + "version": "2.0.4" + }, + "doctrine/instantiator": { + "version": "1.4.0" + }, + "doctrine/lexer": { + "version": "1.2.2" + }, + "doctrine/orm": { + "version": "2.11.1" + }, + "doctrine/persistence": { + "version": "2.3.0" + }, + "doctrine/sql-formatter": { + "version": "1.1.2" + }, + "myclabs/deep-copy": { + "version": "1.10.2" + }, + "nikic/php-parser": { + "version": "v4.13.2" + }, + "paragonie/constant_time_encoding": { + "version": "v2.5.0" + }, + "paragonie/halite": { + "version": "v4.8.0" + }, + "paragonie/hidden-string": { + "version": "v2.0.0" + }, + "paragonie/random_compat": { + "version": "v9.99.100" + }, + "paragonie/sodium_compat": { + "version": "v1.17.0" + }, + "phar-io/manifest": { + "version": "2.0.3" + }, + "phar-io/version": { + "version": "3.1.1" + }, + "phpdocumentor/reflection-common": { + "version": "2.2.0" + }, + "phpdocumentor/reflection-docblock": { + "version": "5.3.0" + }, + "phpdocumentor/type-resolver": { + "version": "1.6.0" + }, + "phpspec/prophecy": { + "version": "v1.15.0" + }, + "phpunit/php-code-coverage": { + "version": "9.2.10" + }, + "phpunit/php-file-iterator": { + "version": "3.0.6" + }, + "phpunit/php-invoker": { + "version": "3.1.1" + }, + "phpunit/php-text-template": { + "version": "2.0.4" + }, + "phpunit/php-timer": { + "version": "5.0.3" + }, + "phpunit/phpunit": { + "version": "9.5", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "master", + "version": "9.3", + "ref": "a6249a6c4392e9169b87abf93225f7f9f59025e6" + }, + "files": [ + ".env.test", + "phpunit.xml.dist", + "tests/bootstrap.php" + ] + }, + "psr/cache": { + "version": "3.0.0" + }, + "psr/container": { + "version": "2.0.2" + }, + "psr/event-dispatcher": { + "version": "1.0.0" + }, + "psr/log": { + "version": "3.0.0" + }, + "sebastian/cli-parser": { + "version": "1.0.1" + }, + "sebastian/code-unit": { + "version": "1.0.8" + }, + "sebastian/code-unit-reverse-lookup": { + "version": "2.0.3" + }, + "sebastian/comparator": { + "version": "4.0.6" + }, + "sebastian/complexity": { + "version": "2.0.2" + }, + "sebastian/diff": { + "version": "4.0.4" + }, + "sebastian/environment": { + "version": "5.1.3" + }, + "sebastian/exporter": { + "version": "4.0.4" + }, + "sebastian/global-state": { + "version": "5.0.4" + }, + "sebastian/lines-of-code": { + "version": "1.0.3" + }, + "sebastian/object-enumerator": { + "version": "4.0.4" + }, + "sebastian/object-reflector": { + "version": "2.0.4" + }, + "sebastian/recursion-context": { + "version": "4.0.4" + }, + "sebastian/resource-operations": { + "version": "3.0.3" + }, + "sebastian/type": { + "version": "2.3.4" + }, + "sebastian/version": { + "version": "3.0.2" + }, + "symfony/cache": { + "version": "v6.0.3" + }, + "symfony/cache-contracts": { + "version": "v3.0.0" + }, + "symfony/config": { + "version": "v6.0.3" + }, + "symfony/console": { + "version": "6.0", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "master", + "version": "5.3", + "ref": "da0c8be8157600ad34f10ff0c9cc91232522e047" + }, + "files": [ + "bin/console" + ] + }, + "symfony/dependency-injection": { + "version": "v6.0.3" + }, + "symfony/deprecation-contracts": { + "version": "v3.0.0" + }, + "symfony/doctrine-bridge": { + "version": "v6.0.3" + }, + "symfony/dotenv": { + "version": "v6.0.3" + }, + "symfony/error-handler": { + "version": "v6.0.3" + }, + "symfony/event-dispatcher": { + "version": "v6.0.3" + }, + "symfony/event-dispatcher-contracts": { + "version": "v3.0.0" + }, + "symfony/filesystem": { + "version": "v6.0.3" + }, + "symfony/finder": { + "version": "v6.0.3" + }, + "symfony/flex": { + "version": "2.1", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "master", + "version": "1.0", + "ref": "c0eeb50665f0f77226616b6038a9b06c03752d8e" + }, + "files": [ + ".env" + ] + }, + "symfony/framework-bundle": { + "version": "6.0", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "master", + "version": "5.4", + "ref": "3cd216a4d007b78d8554d44a5b1c0a446dab24fb" + }, + "files": [ + "config/packages/cache.yaml", + "config/packages/framework.yaml", + "config/preload.php", + "config/routes/framework.yaml", + "config/services.yaml", + "public/index.php", + "src/Controller/.gitignore", + "src/Kernel.php" + ] + }, + "symfony/http-foundation": { + "version": "v6.0.3" + }, + "symfony/http-kernel": { + "version": "v6.0.4" + }, + "symfony/maker-bundle": { + "version": "1.36", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "master", + "version": "1.0", + "ref": "fadbfe33303a76e25cb63401050439aa9b1a9c7f" + } + }, + "symfony/phpunit-bridge": { + "version": "6.0", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "master", + "version": "5.3", + "ref": "97cb3dc7b0f39c7cfc4b7553504c9d7b7795de96" + }, + "files": [ + ".env.test", + "bin/phpunit", + "phpunit.xml.dist", + "tests/bootstrap.php" + ] + }, + "symfony/polyfill-intl-grapheme": { + "version": "v1.24.0" + }, + "symfony/polyfill-intl-normalizer": { + "version": "v1.24.0" + }, + "symfony/polyfill-mbstring": { + "version": "v1.24.0" + }, + "symfony/polyfill-php81": { + "version": "v1.24.0" + }, + "symfony/property-access": { + "version": "v6.0.3" + }, + "symfony/property-info": { + "version": "v6.0.3" + }, + "symfony/routing": { + "version": "6.0", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "master", + "version": "6.0", + "ref": "ab9ad892b7bba7ac584f6dc2ccdb659d358c63c5" + }, + "files": [ + "config/packages/routing.yaml", + "config/routes.yaml" + ] + }, + "symfony/runtime": { + "version": "v6.0.3" + }, + "symfony/service-contracts": { + "version": "v3.0.0" + }, + "symfony/string": { + "version": "v6.0.3" + }, + "symfony/translation-contracts": { + "version": "v3.0.0" + }, + "symfony/twig-bridge": { + "version": "v6.0.3" + }, + "symfony/twig-bundle": { + "version": "6.0", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "master", + "version": "5.4", + "ref": "bffbb8f1a849736e64006735afae730cb428b6ff" + }, + "files": [ + "config/packages/twig.yaml", + "templates/base.html.twig" + ] + }, + "symfony/var-dumper": { + "version": "v6.0.3" + }, + "symfony/var-exporter": { + "version": "v6.0.3" + }, + "symfony/yaml": { + "version": "v6.0.3" + }, + "theseer/tokenizer": { + "version": "1.2.1" + }, + "twig/twig": { + "version": "v3.3.8" + }, + "webmozart/assert": { + "version": "1.10.0" + } +} diff --git a/demo/symfony6.x/templates b/demo/symfony6.x/templates new file mode 120000 index 00000000..e6b9ddfe --- /dev/null +++ b/demo/symfony6.x/templates @@ -0,0 +1 @@ +../shared/templates \ No newline at end of file diff --git a/demo/symfony6.x/tests/SecretTest.php b/demo/symfony6.x/tests/SecretTest.php new file mode 100644 index 00000000..e2783e66 --- /dev/null +++ b/demo/symfony6.x/tests/SecretTest.php @@ -0,0 +1,77 @@ +get('doctrine.orm.entity_manager'); + + // Make sure we do not store testdata + $entityManager->beginTransaction(); + + $name = 'test123'; + $secretString = 'i am a secret string'; + + // Create entity to test with + $newSecretObject = (new $className) + ->setName($name) + ->setSecret($secretString); + + $entityManager->persist($newSecretObject); + $entityManager->flush(); + + // Fetch the actual data + $secretRepository = $entityManager->getRepository($className); + $qb = $secretRepository->createQueryBuilder('s'); + $qb->select('s') + ->addSelect('(s.secret) as rawSecret') + ->where('s.name = :name') + ->setParameter('name',$name) + ->orderBy('s.name','ASC'); + $result = $qb->getQuery()->getSingleResult(); + + $actualSecretObject = $result[0]; + $actualRawSecret = $result['rawSecret']; + + self::assertInstanceOf($className,$actualSecretObject); + self::assertEquals($newSecretObject->getSecret(), $actualSecretObject->getSecret()); + self::assertEquals($newSecretObject->getName(), $actualSecretObject->getName()); + // Make sure it is encrypted + self::assertNotEquals($newSecretObject->getSecret(),$actualRawSecret); + self::assertStringEndsWith(DoctrineEncryptSubscriber::ENCRYPTION_MARKER,$actualRawSecret); + } + + /** + * @covers Entity\Annotation\Secret::getSecret + * @covers Entity\Annotation\Secret::getName + */ + public function testAnnotationSecretsAreEncryptedInDatabase() + { + $this->testSecretsAreEncryptedInDatabase(Entity\Annotation\Secret::class); + } + + /** + * @covers Entity\Attribute\Secret::getSecret + * @covers Entity\Attribute\Secret::getName + * @requires PHP 8.0 + */ + public function testAttributeSecretsAreEncryptedInDatabase() + { + $this->testSecretsAreEncryptedInDatabase(Entity\Attribute\Secret::class); + } +} diff --git a/demo/symfony6.x/tests/bootstrap.php b/demo/symfony6.x/tests/bootstrap.php new file mode 100644 index 00000000..469dccee --- /dev/null +++ b/demo/symfony6.x/tests/bootstrap.php @@ -0,0 +1,11 @@ +bootEnv(dirname(__DIR__).'/.env'); +} diff --git a/phpstan.neon.dist b/phpstan.neon.dist new file mode 100644 index 00000000..743101ea --- /dev/null +++ b/phpstan.neon.dist @@ -0,0 +1,6 @@ +parameters: + level: 0 + paths: + - src + - tests + tmpDir: .phpstan-cache diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 80037be4..e0767699 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,6 +1,6 @@ - - ./Tests/Unit + ./tests/Unit - ./Tests/Functional + ./tests/Functional @@ -26,7 +26,8 @@ - + + diff --git a/src/Command/AbstractCommand.php b/src/Command/AbstractCommand.php index 489875a3..a59263cb 100644 --- a/src/Command/AbstractCommand.php +++ b/src/Command/AbstractCommand.php @@ -16,19 +16,19 @@ abstract class AbstractCommand extends Command { /** - * @var EntityManagerInterface + * @var EntityManagerInterface|EntityManager */ - protected EntityManagerInterface|EntityManager $entityManager; + protected $entityManager; /** * @var DoctrineEncryptSubscriber */ - protected DoctrineEncryptSubscriber $subscriber; + protected $subscriber; /** * @var Reader */ - protected Reader $annotationReader; + protected $annotationReader; /** * AbstractCommand constructor. diff --git a/src/Command/DoctrineDecryptDatabaseCommand.php b/src/Command/DoctrineDecryptDatabaseCommand.php index 52fd45dd..e0bd9226 100644 --- a/src/Command/DoctrineDecryptDatabaseCommand.php +++ b/src/Command/DoctrineDecryptDatabaseCommand.php @@ -146,6 +146,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int } $output->writeln('' . PHP_EOL . 'Decryption finished values found: ' . $valueCounter . ', decrypted: ' . $this->subscriber->decryptCounter . '.' . PHP_EOL . 'All values are now decrypted.'); - return 1; + + return 0; } } diff --git a/src/Command/DoctrineEncryptDatabaseCommand.php b/src/Command/DoctrineEncryptDatabaseCommand.php index 7bb03502..114fef33 100644 --- a/src/Command/DoctrineEncryptDatabaseCommand.php +++ b/src/Command/DoctrineEncryptDatabaseCommand.php @@ -101,7 +101,8 @@ protected function execute(InputInterface $input, OutputInterface $output): int // Say it is finished $output->writeln('Encryption finished. Values encrypted: ' . $this->subscriber->encryptCounter . ' values.' . PHP_EOL . 'All values are now encrypted.'); - return 1; + + return 0; } diff --git a/src/Command/DoctrineEncryptStatusCommand.php b/src/Command/DoctrineEncryptStatusCommand.php index 4338c5bf..998060f2 100644 --- a/src/Command/DoctrineEncryptStatusCommand.php +++ b/src/Command/DoctrineEncryptStatusCommand.php @@ -33,7 +33,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $totalCount = 0; foreach ($metaDataArray as $metaData) { - if ($metaData instanceof ClassMetadataInfo and $metaData->isMappedSuperclass) { + if ($metaData instanceof ClassMetadataInfo && $metaData->isMappedSuperclass) { continue; } @@ -53,6 +53,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $output->writeln(''); $output->writeln(sprintf('%d entities found which are containing %d encrypted properties.', count($metaDataArray), $totalCount)); - return 1; + + return 0; } } diff --git a/src/DependencyInjection/DoctrineEncryptExtension.php b/src/DependencyInjection/DoctrineEncryptExtension.php index 4914ea40..d12be8e9 100644 --- a/src/DependencyInjection/DoctrineEncryptExtension.php +++ b/src/DependencyInjection/DoctrineEncryptExtension.php @@ -31,7 +31,7 @@ public function load(array $configs, ContainerBuilder $container) $config = $this->processConfiguration($configuration, $configs); // If empty encryptor class, use Halite encryptor - if (in_array($config['encryptor_class'], array_keys(self::SupportedEncryptorClasses))) { + if (array_key_exists($config['encryptor_class'], self::SupportedEncryptorClasses)) { $config['encryptor_class_full'] = self::SupportedEncryptorClasses[$config['encryptor_class']]; } else { $config['encryptor_class_full'] = $config['encryptor_class']; @@ -44,6 +44,11 @@ public function load(array $configs, ContainerBuilder $container) // Load service file $loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__ . '/../Resources/config')); $loader->load('services.yml'); + + // Remove usage of AttributeAnnotationReader when using php < 8.0 + if (PHP_VERSION_ID < 80000) { + $container->setAlias('ambta_doctrine_annotation_reader','annotations.reader'); + } } /** diff --git a/src/Encryptors/DefuseEncryptor.php b/src/Encryptors/DefuseEncryptor.php index 0079aba8..56a08350 100644 --- a/src/Encryptors/DefuseEncryptor.php +++ b/src/Encryptors/DefuseEncryptor.php @@ -12,9 +12,12 @@ class DefuseEncryptor implements EncryptorInterface { - private Filesystem $fs; - private ?string $encryptionKey = null; - private string $keyFile; + /** @var Filesystem */ + private $fs; + /** @var string|null */ + private $encryptionKey = null; + /** @var string */ + private $keyFile; /** * {@inheritdoc} diff --git a/src/Encryptors/HaliteEncryptor.php b/src/Encryptors/HaliteEncryptor.php index 07653093..2c3fb979 100644 --- a/src/Encryptors/HaliteEncryptor.php +++ b/src/Encryptors/HaliteEncryptor.php @@ -16,8 +16,10 @@ class HaliteEncryptor implements EncryptorInterface { - private ?EncryptionKey $encryptionKey = null; - private string $keyFile; + /** @var EncryptionKey|null */ + private $encryptionKey = null; + /** @var string */ + private $keyFile; /** * {@inheritdoc} @@ -40,14 +42,7 @@ public function encrypt(string $data): string */ public function decrypt(string $data): string { - $data = Crypto::decrypt($data, $this->getKey()); - - if ($data instanceof HiddenString) - { - $data = $data->getString(); - } - - return $data; + return Crypto::decrypt($data, $this->getKey())->getString(); } private function getKey(): EncryptionKey diff --git a/src/Mapping/AttributeAnnotationReader.php b/src/Mapping/AttributeAnnotationReader.php index 86309e45..8b9402fa 100644 --- a/src/Mapping/AttributeAnnotationReader.php +++ b/src/Mapping/AttributeAnnotationReader.php @@ -17,12 +17,12 @@ final class AttributeAnnotationReader implements Reader /** * @var Reader */ - private Reader $annotationReader; + private $annotationReader; /** * @var AttributeReader */ - private AttributeReader $attributeReader; + private $attributeReader; public function __construct(AttributeReader $attributeReader, Reader $annotationReader) { diff --git a/src/Mapping/AttributeReader.php b/src/Mapping/AttributeReader.php index d8d36470..2b600fd2 100644 --- a/src/Mapping/AttributeReader.php +++ b/src/Mapping/AttributeReader.php @@ -14,7 +14,7 @@ final class AttributeReader { /** @var array */ - private array $isRepeatableAttribute = []; + private $isRepeatableAttribute = []; /** * @param ReflectionClass $class @@ -30,7 +30,7 @@ public function getClassAnnotations(ReflectionClass $class): array * * @return Annotation|Annotation[]|null */ - public function getClassAnnotation(ReflectionClass $class, string $annotationName): array|Annotation|null + public function getClassAnnotation(ReflectionClass $class, string $annotationName) { return $this->getClassAnnotations($class)[$annotationName] ?? null; } @@ -49,7 +49,7 @@ public function getPropertyAnnotations(\ReflectionProperty $property): array * * @return Annotation|Annotation[]|null */ - public function getPropertyAnnotation(\ReflectionProperty $property, string $annotationName): array|Annotation|null + public function getPropertyAnnotation(\ReflectionProperty $property, string $annotationName) { return $this->getPropertyAnnotations($property)[$annotationName] ?? null; } @@ -100,3 +100,5 @@ private function isRepeatable(string $attributeClassName): bool return $this->isRepeatableAttribute[$attributeClassName] = ($attribute->flags & Attribute::IS_REPEATABLE) > 0; } } + + diff --git a/src/Subscribers/DoctrineEncryptSubscriber.php b/src/Subscribers/DoctrineEncryptSubscriber.php index 06f05df4..fe6c0404 100644 --- a/src/Subscribers/DoctrineEncryptSubscriber.php +++ b/src/Subscribers/DoctrineEncryptSubscriber.php @@ -2,6 +2,8 @@ namespace Ambta\DoctrineEncryptBundle\Subscribers; +use Doctrine\ORM\EntityManagerInterface; +use Doctrine\ORM\Event\OnClearEventArgs; use Doctrine\ORM\Event\PreFlushEventArgs; use ReflectionClass; use Doctrine\ORM\Event\PostFlushEventArgs; @@ -40,34 +42,34 @@ class DoctrineEncryptSubscriber implements EventSubscriber * Encryptor * @var EncryptorInterface|null */ - private ?EncryptorInterface $encryptor; + private $encryptor; /** * Annotation reader * @var Reader */ - private Reader $annReader; + private $annReader; /** * Used for restoring the encryptor after changing it * @var EncryptorInterface|string */ - private EncryptorInterface|string $restoreEncryptor; + private $restoreEncryptor; /** * Count amount of decrypted values in this service * @var integer */ - public int $decryptCounter = 0; + public $decryptCounter = 0; /** * Count amount of encrypted values in this service * @var integer */ - public int $encryptCounter = 0; + public $encryptCounter = 0; /** @var array */ - private array $cachedDecryptions = []; + private $cachedDecryptions = []; /** * Initialization of subscriber @@ -122,7 +124,7 @@ public function restoreEncryptor() public function postUpdate(LifecycleEventArgs $args) { $entity = $args->getEntity(); - $this->processFields($entity, false); + $this->processFields($entity, $args->getEntityManager(), false); } /** @@ -134,7 +136,7 @@ public function postUpdate(LifecycleEventArgs $args) public function preUpdate(PreUpdateEventArgs $args) { $entity = $args->getEntity(); - $this->processFields($entity); + $this->processFields($entity, $args->getEntityManager(), true); } /** @@ -146,7 +148,7 @@ public function preUpdate(PreUpdateEventArgs $args) public function postLoad(LifecycleEventArgs $args) { $entity = $args->getEntity(); - $this->processFields($entity, false); + $this->processFields($entity, $args->getEntityManager(), false); } /** @@ -161,7 +163,7 @@ public function preFlush(PreFlushEventArgs $preFlushEventArgs) foreach ($unitOfWOrk->getIdentityMap() as $entityName => $entityArray) { if (isset($this->cachedDecryptions[$entityName])) { foreach ($entityArray as $entityId => $instance) { - $this->processFields($instance); + $this->processFields($instance, $preFlushEventArgs->getEntityManager(), true); } } } @@ -179,7 +181,7 @@ public function onFlush(OnFlushEventArgs $onFlushEventArgs) $unitOfWork = $onFlushEventArgs->getEntityManager()->getUnitOfWork(); foreach ($unitOfWork->getScheduledEntityInsertions() as $entity) { $encryptCounterBefore = $this->encryptCounter; - $this->processFields($entity); + $this->processFields($entity,$onFlushEventArgs->getEntityManager(),true); if ($this->encryptCounter > $encryptCounterBefore ) { $classMetadata = $onFlushEventArgs->getEntityManager()->getClassMetadata(get_class($entity)); $unitOfWork->recomputeSingleEntityChangeSet($classMetadata, $entity); @@ -198,11 +200,19 @@ public function postFlush(PostFlushEventArgs $postFlushEventArgs) $unitOfWork = $postFlushEventArgs->getEntityManager()->getUnitOfWork(); foreach ($unitOfWork->getIdentityMap() as $entityMap) { foreach ($entityMap as $entity) { - $this->processFields($entity, false); + $classMetadata = $postFlushEventArgs->getEntityManager()->getClassMetadata(get_class($entity)); + $this->processFields($entity,$postFlushEventArgs->getEntityManager(), false); } } } + public function onClear(OnClearEventArgs $onClearEventArgs) + { + $this->cachedDecryptions = []; + $this->decryptCounter = 0; + $this->encryptCounter = 0; + } + /** * Realization of EventSubscriber interface method. * @@ -217,6 +227,7 @@ public function getSubscribedEvents(): array Events::onFlush, Events::preFlush, Events::postFlush, + Events::onClear, ); } @@ -224,13 +235,14 @@ public function getSubscribedEvents(): array * Process (encrypt/decrypt) entities fields * * @param Object $entity doctrine entity + * @param EntityManagerInterface $entityManager * @param Boolean $isEncryptOperation If true - encrypt, false - decrypt entity * * @return object|null *@throws \RuntimeException * */ - public function processFields(object $entity, bool $isEncryptOperation = true): ?object + public function processFields(object $entity, EntityManagerInterface $entityManager, bool $isEncryptOperation = true): ?object { if (!empty($this->encryptor)) { // Check which operation to be used @@ -244,7 +256,7 @@ public function processFields(object $entity, bool $isEncryptOperation = true): // Foreach property in the reflection class foreach ($properties as $refProperty) { if ($this->annReader->getPropertyAnnotation($refProperty, 'Doctrine\ORM\Mapping\Embedded')) { - $this->handleEmbeddedAnnotation($entity, $refProperty, $isEncryptOperation); + $this->handleEmbeddedAnnotation($entity, $entityManager, $refProperty, $isEncryptOperation); continue; } @@ -252,21 +264,23 @@ public function processFields(object $entity, bool $isEncryptOperation = true): * If property is an normal value and contains the Encrypt tag, lets encrypt/decrypt that property */ if ($this->annReader->getPropertyAnnotation($refProperty, self::ENCRYPTED_ANN_NAME)) { + $rootEntityName = $entityManager->getClassMetadata(get_class($entity))->rootEntityName; + $pac = PropertyAccess::createPropertyAccessor(); $value = $pac->getValue($entity, $refProperty->getName()); - if ($encryptorMethod == 'decrypt') { + if ($encryptorMethod === 'decrypt') { if (!is_null($value) and !empty($value)) { if (substr($value, -strlen(self::ENCRYPTION_MARKER)) == self::ENCRYPTION_MARKER) { $this->decryptCounter++; $currentPropValue = $this->encryptor->decrypt(substr($value, 0, -5)); $pac->setValue($entity, $refProperty->getName(), $currentPropValue); - $this->cachedDecryptions[get_class($entity)][spl_object_id($entity)][$refProperty->getName()][$currentPropValue] = $value; + $this->cachedDecryptions[$rootEntityName][spl_object_id($entity)][$refProperty->getName()][$currentPropValue] = $value; } } } else { if (!is_null($value) and !empty($value)) { - if (isset($this->cachedDecryptions[get_class($entity)][spl_object_id($entity)][$refProperty->getName()][$value])) { - $pac->setValue($entity, $refProperty->getName(), $this->cachedDecryptions[get_class($entity)][spl_object_id($entity)][$refProperty->getName()][$value]); + if (isset($this->cachedDecryptions[$rootEntityName][spl_object_id($entity)][$refProperty->getName()][$value])) { + $pac->setValue($entity, $refProperty->getName(), $this->cachedDecryptions[$rootEntityName][spl_object_id($entity)][$refProperty->getName()][$value]); } elseif (substr($value, -strlen(self::ENCRYPTION_MARKER)) != self::ENCRYPTION_MARKER) { $this->encryptCounter++; $currentPropValue = $this->encryptor->encrypt($value).self::ENCRYPTION_MARKER; @@ -283,7 +297,7 @@ public function processFields(object $entity, bool $isEncryptOperation = true): return $entity; } - private function handleEmbeddedAnnotation($entity, ReflectionProperty $embeddedProperty, bool $isEncryptOperation = true) + private function handleEmbeddedAnnotation($entity, EntityManagerInterface $entityManager, ReflectionProperty $embeddedProperty, bool $isEncryptOperation = true) { $propName = $embeddedProperty->getName(); @@ -292,7 +306,7 @@ private function handleEmbeddedAnnotation($entity, ReflectionProperty $embeddedP $embeddedEntity = $pac->getValue($entity, $propName); if ($embeddedEntity) { - $this->processFields($embeddedEntity, $isEncryptOperation); + $this->processFields($embeddedEntity, $entityManager, $isEncryptOperation); } } diff --git a/tests/DoctrineCompatibilityTrait.php b/tests/DoctrineCompatibilityTrait.php new file mode 100644 index 00000000..071cce40 --- /dev/null +++ b/tests/DoctrineCompatibilityTrait.php @@ -0,0 +1,36 @@ +executeQuery()->fetchAllAssociative(); + } else { + $statement->execute(); + return $statement->fetchAll(); + } + } + + /** + * Execute statement and fetch singe row + * + * Helper-method since methods changed in different supported versions of Doctrine + */ + private function executeStatementFetch(\Doctrine\DBAL\Statement $statement) + { + if (method_exists($statement,'executeQuery')) { + return $statement->executeQuery()->fetchAssociative(); + } else { + $statement->execute(); + return $statement->fetch(); + } + } +} \ No newline at end of file diff --git a/Tests/Functional/AbstractFunctionalTestCase.php b/tests/Functional/AbstractFunctionalTestCase.php similarity index 89% rename from Tests/Functional/AbstractFunctionalTestCase.php rename to tests/Functional/AbstractFunctionalTestCase.php index 97fc9384..d566a020 100644 --- a/Tests/Functional/AbstractFunctionalTestCase.php +++ b/tests/Functional/AbstractFunctionalTestCase.php @@ -90,11 +90,11 @@ protected function getLatestInsertQuery(): ?array protected function getLatestUpdateQuery(): ?array { - $insertQueries = array_values(array_filter($this->sqlLoggerStack->queries,static function ($queryData) { + $updateQueries = array_values(array_filter($this->sqlLoggerStack->queries,static function ($queryData) { return stripos($queryData['sql'], 'UPDATE ') === 0; })); - return current(array_reverse($insertQueries)) ?: null; + return current(array_reverse($updateQueries)) ?: null; } /** @@ -105,6 +105,11 @@ protected function getCurrentQueryCount(): int return count($this->sqlLoggerStack->queries); } + protected function resetQueryStack(): void + { + $this->sqlLoggerStack->queries = []; + } + /** * Asserts that a string starts with a given prefix. * @@ -114,17 +119,9 @@ protected function getCurrentQueryCount(): int */ public function assertStringDoesNotContain($needle, $string, $ignoreCase = false, $message = ''): void { - if (!\is_string($needle)) { - throw InvalidArgumentHelper::factory(1, 'string'); - } - - if (!\is_string($string)) { - throw InvalidArgumentHelper::factory(2, 'string'); - } - - if (!\is_bool($ignoreCase)) { - throw InvalidArgumentHelper::factory(3, 'bool'); - } + $this->assertIsString($needle,$message); + $this->assertIsString($string,$message); + $this->assertIsBool($ignoreCase,$message); $constraint = new LogicalNot(new StringContains( $needle, diff --git a/Tests/Functional/BasicQueryTest/AbstractBasicQueryTestCase.php b/tests/Functional/BasicQueryTest/AbstractBasicQueryTestCase.php similarity index 78% rename from Tests/Functional/BasicQueryTest/AbstractBasicQueryTestCase.php rename to tests/Functional/BasicQueryTest/AbstractBasicQueryTestCase.php index 0065311c..316b4979 100644 --- a/Tests/Functional/BasicQueryTest/AbstractBasicQueryTestCase.php +++ b/tests/Functional/BasicQueryTest/AbstractBasicQueryTestCase.php @@ -5,6 +5,7 @@ use Ambta\DoctrineEncryptBundle\Subscribers\DoctrineEncryptSubscriber; use Ambta\DoctrineEncryptBundle\Tests\Functional\AbstractFunctionalTestCase; use Ambta\DoctrineEncryptBundle\Tests\Functional\fixtures\Entity\CascadeTarget; +use Ambta\DoctrineEncryptBundle\Tests\Functional\fixtures\Entity\VehicleCar; abstract class AbstractBasicQueryTestCase extends AbstractFunctionalTestCase { @@ -81,4 +82,27 @@ public function testStoredDataIsEncrypted(): void $this->assertStringEndsWith(DoctrineEncryptSubscriber::ENCRYPTION_MARKER,$passwordData); $this->assertStringDoesNotContain('my secret',$passwordData); } + + public function testNoUpdateForUnalteredChildrenOfAbstractEntities() + { + $car = new VehicleCar(); + $car->setSecret('top secret information'); + $car->setNotSecret('123-test'); + $this->entityManager->persist($car); + $this->entityManager->flush(); + + // start transaction, insert, commit + $this->assertEquals(3,$this->getCurrentQueryCount()); + + // Remove all logged queries + $this->resetQueryStack(); + + // Set NotSecret with same data - this does not modify the entity and should not trigger an update + $car->setNotSecret('123-test'); + $this->entityManager->flush(); + + // Verify there are no queries executed + $this->assertNull($this->getLatestUpdateQuery()); + $this->assertEquals(0,$this->getCurrentQueryCount()); + } } diff --git a/Tests/Functional/BasicQueryTest/BasicQueryDefuseTest.php b/tests/Functional/BasicQueryTest/BasicQueryDefuseTest.php similarity index 100% rename from Tests/Functional/BasicQueryTest/BasicQueryDefuseTest.php rename to tests/Functional/BasicQueryTest/BasicQueryDefuseTest.php diff --git a/Tests/Functional/BasicQueryTest/BasicQueryHaliteTest.php b/tests/Functional/BasicQueryTest/BasicQueryHaliteTest.php similarity index 100% rename from Tests/Functional/BasicQueryTest/BasicQueryHaliteTest.php rename to tests/Functional/BasicQueryTest/BasicQueryHaliteTest.php diff --git a/Tests/Functional/DoctrineEncryptSubscriber/AbstractDoctrineEncryptSubscriberTestCase.php b/tests/Functional/DoctrineEncryptSubscriber/AbstractDoctrineEncryptSubscriberTestCase.php similarity index 51% rename from Tests/Functional/DoctrineEncryptSubscriber/AbstractDoctrineEncryptSubscriberTestCase.php rename to tests/Functional/DoctrineEncryptSubscriber/AbstractDoctrineEncryptSubscriberTestCase.php index 2c11b33f..6a2c57fe 100644 --- a/Tests/Functional/DoctrineEncryptSubscriber/AbstractDoctrineEncryptSubscriberTestCase.php +++ b/tests/Functional/DoctrineEncryptSubscriber/AbstractDoctrineEncryptSubscriberTestCase.php @@ -3,14 +3,19 @@ namespace Ambta\DoctrineEncryptBundle\Tests\Functional\DoctrineEncryptSubscriber; +use Ambta\DoctrineEncryptBundle\Tests\DoctrineCompatibilityTrait; use Ambta\DoctrineEncryptBundle\Tests\Functional\AbstractFunctionalTestCase; use Ambta\DoctrineEncryptBundle\Tests\Functional\fixtures\Entity\CascadeTarget; +use Ambta\DoctrineEncryptBundle\Tests\Functional\fixtures\Entity\ClassTableInheritanceBase; +use Ambta\DoctrineEncryptBundle\Tests\Functional\fixtures\Entity\ClassTableInheritanceChild; use Ambta\DoctrineEncryptBundle\Tests\Functional\fixtures\Entity\Owner; use Doctrine\DBAL\Logging\DebugStack; abstract class AbstractDoctrineEncryptSubscriberTestCase extends AbstractFunctionalTestCase { + use DoctrineCompatibilityTrait; + public function testEncryptionHappensOnOnlyAnnotatedFields(): void { $secret = "It's a secret"; @@ -33,8 +38,7 @@ public function testEncryptionHappensOnOnlyAnnotatedFields(): void $this->assertEquals($secret, $owner->getSecret()); $this->assertEquals($notSecret, $owner->getNotSecret()); $stmt->bindValue(1, $owner->getId()); - $stmt->execute(); - $results = $stmt->fetchAll(); + $results = $this->executeStatementFetchAll($stmt); $this->assertCount(1, $results); $result = $results[0]; $this->assertEquals($notSecret, $result['notSecret']); @@ -71,8 +75,7 @@ public function testEncryptionCascades(): void $this->assertEquals($secret, $cascadeTarget->getSecret()); $this->assertEquals($notSecret, $cascadeTarget->getNotSecret()); $stmt->bindValue(1, $cascadeTarget->getId()); - $stmt->execute(); - $results = $stmt->fetchAll(); + $results = $this->executeStatementFetchAll($stmt); $this->assertCount(1, $results); $result = $results[0]; $this->assertEquals($notSecret, $result['notSecret']); @@ -82,11 +85,58 @@ public function testEncryptionCascades(): void $this->assertEquals($secret, $decrypted); } + public function testEncryptionClassTableInheritance(): void + { + $secretBase = "It's a secret. On the base class."; + $notSecretBase = "You're all welcome to know this. On the base class."; + $secretChild = "It's a secret. On the child class."; + $notSecretChild = "You're all welcome to know this. On the child class."; + $em = $this->entityManager; + $child = new ClassTableInheritanceChild(); + $child->setSecretBase($secretBase); + $child->setNotSecretBase($notSecretBase); + $child->setSecretChild($secretChild); + $child->setNotSecretChild($notSecretChild); + $em->persist($child); + $em->flush(); + $em->clear(); + unset($child); + + $connection = $em->getConnection(); + $stmtBase = $connection->prepare('SELECT * from classTableInheritanceBase WHERE id = ?'); + $stmtChild = $connection->prepare('SELECT * from classTableInheritanceChild WHERE id = ?'); + $childs = $em->getRepository(ClassTableInheritanceBase::class)->findAll(); + self::assertCount(1, $childs); + /** @var ClassTableInheritanceChild $child */ + $child = $childs[0]; + self::assertEquals($secretBase, $child->getSecretBase()); + self::assertEquals($notSecretBase, $child->getNotSecretBase()); + self::assertEquals($secretChild, $child->getSecretChild()); + self::assertEquals($notSecretChild, $child->getNotSecretChild()); + + // Now check that the fields are encrypted in the database. First the base table. + $stmtBase->bindValue(1, $child->getId()); + $results = $this->executeStatementFetchAll($stmtBase); + self::assertCount(1, $results); + $result = $results[0]; + self::assertEquals($notSecretBase, $result['notSecretBase']); + self::assertNotEquals($secretBase, $result['secretBase']); + self::assertStringEndsWith('', $result['secretBase']); + $decrypted = $this->encryptor->decrypt(str_replace('', '', $result['secretBase'])); + self::assertEquals($secretBase, $decrypted); + + // and then the child table. + $stmtChild->bindValue(1, $child->getId()); + $results = $this->executeStatementFetchAll($stmtChild); + self::assertCount(1, $results); + $result = $results[0]; + self::assertEquals($notSecretChild, $result['notSecretChild']); + self::assertNotEquals($secretChild, $result['secretChild']); + self::assertStringEndsWith('', $result['secretChild']); + $decrypted = $this->encryptor->decrypt(str_replace('', '', $result['secretChild'])); + self::assertEquals($secretChild, $decrypted); + } - /** - * @throws \Doctrine\DBAL\DBALException - * @throws \Doctrine\ORM\OptimisticLockException - */ public function testEncryptionDoesNotHappenWhenThereIsNoChange(): void { $secret = "It's a secret"; @@ -111,8 +161,7 @@ public function testEncryptionDoesNotHappenWhenThereIsNoChange(): void $connection = $em->getConnection(); $stmt = $connection->prepare('SELECT * from owner WHERE id = ?'); $stmt->bindValue(1, $owner1Id); - $stmt->execute(); - $results = $stmt->fetchAll(); + $results = $this->executeStatementFetchAll($stmt); $this->assertCount(1, $results); $result = $results[0]; $originalEncryption = $result['secret']; @@ -145,8 +194,7 @@ public function testEncryptionDoesNotHappenWhenThereIsNoChange(): void $this->assertCount(0, $stack->queries, "Unexpected queries:\n".var_export($stack->queries, true)); $stmt->bindValue(1, $owner1Id); - $stmt->execute(); - $results = $stmt->fetchAll(); + $results = $this->executeStatementFetchAll($stmt); $this->assertCount(1, $results); $result = $results[0]; $shouldBeTheSameAsBefore = $result['secret']; @@ -155,6 +203,81 @@ public function testEncryptionDoesNotHappenWhenThereIsNoChange(): void } + public function testEncryptionDoesNotHappenWhenThereIsNoChangeClassInheritance(): void + { + $secretBase = "It's a secret. On the base class."; + $notSecretBase = "You're all welcome to know this. On the base class."; + $secretChild = "It's a secret. On the child class."; + $notSecretChild = "You're all welcome to know this. On the child class."; + $em = $this->entityManager; + $child = new ClassTableInheritanceChild(); + $child->setSecretBase($secretBase); + $child->setNotSecretBase($notSecretBase); + $child->setSecretChild($secretChild); + $child->setNotSecretChild($notSecretChild); + $em->persist($child); + $em->flush(); + $em->clear(); + $childId = $child->getId(); + unset($child); + + + // test that it was encrypted correctly + $connection = $em->getConnection(); + $stmtBase = $connection->prepare('SELECT * from classTableInheritanceBase WHERE id = ?'); + $stmtBase->bindValue(1, $childId); + $result = $this->executeStatementFetch($stmtBase); + $originalEncryptionBase = $result['secretBase']; + self::assertStringEndsWith('', $originalEncryptionBase); // is encrypted + + // do the same for the child. + $connection = $em->getConnection(); + $stmtChild = $connection->prepare('SELECT * from classTableInheritanceChild WHERE id = ?'); + $stmtChild->bindValue(1, $childId); + $result = $this->executeStatementFetch($stmtChild); + $originalEncryptionChild = $result['secretChild']; + self::assertStringEndsWith('', $originalEncryptionChild); // is encrypted + + $childs = $em->getRepository(ClassTableInheritanceChild::class)->findAll(); + $child = $childs[0]; + self::assertEquals($secretBase, $child->getSecretBase()); + self::assertEquals($notSecretBase, $child->getNotSecretBase()); + self::assertEquals($secretChild, $child->getSecretChild()); + self::assertEquals($notSecretChild, $child->getNotSecretChild()); + + $stack = new DebugStack(); + $connection->getConfiguration()->setSQLLogger($stack); + self::assertCount(0, $stack->queries); + $beforeFlush = $this->subscriber->encryptCounter; + $em->flush(); + $afterFlush = $this->subscriber->encryptCounter; + // No encryption should have happened because we didn't change anything. + self::assertEquals($beforeFlush, $afterFlush); + // No queries happened because we didn't change anything. + self::assertCount(0, $stack->queries, "Unexpected queries:\n" . var_export($stack->queries, true)); + + // flush again + $beforeFlush = $this->subscriber->encryptCounter; + $em->flush(); + $afterFlush = $this->subscriber->encryptCounter; + // No encryption should have happened because we didn't change anything. + self::assertEquals($beforeFlush, $afterFlush); + // No queries happened because we didn't change anything. + self::assertCount(0, $stack->queries, "Unexpected queries:\n" . var_export($stack->queries, true)); + + $stmtBase->bindValue(1, $childId); + $result = $this->executeStatementFetch($stmtBase); + $shouldBeTheSameAsBeforeBase = $result['secretBase']; + self::assertStringEndsWith('', $shouldBeTheSameAsBeforeBase); // is encrypted + self::assertEquals($originalEncryptionBase, $shouldBeTheSameAsBeforeBase); + + $stmtChild->bindValue(1, $childId); + $result = $this->executeStatementFetch($stmtChild); + $shouldBeTheSameAsBeforeChild = $result['secretChild']; + self::assertStringEndsWith('', $shouldBeTheSameAsBeforeChild); // is encrypted + self::assertEquals($originalEncryptionChild, $shouldBeTheSameAsBeforeChild); + } + public function testEncryptionDoesHappenWhenASecretIsChanged(): void { $secret = "It's a secret"; @@ -173,8 +296,7 @@ public function testEncryptionDoesHappenWhenASecretIsChanged(): void $connection = $em->getConnection(); $stmt = $connection->prepare('SELECT * from owner WHERE id = ?'); $stmt->bindValue(1, $ownerId); - $stmt->execute(); - $results = $stmt->fetchAll(); + $results = $this->executeStatementFetchAll($stmt); $this->assertCount(1, $results); $result = $results[0]; $originalEncryption = $result['secret']; @@ -190,8 +312,7 @@ public function testEncryptionDoesHappenWhenASecretIsChanged(): void $this->assertGreaterThan($beforeFlush, $afterFlush); $stmt->bindValue(1, $ownerId); - $stmt->execute(); - $results = $stmt->fetchAll(); + $results = $this->executeStatementFetchAll($stmt); $this->assertCount(1, $results); $result = $results[0]; $shouldBeDifferentFromBefore = $result['secret']; diff --git a/Tests/Functional/DoctrineEncryptSubscriber/DoctrineEncryptSubscriberDefuseTestCase.php b/tests/Functional/DoctrineEncryptSubscriber/DoctrineEncryptSubscriberDefuseTest.php similarity index 65% rename from Tests/Functional/DoctrineEncryptSubscriber/DoctrineEncryptSubscriberDefuseTestCase.php rename to tests/Functional/DoctrineEncryptSubscriber/DoctrineEncryptSubscriberDefuseTest.php index 70cc47e7..346b8e3c 100644 --- a/Tests/Functional/DoctrineEncryptSubscriber/DoctrineEncryptSubscriberDefuseTestCase.php +++ b/tests/Functional/DoctrineEncryptSubscriber/DoctrineEncryptSubscriberDefuseTest.php @@ -4,9 +4,8 @@ use Ambta\DoctrineEncryptBundle\Encryptors\DefuseEncryptor; use Ambta\DoctrineEncryptBundle\Encryptors\EncryptorInterface; -use Ambta\DoctrineEncryptBundle\Tests\Functional\BasicQueryTest\AbstractBasicQueryTestCase; -class DoctrineEncryptSubscriberDefuseTestCase extends AbstractDoctrineEncryptSubscriberTestCase +class DoctrineEncryptSubscriberDefuseTest extends AbstractDoctrineEncryptSubscriberTestCase { protected function getEncryptor(): EncryptorInterface { diff --git a/Tests/Functional/DoctrineEncryptSubscriber/DoctrineEncryptSubscriberHaliteTestCase.php b/tests/Functional/DoctrineEncryptSubscriber/DoctrineEncryptSubscriberHaliteTest.php similarity index 76% rename from Tests/Functional/DoctrineEncryptSubscriber/DoctrineEncryptSubscriberHaliteTestCase.php rename to tests/Functional/DoctrineEncryptSubscriber/DoctrineEncryptSubscriberHaliteTest.php index 02fc8926..27db9ca6 100644 --- a/Tests/Functional/DoctrineEncryptSubscriber/DoctrineEncryptSubscriberHaliteTestCase.php +++ b/tests/Functional/DoctrineEncryptSubscriber/DoctrineEncryptSubscriberHaliteTest.php @@ -4,9 +4,8 @@ use Ambta\DoctrineEncryptBundle\Encryptors\EncryptorInterface; use Ambta\DoctrineEncryptBundle\Encryptors\HaliteEncryptor; -use Ambta\DoctrineEncryptBundle\Tests\Functional\BasicQueryTest\AbstractBasicQueryTestCase; -class DoctrineEncryptSubscriberHaliteTestCase extends AbstractDoctrineEncryptSubscriberTestCase +class DoctrineEncryptSubscriberHaliteTest extends AbstractDoctrineEncryptSubscriberTestCase { protected function getEncryptor(): EncryptorInterface { diff --git a/tests/Functional/fixtures/Entity/AbstractVehicle.php b/tests/Functional/fixtures/Entity/AbstractVehicle.php new file mode 100644 index 00000000..71d7db97 --- /dev/null +++ b/tests/Functional/fixtures/Entity/AbstractVehicle.php @@ -0,0 +1,76 @@ +id; + } + + /** + * @return mixed + */ + public function getSecret() + { + return $this->secret; + } + + /** + * @param mixed $secret + */ + public function setSecret($secret): void + { + $this->secret = $secret; + } + + /** + * @return mixed + */ + public function getNotSecret() + { + return $this->notSecret; + } + + /** + * @param mixed $notSecret + * @return $this + */ + public function setNotSecret($notSecret): self + { + $this->notSecret = $notSecret; + return $this; + } +} \ No newline at end of file diff --git a/Tests/Functional/fixtures/Entity/CascadeTarget.php b/tests/Functional/fixtures/Entity/CascadeTarget.php similarity index 100% rename from Tests/Functional/fixtures/Entity/CascadeTarget.php rename to tests/Functional/fixtures/Entity/CascadeTarget.php diff --git a/tests/Functional/fixtures/Entity/ClassTableInheritanceBase.php b/tests/Functional/fixtures/Entity/ClassTableInheritanceBase.php new file mode 100644 index 00000000..e5aa089d --- /dev/null +++ b/tests/Functional/fixtures/Entity/ClassTableInheritanceBase.php @@ -0,0 +1,66 @@ +id; + } + + public function getSecretBase() + { + return $this->secretBase; + } + + public function setSecretBase($secretBase) + { + $this->secretBase = $secretBase; + } + + /** + * @return mixed + */ + public function getNotSecretBase() + { + return $this->notSecretBase; + } + + /** + * @param mixed $notSecretBase + */ + public function setNotSecretBase($notSecretBase) + { + $this->notSecretBase = $notSecretBase; + } + +} \ No newline at end of file diff --git a/tests/Functional/fixtures/Entity/ClassTableInheritanceChild.php b/tests/Functional/fixtures/Entity/ClassTableInheritanceChild.php new file mode 100644 index 00000000..7842f72f --- /dev/null +++ b/tests/Functional/fixtures/Entity/ClassTableInheritanceChild.php @@ -0,0 +1,49 @@ +secretChild; + } + + public function setSecretChild($secretChild) + { + $this->secretChild = $secretChild; + } + + /** + * @return mixed + */ + public function getNotSecretChild() + { + return $this->notSecretChild; + } + + /** + * @param mixed $notSecretChild + */ + public function setNotSecretChild($notSecretChild) + { + $this->notSecretChild = $notSecretChild; + } + +} \ No newline at end of file diff --git a/Tests/Functional/fixtures/Entity/Owner.php b/tests/Functional/fixtures/Entity/Owner.php similarity index 99% rename from Tests/Functional/fixtures/Entity/Owner.php rename to tests/Functional/fixtures/Entity/Owner.php index b3c3b419..3b0faa9c 100644 --- a/Tests/Functional/fixtures/Entity/Owner.php +++ b/tests/Functional/fixtures/Entity/Owner.php @@ -39,8 +39,6 @@ class Owner */ private $cascaded; - - public function getId() { return $this->id; @@ -88,5 +86,4 @@ public function setCascaded($cascaded) $this->cascaded = $cascaded; } - } \ No newline at end of file diff --git a/tests/Functional/fixtures/Entity/VehicleBicycle.php b/tests/Functional/fixtures/Entity/VehicleBicycle.php new file mode 100644 index 00000000..5865614d --- /dev/null +++ b/tests/Functional/fixtures/Entity/VehicleBicycle.php @@ -0,0 +1,36 @@ +hasSidewheels; + } + + /** + * @param bool $hasSidewheels + * @return $this + */ + public function setSidewheels($hasSidewheels): self + { + $this->hasSidewheels = $hasSidewheels; + return $this; + } +} \ No newline at end of file diff --git a/tests/Functional/fixtures/Entity/VehicleCar.php b/tests/Functional/fixtures/Entity/VehicleCar.php new file mode 100644 index 00000000..1db05b26 --- /dev/null +++ b/tests/Functional/fixtures/Entity/VehicleCar.php @@ -0,0 +1,36 @@ +licensePlate; + } + + /** + * @param string $licensePlate + * @return $this + */ + public function setLicensePlate($licensePlate): self + { + $this->licensePlate = $licensePlate; + return $this; + } +} \ No newline at end of file diff --git a/Tests/Functional/fixtures/defuse.key b/tests/Functional/fixtures/defuse.key similarity index 100% rename from Tests/Functional/fixtures/defuse.key rename to tests/Functional/fixtures/defuse.key diff --git a/Tests/Functional/fixtures/halite.key b/tests/Functional/fixtures/halite.key similarity index 100% rename from Tests/Functional/fixtures/halite.key rename to tests/Functional/fixtures/halite.key diff --git a/Tests/Unit/DependencyInjection/DoctrineEncryptExtensionTest.php b/tests/Unit/DependencyInjection/DoctrineEncryptExtensionTest.php similarity index 98% rename from Tests/Unit/DependencyInjection/DoctrineEncryptExtensionTest.php rename to tests/Unit/DependencyInjection/DoctrineEncryptExtensionTest.php index 4aced3d8..bd4c1f33 100644 --- a/Tests/Unit/DependencyInjection/DoctrineEncryptExtensionTest.php +++ b/tests/Unit/DependencyInjection/DoctrineEncryptExtensionTest.php @@ -49,8 +49,6 @@ public function testConfigLoadCustom(): void ]; $this->extension->load([$config], $container); - $this->markTestSkipped(); - $this->assertSame(self::class, $container->getParameter('ambta_doctrine_encrypt.encryptor_class_name')); } diff --git a/Tests/Unit/Encryptors/DefuseEncryptorTest.php b/tests/Unit/Encryptors/DefuseEncryptorTest.php similarity index 100% rename from Tests/Unit/Encryptors/DefuseEncryptorTest.php rename to tests/Unit/Encryptors/DefuseEncryptorTest.php diff --git a/Tests/Unit/Encryptors/HaliteEncryptorTest.php b/tests/Unit/Encryptors/HaliteEncryptorTest.php similarity index 73% rename from Tests/Unit/Encryptors/HaliteEncryptorTest.php rename to tests/Unit/Encryptors/HaliteEncryptorTest.php index 794b5a2f..414d1a8f 100644 --- a/Tests/Unit/Encryptors/HaliteEncryptorTest.php +++ b/tests/Unit/Encryptors/HaliteEncryptorTest.php @@ -11,7 +11,7 @@ class HaliteEncryptorTest extends TestCase public function testEncryptExtension(): void { - if (! extension_loaded('sodium')) { + if (! extension_loaded('sodium') && !class_exists('ParagonIE_Sodium_Compat')) { $this->markTestSkipped('This test only runs when the sodium extension is enabled.'); } $keyfile = __DIR__.'/fixtures/halite.key'; @@ -29,7 +29,7 @@ public function testEncryptExtension(): void public function testGenerateKey(): void { - if (! extension_loaded('sodium')) { + if (! extension_loaded('sodium') && !class_exists('ParagonIE_Sodium_Compat')) { $this->markTestSkipped('This test only runs when the sodium extension is enabled.'); } $keyfile = sys_get_temp_dir().'/halite-'.md5(time()); @@ -44,18 +44,4 @@ public function testGenerateKey(): void unlink($keyfile); } - - - public function testEncryptWithoutExtensionThrowsException(): void - { - if (extension_loaded('sodium')) { - $this->markTestSkipped('This only runs when the sodium extension is disabled.'); - } - $keyfile = __DIR__.'/fixtures/halite.key'; - $halite = new HaliteEncryptor($keyfile); - - $this->expectException(\SodiumException::class); - $halite->encrypt(self::DATA); - } - } diff --git a/Tests/Unit/Encryptors/fixtures/defuse.key b/tests/Unit/Encryptors/fixtures/defuse.key similarity index 100% rename from Tests/Unit/Encryptors/fixtures/defuse.key rename to tests/Unit/Encryptors/fixtures/defuse.key diff --git a/Tests/Unit/Encryptors/fixtures/halite.key b/tests/Unit/Encryptors/fixtures/halite.key similarity index 100% rename from Tests/Unit/Encryptors/fixtures/halite.key rename to tests/Unit/Encryptors/fixtures/halite.key diff --git a/Tests/Unit/Subscribers/DoctrineEncryptSubscriberTest.php b/tests/Unit/Subscribers/DoctrineEncryptSubscriberTest.php similarity index 75% rename from Tests/Unit/Subscribers/DoctrineEncryptSubscriberTest.php rename to tests/Unit/Subscribers/DoctrineEncryptSubscriberTest.php index 0a407cc3..79269856 100644 --- a/Tests/Unit/Subscribers/DoctrineEncryptSubscriberTest.php +++ b/tests/Unit/Subscribers/DoctrineEncryptSubscriberTest.php @@ -17,6 +17,7 @@ use Doctrine\ORM\UnitOfWork; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; +use ReflectionClass; class DoctrineEncryptSubscriberTest extends TestCase { @@ -35,6 +36,22 @@ class DoctrineEncryptSubscriberTest extends TestCase */ private $reader; + /** @var EntityManagerInterface|MockObject */ + private $em; + + protected function createMock($originalClassName): MockObject + { + $oldErrorLevel = ini_get('error_reporting'); + ini_set('error_reporting',E_ALL ^ E_DEPRECATED); + + $return = parent::createMock($originalClassName); + + ini_set('error_reporting',$oldErrorLevel); + + return $return; + } + + protected function setUp(): void { $this->encryptor = $this->createMock(EncryptorInterface::class); @@ -67,6 +84,14 @@ protected function setUp(): void return false; }) ; + $this->em = $this->createMock(EntityManagerInterface::class); + $this->em->method('getClassMetadata') + ->willReturnCallback(function (string $className) { + $classMetaData = $this->createMock(ClassMetadata::class); + $classMetaData->rootEntityName = $className; + + return $classMetaData; + }); $this->subscriber = new DoctrineEncryptSubscriber($this->reader, $this->encryptor); } @@ -82,11 +107,19 @@ public function testSetRestorEncryptor(): void $this->assertSame($this->encryptor, $this->subscriber->getEncryptor()); } + protected function triggerProcessFields(Object $entity,bool $encrypt) + { + $class = new ReflectionClass(DoctrineEncryptSubscriber::class); + $method = $class->getMethod('processFields'); + $method->setAccessible(true); + $method->invokeArgs($this->subscriber,[$entity,$this->em,$encrypt]); + } + public function testProcessFieldsEncrypt(): void { $user = new User('David', 'Switzerland'); - $this->subscriber->processFields($user, true); + $this->triggerProcessFields($user,true); $this->assertStringStartsWith('encrypted-', $user->name); $this->assertStringStartsWith('encrypted-', $user->getAddress()); @@ -96,7 +129,7 @@ public function testProcessFieldsEncryptExtend(): void { $user = new ExtendedUser('David', 'Switzerland', 'extra'); - $this->subscriber->processFields($user, true); + $this->triggerProcessFields($user,true); $this->assertStringStartsWith('encrypted-', $user->name); $this->assertStringStartsWith('encrypted-', $user->getAddress()); @@ -105,9 +138,10 @@ public function testProcessFieldsEncryptExtend(): void public function testProcessFieldsEncryptEmbedded(): void { + $withUser = new WithUser('Thing', 'foo', new User('David', 'Switzerland')); - $this->subscriber->processFields($withUser, true); + $this->triggerProcessFields($withUser,true); $this->assertStringStartsWith('encrypted-', $withUser->name); $this->assertSame('foo', $withUser->foo); @@ -119,7 +153,7 @@ public function testProcessFieldsEncryptNull(): void { $user = new User('David', null); - $this->subscriber->processFields($user, true); + $this->triggerProcessFields($user,true); $this->assertStringStartsWith('encrypted-', $user->name); $this->assertNull($user->getAddress()); @@ -130,7 +164,7 @@ public function testProcessFieldsNoEncryptor(): void $user = new User('David', 'Switzerland'); $this->subscriber->setEncryptor(null); - $this->subscriber->processFields($user, true); + $this->triggerProcessFields($user,true); $this->assertSame('David', $user->name); $this->assertSame('Switzerland', $user->getAddress()); @@ -140,7 +174,7 @@ public function testProcessFieldsDecrypt(): void { $user = new User('encrypted-David', 'encrypted-Switzerland'); - $this->subscriber->processFields($user, false); + $this->triggerProcessFields($user,false); $this->assertSame('David', $user->name); $this->assertSame('Switzerland', $user->getAddress()); @@ -150,7 +184,7 @@ public function testProcessFieldsDecryptExtended(): void { $user = new ExtendedUser('encrypted-David', 'encrypted-Switzerland', 'encrypted-extra'); - $this->subscriber->processFields($user, false); + $this->triggerProcessFields($user,false); $this->assertSame('David', $user->name); $this->assertSame('Switzerland', $user->getAddress()); @@ -161,7 +195,7 @@ public function testProcessFieldsDecryptEmbedded(): void { $withUser = new WithUser('encrypted-Thing', 'foo', new User('encrypted-David', 'encrypted-Switzerland')); - $this->subscriber->processFields($withUser, false); + $this->triggerProcessFields($withUser,false); $this->assertSame('Thing', $withUser->name); $this->assertSame('foo', $withUser->foo); @@ -173,7 +207,7 @@ public function testProcessFieldsDecryptNull(): void { $user = new User('encrypted-David', null); - $this->subscriber->processFields($user, false); + $this->triggerProcessFields($user,false); $this->assertSame('David', $user->name); $this->assertNull($user->getAddress()); @@ -184,7 +218,7 @@ public function testProcessFieldsDecryptNonEncrypted(): void // no trailing but somethint that our mock decrypt would change if called $user = new User('encrypted-David', 'encrypted-Switzerland'); - $this->subscriber->processFields($user, false); + $this->triggerProcessFields($user,false); $this->assertSame('encrypted-David', $user->name); $this->assertSame('encrypted-Switzerland', $user->getAddress()); @@ -208,8 +242,15 @@ public function testOnFlush(): void ->willReturn($uow) ; $classMetaData = $this->createMock(ClassMetadata::class); - $em->expects($this->once())->method('getClassMetadata')->willReturn($classMetaData); - $uow->expects($this->once())->method('recomputeSingleEntityChangeSet'); + $classMetaData->rootEntityName = User::class; + $em->method('getClassMetadata') + ->willReturnCallback(function (string $className) { + $classMetaData = $this->createMock(ClassMetadata::class); + $classMetaData->rootEntityName = $className; + + return $classMetaData; + }); + $uow->expects($this->any())->method('recomputeSingleEntityChangeSet'); $onFlush = new OnFlushEventArgs($em); @@ -236,6 +277,16 @@ public function testPostFlush(): void ->method('getUnitOfWork') ->willReturn($uow) ; + $classMetaData = $this->createMock(ClassMetadata::class); + $classMetaData->rootEntityName = User::class; + $em->method('getClassMetadata') + ->willReturnCallback(function (string $className) { + $classMetaData = $this->createMock(ClassMetadata::class); + $classMetaData->rootEntityName = $className; + + return $classMetaData; + }); + $postFlush = new PostFlushEventArgs($em); $this->subscriber->postFlush($postFlush); diff --git a/Tests/Unit/Subscribers/fixtures/ExtendedUser.php b/tests/Unit/Subscribers/fixtures/ExtendedUser.php similarity index 100% rename from Tests/Unit/Subscribers/fixtures/ExtendedUser.php rename to tests/Unit/Subscribers/fixtures/ExtendedUser.php diff --git a/Tests/Unit/Subscribers/fixtures/User.php b/tests/Unit/Subscribers/fixtures/User.php similarity index 100% rename from Tests/Unit/Subscribers/fixtures/User.php rename to tests/Unit/Subscribers/fixtures/User.php diff --git a/Tests/Unit/Subscribers/fixtures/WithUser.php b/tests/Unit/Subscribers/fixtures/WithUser.php similarity index 100% rename from Tests/Unit/Subscribers/fixtures/WithUser.php rename to tests/Unit/Subscribers/fixtures/WithUser.php diff --git a/Tests/bootstrap.php b/tests/bootstrap.php similarity index 100% rename from Tests/bootstrap.php rename to tests/bootstrap.php