diff --git a/.travis.yml b/.travis.yml index 814b7bc1..6cdaefe9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,6 +4,11 @@ branches: only: - master +cache: + directories: + - vendor + - venv + before_install: ./.travis/before_install.php install: ./.travis/install.php script: ./.travis/script.php @@ -11,21 +16,20 @@ script: ./.travis/script.php matrix: include: # old PHP versions with latest Symfony version - - php: 5.3 + - php: 5.4 env: SYMFONY_VERSION=2.8.* # Symfony 3 doesn't support PHP 5.3 - php: 5.6 - env: SYMFONY_VERSION=3.1.* - # HHVM - - php: hhvm - env: SYMFONY_VERSION=3.1.* - # current PHP with all relevant Symfony versions + env: SYMFONY_VERSION=3.2.* - php: 7.0 + env: SYMFONY_VERSION=3.2.* + # current PHP with all relevant Symfony versions + - php: 7.1 env: SYMFONY_VERSION=2.3.* - - php: 7.0 + - php: 7.1 env: SYMFONY_VERSION=2.8.* - - php: 7.0 - env: SYMFONY_VERSION=3.1.* - - php: 7.0 + - php: 7.1 + env: SYMFONY_VERSION=3.3.* + - php: 7.1 env: SYMFONY_VERSION=dev-master allow_failures: - env: SYMFONY_VERSION=dev-master diff --git a/.travis/before_install.php b/.travis/before_install.php index b3479f12..4bc9986c 100755 --- a/.travis/before_install.php +++ b/.travis/before_install.php @@ -3,7 +3,5 @@ include_once 'common.php'; -if (!isHhvm()) { - // Disable XDebug - run('phpenv config-rm xdebug.ini'); -} +// Disable XDebug +run('phpenv config-rm xdebug.ini'); diff --git a/.travis/common.php b/.travis/common.php index 4cdbda96..2c6087db 100644 --- a/.travis/common.php +++ b/.travis/common.php @@ -5,19 +5,14 @@ function shouldBuildDocs() return isLatestPhp() && isLatestSymfony(); } -function isHhvm() -{ - return getPhpVersion() === 'hhvm'; -} - function isLatestPhp() { - return getPhpVersion() === '7.0'; + return getPhpVersion() === '7.1'; } function isLatestSymfony() { - return getSymfonyVersion() === '3.1.*'; + return getSymfonyVersion() === '3.3.*'; } function getSymfonyVersion() diff --git a/.travis/install.php b/.travis/install.php index 985ba6cf..54bc680c 100755 --- a/.travis/install.php +++ b/.travis/install.php @@ -3,8 +3,6 @@ include_once 'common.php'; -run('composer self-update'); - if (isLatestPhp() && isLatestSymfony()) { // Make sure composer.json references all necessary components by having one // job run a `composer update`. Since `composer update` will install the @@ -12,11 +10,6 @@ // latest symfony version. run('composer update --prefer-dist'); } else { - if (getPhpVersion() === '5.3') { - // Prevent Travis throwing an out of memory error - run('echo "memory_limit=-1" >> ~/.phpenv/versions/5.3/etc/conf.d/travis.ini'); - } - run('composer require --prefer-dist symfony/symfony:'.getSymfonyVersion()); } diff --git a/CHANGELOG.md b/CHANGELOG.md index edde1545..ed3db181 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,32 @@ All notable changes to this project are documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). +## [1.3.0] - 2017-01-22 +### Changed +- `JMS\Payment\CoreBundle\Model\ExtendedDataInterface` has changed. If any of your classes implement this interface, you need to update them accordingly: + - Added missing `mayBePersisted` method + - Added missing `$persist` parameter to `set` method +- `JMS\Payment\CoreBundle\EntityExtendedDataType::convertToDatabaseValue` now throws an exception when attempting to convert an object which does not implement `JMS\Payment\CoreBundle\Model\ExtendedDataInterface`. +- Encryption is now optional and disabled by default, unless the `secret` or `encryption.secret` configuration options are set. +- `defuse_php_encryption` is now the default encryption provider, unless when using the `secret` configuration option, in which case the default is set to `mcrypt`. +- The EntityManager is no longer closed when an Exception is thrown (#145) + +### Deprecated +- The service `payment.encryption_service` has been deprecated and is now an alias to `payment.encryption.mcrypt`. Parameters specified for `payment.encryption_service` are automatically set for `payment.encryption.mcrypt` so no changes are required in service configuration until `payment.encryption_service` is removed in 2.0. +- The `secret` configuration option has been deprecated in favor of `encryption.secret` and will be removed in 2.0. Please note that if you start using `encryption.secret` you also need to set `encryption.provider` to `mcrypt` since mcrypt is not the default when using the `encryption.*` options. +- `JMS\Payment\CoreBundle\Cryptography\MCryptEncryptionService` has been deprecated and will be removed in 2.0 (`mcrypt` has been deprecated in PHP 7.1 and is removed in PHP 7.2). Refer to http://jmspaymentcorebundle.readthedocs.io/en/stable/guides/mcrypt.html for instructions on how to migrate away from `mcrypt`. + +### Added +- Added ``method_options`` and ``choice_options`` to form. See the [documentation](http://jmspaymentcorebundle.readthedocs.io/en/stable/payment_form.html#choice-options) for more information. +- Added a guides section to the documentation +- Added support for custom encryption providers. +- Added support for data encryption with [defuse/php-encryption](https://github.com/defuse/php-encryption). +- Added console command to generate an encryption key for use with `defuse_php_encryption`. +- Added ability to configure which encryption provider should be used. Current available options are `mcrypt` (not recommended since it will be removed in PHP 7.2) and `defuse_php_encryption`. + +### Removed +- Removed support for PHP 5.3. If you're still using PHP 5.3, please consider upgrading since it reached End Of Life in August 2014. Otherwise, use `1.2.*`. + ## [1.2.0] - 2016-10-03 ### Added - Added support for Symfony 3.0. Note that Symfony 3.0 introduces BC breaks. This means that you'll probably need to do more than simply updating to version `1.2.0` of this bundle for your code to keep working under Symfony 3.0. Please see Symfony's [Upgrade Guide](https://github.com/symfony/symfony/blob/master/UPGRADE-3.0.md) for information on what you need to change. diff --git a/Command/GenerateKeyCommand.php b/Command/GenerateKeyCommand.php new file mode 100644 index 00000000..a3ba623d --- /dev/null +++ b/Command/GenerateKeyCommand.php @@ -0,0 +1,24 @@ +setName('jms_payment_core:generate-key') + ->setDescription('Generate an encryption key') + ; + } + + protected function execute(InputInterface $input, OutputInterface $output) + { + $output->writeln(Key::createNewRandomKey()->saveToAsciiSafeString()); + } +} diff --git a/Cryptography/DefusePhpEncryptionService.php b/Cryptography/DefusePhpEncryptionService.php new file mode 100644 index 00000000..2ffe7597 --- /dev/null +++ b/Cryptography/DefusePhpEncryptionService.php @@ -0,0 +1,26 @@ +key = Key::loadFromAsciiSafeString($secret); + } + + public function decrypt($encryptedValue) + { + return Crypto::decrypt($encryptedValue, $this->key); + } + + public function encrypt($rawValue) + { + return Crypto::encrypt($rawValue, $this->key); + } +} diff --git a/Cryptography/MCryptEncryptionService.php b/Cryptography/MCryptEncryptionService.php index e8a0ffb5..54d8e48b 100644 --- a/Cryptography/MCryptEncryptionService.php +++ b/Cryptography/MCryptEncryptionService.php @@ -46,11 +46,13 @@ public function __construct($secret, $cipher = 'rijndael-256', $mode = 'ctr') throw new \RuntimeException('The mcrypt extension must be loaded.'); } - if (!in_array($cipher, mcrypt_list_algorithms(), true)) { + @trigger_error('mcrypt has been deprecated in PHP 7.1 and is removed in PHP 7.2. Refer to http://jmspaymentcorebundle.readthedocs.io/en/stable/guides/mcrypt.html for instructions on how to migrate away from mcrypt', E_USER_DEPRECATED); + + if (!in_array($cipher, @mcrypt_list_algorithms(), true)) { throw new \InvalidArgumentException(sprintf('The cipher "%s" is not supported.', $cipher)); } - if (!in_array($mode, mcrypt_list_modes(), true)) { + if (!in_array($mode, @mcrypt_list_modes(), true)) { throw new \InvalidArgumentException(sprintf('The mode "%s" is not supported.', $mode)); } @@ -62,7 +64,7 @@ public function __construct($secret, $cipher = 'rijndael-256', $mode = 'ctr') } $key = hash('sha256', $secret, true); - if (strlen($key) > $size = mcrypt_get_key_size($this->cipher, $this->mode)) { + if (strlen($key) > $size = @mcrypt_get_key_size($this->cipher, $this->mode)) { $key = substr($key, 0, $size); } $this->key = $key; diff --git a/DependencyInjection/Compiler/AddPaymentMethodFormTypesPass.php b/DependencyInjection/Compiler/AddPaymentMethodFormTypesPass.php index 35c58e66..709ecf4f 100755 --- a/DependencyInjection/Compiler/AddPaymentMethodFormTypesPass.php +++ b/DependencyInjection/Compiler/AddPaymentMethodFormTypesPass.php @@ -20,22 +20,22 @@ public function process(ContainerBuilder $container) } $paymentMethodFormTypes = array(); - foreach ($container->findTaggedServiceIds('payment.method_form_type') as $id => $attributes) { + foreach ($container->findTaggedServiceIds('payment.method_form_type') as $id => $attrs) { $definition = $container->getDefinition($id); // check that this definition is also registered as a form type - $attributes = $definition->getTag('form.type'); - if (!$attributes) { + $attrs = $definition->getTag('form.type'); + if (!$attrs) { throw new \RuntimeException(sprintf('The service "%s" is marked as payment method form type (tagged with "payment.method_form_type"), but is not registered as a form type with the Form Component. Please also add a "form.type" tag.', $id)); } - if (!isset($attributes[0]['alias'])) { + if (!isset($attrs[0]['alias'])) { throw new \RuntimeException(sprintf('Please define an alias attribute for tag "form.type" of service "%s".', $id)); } - $paymentMethodFormTypes[$attributes[0]['alias']] = Legacy::supportsFormTypeName() - ? $attributes[0]['alias'] - : $definition->getClass() + $paymentMethodFormTypes[$attrs[0]['alias']] = Legacy::supportsFormTypeClass() + ? $definition->getClass() + : $attrs[0]['alias'] ; } diff --git a/DependencyInjection/Compiler/AddPaymentPluginsPass.php b/DependencyInjection/Compiler/AddPaymentPluginsPass.php index 19c574a3..b08db293 100644 --- a/DependencyInjection/Compiler/AddPaymentPluginsPass.php +++ b/DependencyInjection/Compiler/AddPaymentPluginsPass.php @@ -31,7 +31,7 @@ public function process(ContainerBuilder $container) } $def = $container->findDefinition('payment.plugin_controller'); - foreach ($container->findTaggedServiceIds('payment.plugin') as $id => $attr) { + foreach ($container->findTaggedServiceIds('payment.plugin') as $id => $attrs) { $def->addMethodCall('addPlugin', array(new Reference($id))); } } diff --git a/DependencyInjection/Compiler/ConfigureEncryptionPass.php b/DependencyInjection/Compiler/ConfigureEncryptionPass.php new file mode 100644 index 00000000..6cc6c912 --- /dev/null +++ b/DependencyInjection/Compiler/ConfigureEncryptionPass.php @@ -0,0 +1,34 @@ +getParameter('payment.encryption.enabled')) { + return; + } + + $providers = array(); + + foreach ($container->findTaggedServiceIds('payment.encryption') as $id => $attrs) { + if (!isset($attrs[0]['alias'])) { + throw new \RuntimeException("Please define an alias attribute for tag 'payment.encryption' of service '$id'"); + } + + $providers[$attrs[0]['alias']] = $id; + } + + $configuredProvider = $container->getParameter('payment.encryption'); + + if (!array_key_exists($configuredProvider, $providers)) { + throw new \RuntimeException("The configured encryption provider ($configuredProvider) must match the alias of one of the services tagged with 'payment.encryption'"); + } + + $container->setAlias('payment.encryption', $providers[$configuredProvider]); + } +} diff --git a/DependencyInjection/Compiler/LegacyEncryptionPass.php b/DependencyInjection/Compiler/LegacyEncryptionPass.php new file mode 100644 index 00000000..7a0f748e --- /dev/null +++ b/DependencyInjection/Compiler/LegacyEncryptionPass.php @@ -0,0 +1,56 @@ +has('payment.encryption_service')) { + return; + } + + if (!$container->has('payment.encryption.mcrypt')) { + return; + } + + $parameters = array( + 'class' => 'JMS\Payment\CoreBundle\Cryptography\MCryptEncryptionService', + 'secret' => '', + 'cipher' => 'rijndael-256', + 'mode' => 'ctr', + ); + + foreach ($parameters as $parameter => $defaultValue) { + if (!$container->hasParameter('payment.encryption_service.'.$parameter)) { + continue; + } + + if (!$container->hasParameter('payment.encryption.mcrypt.'.$parameter)) { + continue; + } + + $legacyValue = $container->getParameter('payment.encryption_service.'.$parameter); + $modernValue = $container->getParameter('payment.encryption.mcrypt.'.$parameter); + + // Parameters set for payment.encryption.mcrypt take precedence over + // ones set for payment.encryption_service + if ($modernValue !== $defaultValue) { + $container->setParameter('payment.encryption.mcrypt.'.$parameter, $modernValue); + } elseif ($legacyValue !== $defaultValue) { + $container->setParameter('payment.encryption.mcrypt.'.$parameter, $legacyValue); + @trigger_error('payment.encryption_service.'.$parameter.' has been deprecated in favor of payment.encryption.mcrypt.'.$parameter.' and will be removed in 2.0', E_USER_DEPRECATED); + } + } + } +} diff --git a/DependencyInjection/Configuration.php b/DependencyInjection/Configuration.php index aadeb886..9d731d29 100644 --- a/DependencyInjection/Configuration.php +++ b/DependencyInjection/Configuration.php @@ -3,6 +3,7 @@ namespace JMS\Payment\CoreBundle\DependencyInjection; use Symfony\Component\Config\Definition\Builder\TreeBuilder; +use Symfony\Component\Config\Definition\ConfigurationInterface; /* * Copyright 2010 Johannes M. Schmitt @@ -20,18 +21,63 @@ * limitations under the License. */ -class Configuration +class Configuration implements ConfigurationInterface { - public function getConfigTree() + private $alias; + + public function __construct($alias) + { + $this->alias = $alias; + } + + public function getConfigTreeBuilder() { - $tb = new TreeBuilder(); + $builder = new TreeBuilder(); - return $tb - ->root('jms_payment_core', 'array') - ->children() - ->scalarNode('secret')->isRequired()->cannotBeEmpty()->end() + $builder->root($this->alias, 'array') + ->children() + ->arrayNode('encryption') + ->canBeEnabled() + ->addDefaultsIfNotSet() + ->children() + ->scalarNode('provider')->defaultValue('defuse_php_encryption')->end() + ->scalarNode('secret')->cannotBeEmpty()->end() + ->end() + ->validate() + ->ifTrue(function ($config) { + return $config['enabled'] && !array_key_exists('secret', $config); + }) + ->thenInvalid('An encryption secret is required') + ->end() + ->end() + ->scalarNode('secret') + ->cannotBeEmpty() + ->info($this->getSecretDeprecationMessage()) ->end() ->end() - ->buildTree(); + ->beforeNormalization() + ->ifTrue(function ($config) { + return !empty($config['secret']); + }) + ->then(function ($config) { + @trigger_error($this->getSecretDeprecationMessage(), E_USER_DEPRECATED); + + $config['encryption'] = array( + 'enabled' => true, + 'provider' => 'mcrypt', + 'secret' => $config['secret'], + ); + + return $config; + }) + ->end() + ; + + return $builder; + } + + private function getSecretDeprecationMessage() + { + return "The 'secret' configuration option has been deprecated in favor of 'encryption.secret' and will be removed in 2.0. Please note that if you start using 'encryption.secret' you also need to set 'encryption.provider' to 'mcrypt' since mcrypt is not the default when using the 'encryption.*' options."; } } diff --git a/DependencyInjection/JMSPaymentCoreExtension.php b/DependencyInjection/JMSPaymentCoreExtension.php index 17990f29..167e9bbc 100644 --- a/DependencyInjection/JMSPaymentCoreExtension.php +++ b/DependencyInjection/JMSPaymentCoreExtension.php @@ -3,13 +3,11 @@ namespace JMS\Payment\CoreBundle\DependencyInjection; use JMS\Payment\CoreBundle\Entity\ExtendedDataType; -use Symfony\Component\Config\Definition\Processor; use Symfony\Component\Config\FileLocator; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface; use Symfony\Component\DependencyInjection\Loader\XmlFileLoader; use Symfony\Component\HttpKernel\DependencyInjection\Extension; -use Symfony\Component\HttpKernel\Kernel; /* * Copyright 2010 Johannes M. Schmitt @@ -45,16 +43,30 @@ public function load(array $configs, ContainerBuilder $container) $xmlLoader = new XmlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config')); $xmlLoader->load('payment.xml'); - $configuration = new Configuration(); - $processor = new Processor(); - $config = $processor->process($configuration->getConfigTree(), $configs); + $config = $this->processConfiguration( + $this->getConfiguration($configs, $container), + $configs + ); - if (isset($config['secret'])) { - $container->setParameter('payment.encryption_service.secret', $config['secret']); - } + $container->setParameter('payment.encryption.enabled', $config['encryption']['enabled']); + + if ($config['encryption']['enabled']) { + $container->setParameter('payment.encryption', $config['encryption']['provider']); + $container->setParameter('payment.encryption.secret', $config['encryption']['secret']); - if (version_compare(Kernel::VERSION, '2.1.0-DEV', '<')) { - $container->removeDefinition('payment.form.choose_payment_method_type'); + foreach (array('mcrypt', 'defuse_php_encryption') as $provider) { + $container->setParameter("payment.encryption.$provider.secret", $config['encryption']['secret']); + } + } else { + $container->removeAlias('payment.encryption_service'); + $container->removeDefinition('payment.encryption'); + $container->removeDefinition('payment.encryption.mcrypt'); + $container->removeDefinition('payment.encryption.defuse_php_encryption'); } } + + public function getConfiguration(array $config, ContainerBuilder $container) + { + return new Configuration($this->getAlias()); + } } diff --git a/Entity/ExtendedDataType.php b/Entity/ExtendedDataType.php index 6049f441..e058ab9e 100644 --- a/Entity/ExtendedDataType.php +++ b/Entity/ExtendedDataType.php @@ -6,6 +6,7 @@ use Doctrine\DBAL\Types\ConversionException; use Doctrine\DBAL\Types\ObjectType; use JMS\Payment\CoreBundle\Cryptography\EncryptionServiceInterface; +use JMS\Payment\CoreBundle\Model\ExtendedDataInterface; /* * Copyright 2010 Johannes M. Schmitt @@ -41,23 +42,35 @@ public static function getEncryptionService() public function convertToDatabaseValue($extendedData, AbstractPlatform $platform) { - if (null === $extendedData) { + if ($extendedData === null) { return null; } - $reflection = new \ReflectionProperty($extendedData, 'data'); - $reflection->setAccessible(true); - $data = $reflection->getValue($extendedData); - $reflection->setAccessible(false); + if (!$extendedData instanceof ExtendedDataInterface) { + throw new \InvalidArgumentException( + '$extendedData must implement JMS\Payment\CoreBundle\Model\ExtendedDataInterface' + ); + } - foreach ($data as $name => $value) { - if (false === $value[2]) { - unset($data[$name]); + $data = array(); + + foreach (array_keys($extendedData->all()) as $name) { + if (!$extendedData->mayBePersisted($name)) { continue; } - if (true === $value[1]) { - $data[$name][0] = self::$encryptionService->encrypt(serialize($value[0])); + + $value = $extendedData->get($name); + $isEncryptionRequired = $extendedData->isEncryptionRequired($name); + + if ($isEncryptionRequired && self::$encryptionService) { + $value = self::$encryptionService->encrypt(serialize($value)); } + + $data[$name] = array( + $value, + $isEncryptionRequired, + $mayBePersisted = true, + ); } return parent::convertToDatabaseValue($data, $platform); @@ -67,25 +80,28 @@ public function convertToPHPValue($value, AbstractPlatform $platform) { $data = parent::convertToPHPValue($value, $platform); - if (null === $data) { + if ($data === null) { return null; - } elseif (is_array($data)) { - foreach ($data as $name => $value) { - if (true === $value[1]) { - $data[$name][0] = unserialize(self::$encryptionService->decrypt($value[0])); - } - } - - $extendedData = new ExtendedData(); - $reflection = new \ReflectionProperty($extendedData, 'data'); - $reflection->setAccessible(true); - $reflection->setValue($extendedData, $data); - $reflection->setAccessible(false); + } - return $extendedData; - } else { + if (!is_array($data)) { throw ConversionException::conversionFailed($value, $this->getName()); } + + $extendedData = new ExtendedData(); + + foreach ($data as $name => $value) { + $isEncryptionRequired = (bool) $value[1]; + $value = $value[0]; + + if ($isEncryptionRequired && self::$encryptionService) { + $value = unserialize(self::$encryptionService->decrypt($value)); + } + + $extendedData->set($name, $value, $isEncryptionRequired); + } + + return $extendedData; } public function getName() diff --git a/Form/ChoosePaymentMethodType.php b/Form/ChoosePaymentMethodType.php index f0439064..294a2788 100755 --- a/Form/ChoosePaymentMethodType.php +++ b/Form/ChoosePaymentMethodType.php @@ -92,15 +92,18 @@ protected function buildChoiceList(FormBuilderInterface $builder, array $options $label = 'form.label.'.$method; if (Legacy::formChoicesAsValues()) { - $options['choices'][$method] = $label; - } else { $options['choices'][$label] = $method; + if (Legacy::needsChoicesAsValuesOption()) { + $options['choices_as_values'] = true; + } + } else { + $options['choices'][$method] = $label; } } - $type = Legacy::supportsFormTypeName() - ? 'choice' - : 'Symfony\Component\Form\Extension\Core\Type\ChoiceType' + $type = Legacy::supportsFormTypeClass() + ? 'Symfony\Component\Form\Extension\Core\Type\ChoiceType' + : 'choice' ; $builder->add('method', $type, $options); @@ -161,11 +164,11 @@ public function configureOptions(OptionsResolver $resolver) 'choice_options' => 'array', ); - if (Legacy::supportsFormTypeConfigureOptions()) { + if (Legacy::supportsOptionsResolverSetAllowedTypesAsArray()) { $resolver->setAllowedTypes($allowedTypes); } else { foreach ($allowedTypes as $key => $value) { - $resolver->addAllowedTypes($key, $value); + $resolver->setAllowedTypes($key, $value); } } } diff --git a/JMSPaymentCoreBundle.php b/JMSPaymentCoreBundle.php index a077fb5f..341643f0 100644 --- a/JMSPaymentCoreBundle.php +++ b/JMSPaymentCoreBundle.php @@ -4,6 +4,8 @@ use JMS\Payment\CoreBundle\DependencyInjection\Compiler\AddPaymentMethodFormTypesPass; use JMS\Payment\CoreBundle\DependencyInjection\Compiler\AddPaymentPluginsPass; +use JMS\Payment\CoreBundle\DependencyInjection\Compiler\ConfigureEncryptionPass; +use JMS\Payment\CoreBundle\DependencyInjection\Compiler\LegacyEncryptionPass; use JMS\Payment\CoreBundle\Entity\ExtendedDataType; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\HttpKernel\Bundle\Bundle; @@ -28,7 +30,9 @@ class JMSPaymentCoreBundle extends Bundle { public function boot() { - ExtendedDataType::setEncryptionService($this->container->get('payment.encryption_service')); + if ($this->container->has('payment.encryption')) { + ExtendedDataType::setEncryptionService($this->container->get('payment.encryption')); + } } public function build(ContainerBuilder $builder) @@ -37,5 +41,7 @@ public function build(ContainerBuilder $builder) $builder->addCompilerPass(new AddPaymentPluginsPass()); $builder->addCompilerPass(new AddPaymentMethodFormTypesPass()); + $builder->addCompilerPass(new LegacyEncryptionPass()); + $builder->addCompilerPass(new ConfigureEncryptionPass()); } } diff --git a/Model/ExtendedDataInterface.php b/Model/ExtendedDataInterface.php index 8c8a88c4..88658257 100644 --- a/Model/ExtendedDataInterface.php +++ b/Model/ExtendedDataInterface.php @@ -21,8 +21,9 @@ interface ExtendedDataInterface { public function isEncryptionRequired($name); + public function mayBePersisted($name); public function remove($name); - public function set($name, $value, $encrypt = true); + public function set($name, $value, $encrypt = true, $persist = true); public function get($name); public function has($name); public function all(); diff --git a/README.md b/README.md index e44b8fb4..6f16ea36 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ Features: - Persistence of financial entities (such as payments, transactions, etc.) - Transaction management including retry logic - Encryption of sensitive data -- Supports [many](http://jmspaymentcorebundle.readthedocs.io/en/latest/backends.html) payment backends out of the box +- Supports [many](http://jmspaymentcorebundle.readthedocs.io/en/stable/backends.html) payment backends out of the box - Easily support other payment backends # Documentation diff --git a/Resources/config/payment.xml b/Resources/config/payment.xml index 4085213a..7b01e7cd 100644 --- a/Resources/config/payment.xml +++ b/Resources/config/payment.xml @@ -15,10 +15,20 @@ JMS\Payment\CoreBundle\Form\ChoosePaymentMethodType JMS\Payment\CoreBundle\Form\Transformer\ChoosePaymentMethodTransformer + JMS\Payment\CoreBundle\Cryptography\MCryptEncryptionService + + rijndael-256 + ctr + + JMS\Payment\CoreBundle\Cryptography\DefusePhpEncryptionService + + + JMS\Payment\CoreBundle\Cryptography\MCryptEncryptionService rijndael-256 ctr + @@ -38,10 +48,19 @@ - - %payment.encryption_service.secret% - %payment.encryption_service.cipher% - %payment.encryption_service.mode% + + + + + %payment.encryption.mcrypt.secret% + %payment.encryption.mcrypt.cipher% + %payment.encryption.mcrypt.mode% + + + + + %payment.encryption.defuse_php_encryption.secret% + diff --git a/Resources/doc/backends.rst b/Resources/doc/backends.rst index c8c69393..297a41c2 100755 --- a/Resources/doc/backends.rst +++ b/Resources/doc/backends.rst @@ -9,7 +9,7 @@ This is the list of currently supported payment backends, through community-crea If you have implemented a payment backend, please add it to this list by `editing this file on GitHub `_ and submitting a Pull Request. - `Adyen `_ -- `Atos SIPS `_ +- `Atos SIPS `_ - `Be2Bill (Rentabiliweb) `_ - `Dotpay `_ - `Ogone `_ diff --git a/Resources/doc/guides/mcrypt.rst b/Resources/doc/guides/mcrypt.rst new file mode 100644 index 00000000..92c310f1 --- /dev/null +++ b/Resources/doc/guides/mcrypt.rst @@ -0,0 +1,4 @@ +Migrating from Mcrypt +===================== + +Coming soon diff --git a/Resources/doc/setup.rst b/Resources/doc/setup.rst index 037ec03d..e09d2b0d 100644 --- a/Resources/doc/setup.rst +++ b/Resources/doc/setup.rst @@ -25,38 +25,28 @@ And register the bundle in your ``AppKernel.php``: Configuration ------------- -The configuration is as simple as setting a random secret which will be used for encrypting data, in case this is requested. - -You can generate the secret with the following command: +The configuration is as simple as setting an encryption key which will be used for encrypting data. You can generate a random key with the following command: .. code-block :: bash - # Feel free to increase the length of the generated string by - # passing a larger number to openssl_random_pseudo_bytes - php -r 'echo bin2hex(openssl_random_pseudo_bytes(16))."\n";' + bin/console jms_payment_core:generate-key And then use it in your configuration: -.. configuration-block :: - - .. code-block :: yaml - - # app/config/config.yml - jms_payment_core: - secret: yoursecret - - .. code-block :: xml +.. code-block :: yaml - - + # app/config/config.yml + jms_payment_core: + encryption: + secret: output_of_above_command -.. note :: +.. warning :: - If you change the secret, all data encrypted with the old secret will become unreadable. + If you change the ``secret`` or the ``crypto`` provider, all encrypted data will become unreadable. Create database tables ---------------------- -This bundle requires a few database tables to function. You can create these tables as follows. +This bundle requires a few database tables, which you can create as follows. If you're not using database migrations: @@ -71,14 +61,13 @@ Or, if you're using migrations: bin/console doctrine:migrations:diff bin/console doctrine:migrations:migrate -.. warning :: +.. note :: It's assumed you have entity auto mapping enabled, which is usually the case. If you don't, you need to either enable it: .. code-block :: yaml # app/config/config.yml - doctrine: orm: auto_mapping: true @@ -88,7 +77,6 @@ Or, if you're using migrations: .. code-block :: yaml # app/config/config.yml - doctrine: orm: mappings: diff --git a/Tests/Cryptography/DefusePhpEncryptionServiceTest.php b/Tests/Cryptography/DefusePhpEncryptionServiceTest.php new file mode 100644 index 00000000..45bfed6b --- /dev/null +++ b/Tests/Cryptography/DefusePhpEncryptionServiceTest.php @@ -0,0 +1,48 @@ +assertNotEquals($data, $service1->encrypt($data)); + $this->assertNotEquals($data, $service2->encrypt($data)); + + $this->assertEquals($data, $service1->decrypt($service1->encrypt($data))); + $this->assertEquals($data, $service2->decrypt($service2->encrypt($data))); + $this->assertEquals($data, $service3->decrypt($service1->encrypt($data))); + } + + /** + * @dataProvider getTestData + * @expectedException \Defuse\Crypto\Exception\WrongKeyOrModifiedCiphertextException + */ + public function testEncryptDecryptFailure($data) + { + $service1 = new DefusePhpEncryptionService('def00000290a4b250a1b24c41f3076b5e3955e1a51d8535a5dbcf209d17f1eb8d772349cbd12af5dc8f4b05d43ca900489c0fb5aa5c4c5190ccffb5663ae4831e3022fc6'); + $service2 = new DefusePhpEncryptionService('def00000000bdbf82c58d9fbd77d15fa5314bdafebe7586e03b0679ef09f622577afe58485b2a4b3c2e74a16a7375ad348f29e9254a57237691fdf2d71b1d78cd3958497'); + + $this->assertNotEquals($data, $service1->decrypt($service2->encrypt($data))); + } + + public function getTestData() + { + return array( + array('this is some test data, very sensitive stuff'), + array('12345674234'), + array('123'), + array('4565-3346-2124-5653'), + array('HDarfg$§fasHaha&$%§'), + ); + } +} diff --git a/Tests/Cryptography/MCryptEncryptionServiceTest.php b/Tests/Cryptography/MCryptEncryptionServiceTest.php index 432d4c3b..9c9cbe6e 100644 --- a/Tests/Cryptography/MCryptEncryptionServiceTest.php +++ b/Tests/Cryptography/MCryptEncryptionServiceTest.php @@ -8,6 +8,10 @@ class MCryptEncryptionServiceTest extends \PHPUnit_Framework_TestCase { protected function setUp() { + if (version_compare(phpversion(), '7.1', '>=')) { + $this->markTestSkipped('mcrypt is deprecated since PHP 7.1'); + } + if (false !== strpos(PHP_OS, 'WIN')) { $this->markTestSkipped('Windows is not suited for generating random data.'); } diff --git a/Tests/DependencyInjection/Configuration/ConfigurationTest.php b/Tests/DependencyInjection/Configuration/ConfigurationTest.php new file mode 100644 index 00000000..99c6440b --- /dev/null +++ b/Tests/DependencyInjection/Configuration/ConfigurationTest.php @@ -0,0 +1,138 @@ +assertConfigurationIsValid(array()); + $this->assertConfigurationIsInvalid(array('secret' => '')); + + $this->assertConfigurationEquals( + array(), + array('encryption' => array( + 'enabled' => false, + 'provider' => 'defuse_php_encryption', + )) + ); + } + + public function testSecret() + { + $this->assertConfigurationIsValid(array('secret' => 'foo')); + + $this->assertConfigurationEquals( + array('secret' => 'foo'), + array( + 'secret' => 'foo', + 'encryption' => array( + 'enabled' => true, + 'secret' => 'foo', + 'provider' => 'mcrypt', + ), + ) + ); + } + + public function testEncryptionDisabled() + { + $this->assertConfigurationIsValid(array()); + $this->assertConfigurationIsValid(array('encryption' => false)); + + $this->assertConfigurationEquals( + array(), + array('encryption' => array( + 'enabled' => false, + 'provider' => 'defuse_php_encryption', + )) + ); + + $this->assertConfigurationEquals( + array('encryption' => false), + array('encryption' => array( + 'enabled' => false, + 'provider' => 'defuse_php_encryption', + )) + ); + + $this->assertConfigurationEquals( + array('encryption' => array( + 'enabled' => false, + )), + array('encryption' => array( + 'enabled' => false, + 'provider' => 'defuse_php_encryption', + )) + ); + } + + public function testEncryptionEnabled() + { + $this->assertConfigurationIsInvalid(array('encryption' => true)); + + $this->assertConfigurationIsInvalid(array('encryption' => array( + 'enabled' => true, + ))); + + $this->assertConfigurationIsValid(array('encryption' => array( + 'enabled' => true, + 'secret' => 'foo', + ))); + + $this->assertConfigurationIsValid(array('encryption' => array( + 'secret' => 'foo', + ))); + + $this->assertConfigurationEquals( + array('encryption' => array( + 'secret' => 'foo', + )), + array('encryption' => array( + 'enabled' => true, + 'secret' => 'foo', + 'provider' => 'defuse_php_encryption', + )) + ); + + $this->assertConfigurationEquals( + array('encryption' => array( + 'enabled' => true, + 'secret' => 'foo', + )), + array('encryption' => array( + 'enabled' => true, + 'secret' => 'foo', + 'provider' => 'defuse_php_encryption', + )) + ); + } + + protected function getConfiguration() + { + return new Configuration('jms_payment_core'); + } + + protected function assertConfigurationIsInvalid(array $config, $expected = null, $useRegExp = false) + { + parent::assertConfigurationIsInvalid(array($config), $expected, $useRegExp); + } + + protected function assertConfigurationIsValid(array $config, $breadcrumbPath = null) + { + parent::assertConfigurationIsValid(array($config), $breadcrumbPath); + } + + protected function assertConfigurationEquals($config, $expected, $breadcrumbPath = null) + { + $this->assertProcessedConfigurationEquals($config, $expected, $breadcrumbPath); + } + + protected function assertProcessedConfigurationEquals(array $config, array $expected, $breadcrumbPath = null) + { + parent::assertProcessedConfigurationEquals(array($config), $expected, $breadcrumbPath); + } +} diff --git a/Tests/Entity/ExtendedDataTypeTest.php b/Tests/Entity/ExtendedDataTypeTest.php index 41807c2e..a4bf4abd 100644 --- a/Tests/Entity/ExtendedDataTypeTest.php +++ b/Tests/Entity/ExtendedDataTypeTest.php @@ -3,7 +3,7 @@ namespace JMS\Payment\CoreBundle\Tests\Entity; use Doctrine\DBAL\Types\Type; -use JMS\Payment\CoreBundle\Cryptography\MCryptEncryptionService; +use JMS\Payment\CoreBundle\Cryptography\DefusePhpEncryptionService; use JMS\Payment\CoreBundle\Entity\ExtendedData; use JMS\Payment\CoreBundle\Entity\ExtendedDataType; @@ -20,7 +20,7 @@ protected function setUp() public function testStaticSetGetEncryptionService() { - $service = new MCryptEncryptionService('foo'); + $service = new DefusePhpEncryptionService('def00000812bba10524777c97f1155877f0c91a4fac2c9f3d71a39d0df7214eab90d492faa58d2db667c5003c3c2228e3f19ad493ae86c74079a600a1ed51cd65e21f28e'); $this->assertNull(ExtendedDataType::getEncryptionService()); ExtendedDataType::setEncryptionService($service); @@ -37,7 +37,7 @@ public function testGetName() public function testConversion() { - ExtendedDataType::setEncryptionService(new MCryptEncryptionService('foo')); + ExtendedDataType::setEncryptionService(new DefusePhpEncryptionService('def00000812bba10524777c97f1155877f0c91a4fac2c9f3d71a39d0df7214eab90d492faa58d2db667c5003c3c2228e3f19ad493ae86c74079a600a1ed51cd65e21f28e')); $extendedData = new ExtendedData(); $extendedData->set('foo', 'foo', false); diff --git a/Tests/Form/ChoosePaymentMethodTypeTest.php b/Tests/Form/ChoosePaymentMethodTypeTest.php index d7e6ad22..4f7de9e5 100644 --- a/Tests/Form/ChoosePaymentMethodTypeTest.php +++ b/Tests/Form/ChoosePaymentMethodTypeTest.php @@ -3,8 +3,8 @@ namespace JMS\Payment\CoreBundle\Tests\Form\ChoosePaymentMethodTypeTest; use JMS\Payment\CoreBundle\Form\ChoosePaymentMethodType; +use JMS\Payment\CoreBundle\Tests\Functional\TestPlugin\Form\TestPluginType; use JMS\Payment\CoreBundle\Util\Legacy; -use JMS\Payment\PaypalBundle\Form\ExpressCheckoutType; use Symfony\Component\Form\PreloadedExtension; use Symfony\Component\Form\Test\TypeTestCase; use Symfony\Component\HttpKernel\Kernel; @@ -50,7 +50,7 @@ public function testMethodData() $config = $form->get('data_'.$method)->getConfig(); $this->assertInstanceOf( - 'JMS\Payment\PaypalBundle\Form\ExpressCheckoutType', + 'JMS\Payment\CoreBundle\Tests\Functional\TestPlugin\Form\TestPluginType', $config->getType()->getInnerType() ); } @@ -58,7 +58,7 @@ public function testMethodData() public function testMethodChoices() { - if (Legacy::formChoicesAsValues()) { + if (!Legacy::formChoicesAsValues()) { $this->markTestSkipped(); } @@ -72,7 +72,7 @@ public function testMethodChoices() public function testLegacyMethodChoices() { - if (!Legacy::formChoicesAsValues()) { + if (Legacy::formChoicesAsValues()) { $this->markTestSkipped(); } @@ -104,7 +104,7 @@ public function testDefaultMethod() public function testAllowedMethods() { - if (Legacy::formChoicesAsValues()) { + if (!Legacy::formChoicesAsValues()) { $this->markTestSkipped(); } @@ -122,7 +122,7 @@ public function testAllowedMethods() public function testLegacyAllowedMethods() { - if (!Legacy::formChoicesAsValues()) { + if (Legacy::formChoicesAsValues()) { $this->markTestSkipped(); } @@ -173,9 +173,9 @@ private function createForm($options = array(), $data = array()) 'currency' => 'EUR', ), $options); - $form = Legacy::supportsFormTypeName() - ? 'jms_choose_payment_method' - : 'JMS\Payment\CoreBundle\Form\ChoosePaymentMethodType' + $form = Legacy::supportsFormTypeClass() + ? 'JMS\Payment\CoreBundle\Form\ChoosePaymentMethodType' + : 'jms_choose_payment_method' ; $form = $this->factory->create($form, null, $options); @@ -194,12 +194,12 @@ protected function setUp() protected function getExtensions() { - $pluginType = new ExpressCheckoutType(); + $pluginType = new TestPluginType(); - if (Legacy::supportsFormTypeName()) { - $pluginTypeName = $pluginType->getName(); - } else { + if (Legacy::supportsFormTypeClass()) { $pluginTypeName = get_class($pluginType); + } else { + $pluginTypeName = $pluginType->getName(); } $type = new ChoosePaymentMethodType($this->pluginController, array( @@ -207,13 +207,13 @@ protected function getExtensions() 'bar' => $pluginTypeName, )); - if (Legacy::supportsFormTypeName()) { + if (Legacy::supportsFormTypeClass()) { + $extensions = array($pluginType, $type); + } else { $extensions = array( $pluginType->getName() => $pluginType, $type->getName() => $type, ); - } else { - $extensions = array($pluginType, $type); } return array(new PreloadedExtension($extensions, array())); diff --git a/Tests/Functional/AppKernel.php b/Tests/Functional/AppKernel.php index e4a729f6..bd8be997 100644 --- a/Tests/Functional/AppKernel.php +++ b/Tests/Functional/AppKernel.php @@ -2,6 +2,7 @@ namespace JMS\Payment\CoreBundle\Tests\Functional; +use JMS\Payment\CoreBundle\Tests\Functional\TestPlugin\TestPluginBundle; use Symfony\Component\Config\Loader\LoaderInterface; use Symfony\Component\Filesystem\Filesystem; use Symfony\Component\HttpKernel\Kernel; @@ -16,7 +17,7 @@ public function __construct($config) $fs = new Filesystem(); if (!$fs->isAbsolutePath($config)) { - $config = __DIR__.'/config/'.$config; + $config = __DIR__.'/TestBundle/Resources/config/'.$config; } if (!file_exists($config)) { @@ -34,7 +35,7 @@ public function registerBundles() new \Symfony\Bundle\TwigBundle\TwigBundle(), new \JMS\Payment\CoreBundle\Tests\Functional\TestBundle\TestBundle(), new \JMS\Payment\CoreBundle\JMSPaymentCoreBundle(), - new \JMS\Payment\PaypalBundle\JMSPaymentPaypalBundle(), + new TestPluginBundle(), new \Sensio\Bundle\FrameworkExtraBundle\SensioFrameworkExtraBundle(), ); } @@ -46,6 +47,11 @@ public function registerContainerConfiguration(LoaderInterface $loader) public function getCacheDir() { - return sys_get_temp_dir().'/JMSPaymentCoreBundle'; + return sys_get_temp_dir().'/JMSPaymentCoreBundle/cache'; + } + + public function getLogDir() + { + return sys_get_temp_dir().'/JMSPaymentCoreBundle/logs'; } } diff --git a/Tests/Functional/BasePaymentWorkflowTest.php b/Tests/Functional/BasePaymentWorkflowTest.php new file mode 100755 index 00000000..a6cbbdc2 --- /dev/null +++ b/Tests/Functional/BasePaymentWorkflowTest.php @@ -0,0 +1,51 @@ +getContainer()->get('em'); + + $stmt = $em->getConnection()->prepare( + 'SELECT extended_data FROM payment_instructions WHERE id = '.$paymentInstruction->getId() + ); + + $stmt->execute(); + $result = $stmt->fetchAll(); + + return unserialize($result[0]['extended_data']); + } + + protected function doTestPaymentDetails() + { + $client = $this->createClient(); + $this->importDatabaseSchema(); + + $em = self::$kernel->getContainer()->get('em'); + $router = self::$kernel->getContainer()->get('router'); + + $order = new Order(123.45); + $em->persist($order); + $em->flush(); + + $crawler = $client->request('GET', $router->generate('payment_details', array('id' => $order->getId()))); + $form = $crawler->selectButton('submit_btn')->form(); + $form['jms_choose_payment_method[method]']->select('test_plugin'); + $client->submit($form); + + $response = $client->getResponse(); + $this->assertSame(201, $response->getStatusCode(), substr($response, 0, 2000)); + + $em->clear(); + $order = $em->getRepository('TestBundle:Order')->find($order->getId()); + $this->assertTrue(Number::compare(123.45, $order->getPaymentInstruction()->getAmount(), '==')); + $this->assertEquals('bar', $order->getPaymentInstruction()->getExtendedData()->get('foo')); + + return $order; + } +} diff --git a/Tests/Functional/BaseTestCase.php b/Tests/Functional/BaseTestCase.php index ab5fca4b..cf617960 100644 --- a/Tests/Functional/BaseTestCase.php +++ b/Tests/Functional/BaseTestCase.php @@ -10,7 +10,7 @@ class BaseTestCase extends WebTestCase protected static function createKernel(array $options = array()) { return self::$kernel = new AppKernel( - isset($options['config']) ? $options['config'] : 'default.yml' + isset($options['config']) ? $options['config'] : 'config.yml' ); } @@ -20,7 +20,7 @@ protected function setUp() $fs->remove(sys_get_temp_dir().'/JMSPaymentCoreBundle/'); } - final protected function importDatabaseSchema() + protected function importDatabaseSchema() { $em = self::$kernel->getContainer()->get('doctrine.orm.entity_manager'); diff --git a/Tests/Functional/BasicEntityInteractionsTest.php b/Tests/Functional/BasicEntityInteractionsTest.php index 0f9edea1..c48d4866 100644 --- a/Tests/Functional/BasicEntityInteractionsTest.php +++ b/Tests/Functional/BasicEntityInteractionsTest.php @@ -17,7 +17,7 @@ public function testIndependentCredit() $c = self::$kernel->getContainer(); $ppc = $c->get('payment.plugin_controller'); $em = $c->get('doctrine.orm.entity_manager'); - $instruction = new PaymentInstruction(123.45, 'EUR', 'paypal_express_checkout'); + $instruction = new PaymentInstruction(123.45, 'EUR', 'test_plugin'); $ppc->createPaymentInstruction($instruction); $credit = $ppc->createIndependentCredit($instruction->getId(), 123); diff --git a/Tests/Functional/Command/GenerateKeyCommandTest.php b/Tests/Functional/Command/GenerateKeyCommandTest.php new file mode 100644 index 00000000..424afdad --- /dev/null +++ b/Tests/Functional/Command/GenerateKeyCommandTest.php @@ -0,0 +1,48 @@ +add(new GenerateKeyCommand()); + + $this->command = $application->find('jms_payment_core:generate-key'); + + parent::setUp(); + } + + /** + * @runInSeparateProcess + */ + public function testGeneration() + { + $key = trim($this->execute(array())); + + $cipher = new DefusePhpEncryptionService($key); + + $this->assertNotEmpty($key); + $this->assertEquals('foo', $cipher->decrypt($cipher->encrypt('foo'))); + } + + private function execute(array $input) + { + $commandTester = new CommandTester($this->command); + + $commandTester->execute(array_merge(array( + 'command' => $this->command->getName(), + ), $input)); + + return $commandTester->getDisplay(); + } +} diff --git a/Tests/Functional/PaymentWorkflowMcryptTest.php b/Tests/Functional/PaymentWorkflowMcryptTest.php new file mode 100755 index 00000000..c10d224f --- /dev/null +++ b/Tests/Functional/PaymentWorkflowMcryptTest.php @@ -0,0 +1,27 @@ + 'config_mcrypt.yml')); + } + + /** + * @runInSeparateProcess + */ + public function testPaymentDetails() + { + if (version_compare(phpversion(), '7.1', '>=')) { + $this->markTestSkipped('mcrypt is deprecated since PHP 7.1'); + } + + $order = parent::doTestPaymentDetails(); + + $extendedData = $this->getRawExtendedData($order->getPaymentInstruction()); + $this->assertArrayHasKey('foo', $extendedData); + $this->assertNotEquals('bar', $extendedData['foo'][0]); + } +} diff --git a/Tests/Functional/PaymentWorkflowNoEncryptionTest.php b/Tests/Functional/PaymentWorkflowNoEncryptionTest.php new file mode 100755 index 00000000..a12ec5a1 --- /dev/null +++ b/Tests/Functional/PaymentWorkflowNoEncryptionTest.php @@ -0,0 +1,23 @@ + 'config_no_encryption.yml')); + } + + /** + * @runInSeparateProcess + */ + public function testPaymentDetails() + { + $order = parent::doTestPaymentDetails(); + + $extendedData = $this->getRawExtendedData($order->getPaymentInstruction()); + $this->assertArrayHasKey('foo', $extendedData); + $this->assertEquals('bar', $extendedData['foo'][0]); + } +} diff --git a/Tests/Functional/PaymentWorkflowTest.php b/Tests/Functional/PaymentWorkflowTest.php index 9a0a8f4e..f6fb7829 100755 --- a/Tests/Functional/PaymentWorkflowTest.php +++ b/Tests/Functional/PaymentWorkflowTest.php @@ -2,37 +2,17 @@ namespace JMS\Payment\CoreBundle\Tests\Functional; -use JMS\Payment\CoreBundle\Tests\Functional\TestBundle\Entity\Order; -use JMS\Payment\CoreBundle\Util\Number; - -class PaymentWorkflowTest extends BaseTestCase +class PaymentWorkflowTest extends BasePaymentWorkflowTest { /** * @runInSeparateProcess */ public function testPaymentDetails() { - $client = $this->createClient(); - $this->importDatabaseSchema(); - - $em = self::$kernel->getContainer()->get('em'); - $router = self::$kernel->getContainer()->get('router'); - - $order = new Order(123.45); - $em->persist($order); - $em->flush(); - - $crawler = $client->request('GET', $router->generate('payment_details', array('id' => $order->getId()))); - $form = $crawler->selectButton('submit_btn')->form(); - $form['jms_choose_payment_method[method]']->select('paypal_express_checkout'); - $client->submit($form); - - $response = $client->getResponse(); - $this->assertSame(201, $response->getStatusCode(), substr($response, 0, 2000)); + $order = parent::doTestPaymentDetails(); - $em->clear(); - $order = $em->getRepository('TestBundle:Order')->find($order->getId()); - $this->assertTrue(Number::compare(123.45, $order->getPaymentInstruction()->getAmount(), '==')); - $this->assertEquals('bar', $order->getPaymentInstruction()->getExtendedData()->get('foo')); + $extendedData = $this->getRawExtendedData($order->getPaymentInstruction()); + $this->assertArrayHasKey('foo', $extendedData); + $this->assertNotEquals('bar', $extendedData['foo'][0]); } } diff --git a/Tests/Functional/SchemaTest.php b/Tests/Functional/SchemaTest.php index bc23c613..6d6d08d4 100644 --- a/Tests/Functional/SchemaTest.php +++ b/Tests/Functional/SchemaTest.php @@ -7,6 +7,9 @@ class SchemaTest extends BaseTestCase { + /** + * @runInSeparateProcess + */ public function testLegacySchemaIsValid() { if (!Legacy::supportsSecureRandom()) { @@ -18,6 +21,9 @@ public function testLegacySchemaIsValid() $this->doTestSchemaIsValid(); } + /** + * @runInSeparateProcess + */ public function testSchemaIsValid() { if (Legacy::supportsSecureRandom()) { @@ -29,6 +35,9 @@ public function testSchemaIsValid() $this->doTestSchemaIsValid(); } + /** + * @runInSeparateProcess + */ private function doTestSchemaIsValid() { $this->createClient(); diff --git a/Tests/Functional/TestBundle/Controller/OrderController.php b/Tests/Functional/TestBundle/Controller/OrderController.php index 95de50ee..06ed6a6d 100755 --- a/Tests/Functional/TestBundle/Controller/OrderController.php +++ b/Tests/Functional/TestBundle/Controller/OrderController.php @@ -24,17 +24,16 @@ class OrderController extends Controller */ public function paymentDetailsAction(Order $order) { - $formType = Legacy::supportsFormTypeName() - ? 'jms_choose_payment_method' - : 'JMS\Payment\CoreBundle\Form\ChoosePaymentMethodType' + $formType = Legacy::supportsFormTypeClass() + ? 'JMS\Payment\CoreBundle\Form\ChoosePaymentMethodType' + : 'jms_choose_payment_method' ; $form = $this->get('form.factory')->create($formType, null, array( 'currency' => 'EUR', 'amount' => $order->getAmount(), - 'csrf_protection' => false, 'predefined_data' => array( - 'paypal_express_checkout' => array( + 'test_plugin' => array( 'foo' => 'bar', ), ), diff --git a/Tests/Functional/TestBundle/Resources/config/config.yml b/Tests/Functional/TestBundle/Resources/config/config.yml new file mode 100644 index 00000000..c19cbf54 --- /dev/null +++ b/Tests/Functional/TestBundle/Resources/config/config.yml @@ -0,0 +1,7 @@ +imports: + - { resource: default.yml } + +jms_payment_core: + encryption: + provider: defuse_php_encryption + secret: def00000812bba10524777c97f1155877f0c91a4fac2c9f3d71a39d0df7214eab90d492faa58d2db667c5003c3c2228e3f19ad493ae86c74079a600a1ed51cd65e21f28e diff --git a/Tests/Functional/TestBundle/Resources/config/config_mcrypt.yml b/Tests/Functional/TestBundle/Resources/config/config_mcrypt.yml new file mode 100644 index 00000000..9e504f1f --- /dev/null +++ b/Tests/Functional/TestBundle/Resources/config/config_mcrypt.yml @@ -0,0 +1,5 @@ +imports: + - { resource: default.yml } + +jms_payment_core: + secret: test diff --git a/Tests/Functional/TestBundle/Resources/config/config_no_encryption.yml b/Tests/Functional/TestBundle/Resources/config/config_no_encryption.yml new file mode 100644 index 00000000..c74c782a --- /dev/null +++ b/Tests/Functional/TestBundle/Resources/config/config_no_encryption.yml @@ -0,0 +1,4 @@ +imports: + - { resource: default.yml } + +jms_payment_core: ~ \ No newline at end of file diff --git a/Tests/Functional/TestBundle/Resources/config/database.php b/Tests/Functional/TestBundle/Resources/config/database.php new file mode 100755 index 00000000..568a7c61 --- /dev/null +++ b/Tests/Functional/TestBundle/Resources/config/database.php @@ -0,0 +1,8 @@ +loadFromExtension('doctrine', array( + 'dbal' => array( + 'driver' => 'pdo_sqlite', + 'path' => tempnam(sys_get_temp_dir(), 'database'), + ), +)); diff --git a/Tests/Functional/config/doctrine.yml b/Tests/Functional/TestBundle/Resources/config/default.yml similarity index 74% rename from Tests/Functional/config/doctrine.yml rename to Tests/Functional/TestBundle/Resources/config/default.yml index a7fc036e..e17a56e0 100644 --- a/Tests/Functional/config/doctrine.yml +++ b/Tests/Functional/TestBundle/Resources/config/default.yml @@ -1,5 +1,6 @@ imports: - { resource: database.php } + - { resource: framework.php } doctrine: orm: @@ -9,4 +10,4 @@ doctrine: auto_mapping: true services: - em: "@doctrine.orm.entity_manager" \ No newline at end of file + em: "@doctrine.orm.entity_manager" diff --git a/Tests/Functional/TestBundle/Resources/config/framework.php b/Tests/Functional/TestBundle/Resources/config/framework.php new file mode 100644 index 00000000..6a0af72b --- /dev/null +++ b/Tests/Functional/TestBundle/Resources/config/framework.php @@ -0,0 +1,29 @@ + false); + +if (version_compare(Kernel::VERSION, '2.7', '<')) { + // The 'assets' configuration is only available for Symfony >= 2.7 + $assets = array(); +} + +$container->loadFromExtension('framework', array_merge($assets, array( + 'secret' => 'test', + 'test' => true, + 'session' => array( + 'storage_id' => 'session.storage.mock_file', + ), + 'templating' => array( + 'engines' => array('twig', 'php'), + ), + 'router' => array( + 'resource' => '%kernel.root_dir%/TestBundle/Resources/config/routing.yml', + ), + 'form' => true, + 'validation' => array( + 'enabled' => true, + 'enable_annotations' => true, + ), +))); diff --git a/Tests/Functional/config/routing.yml b/Tests/Functional/TestBundle/Resources/config/routing.yml similarity index 100% rename from Tests/Functional/config/routing.yml rename to Tests/Functional/TestBundle/Resources/config/routing.yml diff --git a/Tests/Functional/TestPlugin/DependencyInjection/TestPluginExtension.php b/Tests/Functional/TestPlugin/DependencyInjection/TestPluginExtension.php new file mode 100644 index 00000000..636060b5 --- /dev/null +++ b/Tests/Functional/TestPlugin/DependencyInjection/TestPluginExtension.php @@ -0,0 +1,17 @@ +load('services.yml'); + } +} \ No newline at end of file diff --git a/Tests/Functional/TestPlugin/Form/TestPluginType.php b/Tests/Functional/TestPlugin/Form/TestPluginType.php new file mode 100644 index 00000000..cc232a0a --- /dev/null +++ b/Tests/Functional/TestPlugin/Form/TestPluginType.php @@ -0,0 +1,17 @@ +loadFromExtension('doctrine', array( - 'dbal' => array( - 'driver' => 'pdo_sqlite', - 'path' => tempnam(sys_get_temp_dir(), 'database'), - ), -)); diff --git a/Tests/Functional/config/default.yml b/Tests/Functional/config/default.yml deleted file mode 100644 index de21a814..00000000 --- a/Tests/Functional/config/default.yml +++ /dev/null @@ -1,14 +0,0 @@ -imports: - - { resource: framework.yml } - - { resource: doctrine.yml } - - { resource: twig.yml } - -jms_payment_core: - secret: test - -jms_payment_paypal: - username: schmit_1283340315_biz_api1.gmail.com - password: 1283340321 - signature: A93vj6VJ.ZIRNjbI6GFgi4N2Km.5ATLs-EinlyWk2htEGX0xc3L8YIBo - return_url: http://paypal.test/payment - cancel_url: http://paypal.test/payment diff --git a/Tests/Functional/config/framework.yml b/Tests/Functional/config/framework.yml deleted file mode 100644 index 701edc1b..00000000 --- a/Tests/Functional/config/framework.yml +++ /dev/null @@ -1,12 +0,0 @@ -framework: - secret: test - test: ~ - session: - storage_id: session.storage.mock_file - form: true - csrf_protection: true - validation: - enabled: true - enable_annotations: true - router: - resource: "%kernel.root_dir%/config/routing.yml" diff --git a/Tests/Functional/config/twig.yml b/Tests/Functional/config/twig.yml deleted file mode 100644 index f95d3bc0..00000000 --- a/Tests/Functional/config/twig.yml +++ /dev/null @@ -1,7 +0,0 @@ -framework: - templating: - engines: [twig, php] - -twig: - debug: "%kernel.debug%" - strict_variables: "%kernel.debug%" \ No newline at end of file diff --git a/Util/Legacy.php b/Util/Legacy.php index f239ad9a..cf619c39 100644 --- a/Util/Legacy.php +++ b/Util/Legacy.php @@ -11,23 +11,30 @@ class Legacy { /** - * Symfony 3.0 removes support for form type names. + * Symfony 2.8 adds support for form type as FQCN. * - * Instead of referencing types by name, they must be referenced by their + * Instead of referencing types by name, they should be referenced by their * fully-qualified class name (FQCN). */ - public static function supportsFormTypeName() + public static function supportsFormTypeClass() { - return version_compare(Kernel::VERSION, '3.0.0', '<'); + return method_exists( + 'Symfony\Component\Form\AbstractType', + 'getBlockPrefix' + ); } /** - * Symfony 3.0 requires using `configureOptions` instead of `setDefaultOptions` - * in form types. + * Before Symfony 2.6, setAllowedTypes() and addAllowedTypes() expected the + * values to be given as an array mapping option names to allowed types: + * $resolver->setAllowedTypes(array('port' => array('null', 'int'))); */ - public static function supportsFormTypeConfigureOptions() + public static function supportsOptionsResolverSetAllowedTypesAsArray() { - return version_compare(Kernel::VERSION, '3.0.0', '<'); + return !method_exists( + 'Symfony\Component\OptionsResolver\OptionsResolver', + 'setDefined' + ); } /** @@ -43,7 +50,22 @@ public static function supportsFormTypeConfigureOptions() */ public static function formChoicesAsValues() { - return version_compare(Kernel::VERSION, '3.0.0', '<'); + return method_exists( + 'Symfony\Component\Form\AbstractType', + 'configureOptions' + ); + } + + /** + * When using `choices_as_values` before Symfony 3.0, one must make sure to + * set the `choices_as_values` option to true + */ + public static function needsChoicesAsValuesOption() + { + return self::formChoicesAsValues() && method_exists( + 'Symfony\Component\Form\FormTypeInterface', + 'setDefaultOptions' + ); } /** diff --git a/composer.json b/composer.json index d8884511..b55f4571 100644 --- a/composer.json +++ b/composer.json @@ -1,7 +1,7 @@ { "name": "jms/payment-core-bundle", "type": "symfony-bundle", - "description": "This bundle is providing the foundation for various payment plugins.", + "description": "A unified API for processing payments with Symfony", "keywords": ["payment"], "homepage": "http://jmspaymentcorebundle.readthedocs.io", "license": "Apache2", @@ -20,8 +20,7 @@ } ], "require": { - "php": ">=5.3.2", - "ext-mcrypt": "*", + "php": ">=5.4", "doctrine/common": "~2.3", "doctrine/dbal": "~2.3", "doctrine/orm": "~2.3", @@ -33,13 +32,13 @@ "symfony/options-resolver": "~2.3|~3.0", "symfony/http-foundation": "~2.3|~3.0", "symfony/http-kernel": "~2.3|~3.0", - "symfony/validator": "~2.3|~3.0" + "symfony/validator": "~2.3|~3.0", + "defuse/php-encryption": "~2.0" }, "require-dev": { "phpunit/phpunit": "~4.8|~5.4", "symfony/phpunit-bridge": "~2.7", "doctrine/doctrine-bundle": "~1.6", - "jms/payment-paypal-bundle": "~1.0", "sensio/framework-extra-bundle": "~3.0", "symfony/dom-crawler": "~2.3|~3.0", "symfony/doctrine-bridge": "~2.3|~3.0", @@ -47,7 +46,10 @@ "symfony/routing": "~2.3|~3.0", "symfony/templating": "~2.3|~3.0", "symfony/twig-bundle": "~2.3|~3.0", - "symfony/twig-bridge": "~2.3|~3.0" + "symfony/twig-bridge": "~2.3|~3.0", + "twig/twig": "~1.0", + "matthiasnoback/symfony-config-test": "^2.1" + }, "autoload": { "psr-0": { "JMS\\Payment\\CoreBundle": "" } @@ -55,7 +57,7 @@ "target-dir": "JMS/Payment/CoreBundle", "extra": { "branch-alias": { - "dev-master": "1.2-dev" + "dev-master": "1.3-dev" } } } diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 03f2f128..35941ea0 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -12,7 +12,7 @@ bootstrap="Tests/bootstrap.php" > - + ./Tests/