diff --git a/app/code/Magento/Config/Model/Config/Parser/Comment.php b/app/code/Magento/Config/Model/Config/Parser/Comment.php new file mode 100644 index 0000000000000000000000000000000000000000..56dd38d55d800ec27c95b983d53dbb04a144dc87 --- /dev/null +++ b/app/code/Magento/Config/Model/Config/Parser/Comment.php @@ -0,0 +1,80 @@ +<?php +/** + * Copyright © 2013-2017 Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Config\Model\Config\Parser; + +use Magento\Config\Model\Placeholder\Environment; +use Magento\Config\Model\Placeholder\PlaceholderInterface; +use Magento\Framework\App\Config\CommentParserInterface; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Exception\FileSystemException; +use Magento\Framework\Filesystem; + +/** + * Class Comment. It is used to parse config paths from comment section. + */ +class Comment implements CommentParserInterface +{ + /** + * @var Filesystem + */ + private $filesystem; + + /** + * @var PlaceholderInterface + */ + private $placeholder; + + /** + * @param Filesystem $filesystem + * @param PlaceholderInterface $placeholder + */ + public function __construct( + Filesystem $filesystem, + PlaceholderInterface $placeholder + ) { + $this->filesystem = $filesystem; + $this->placeholder = $placeholder; + } + + /** + * Retrieves config paths from comment section of the file. + * Example of comment: + * * CONFIG__DEFAULT__SOME__CONF__PATH_ONE + * * CONFIG__DEFAULT__SOME__CONF__PATH_TWO + * This method will return: + * array( + * 'CONFIG__DEFAULT__SOME__CONF__PATH_ONE' => 'some/conf/path_one', + * 'CONFIG__DEFAULT__SOME__CONF__PATH_TWO' => 'some/conf/path_two' + * ); + * + * @param string $fileName + * @return array + * @throws FileSystemException + */ + public function execute($fileName) + { + $fileContent = $this->filesystem + ->getDirectoryRead(DirectoryList::CONFIG) + ->readFile($fileName); + + $pattern = sprintf('/\s+\*\s+(?P<placeholder>%s.*?)\s/', preg_quote(Environment::PREFIX)); + preg_match_all($pattern, $fileContent, $matches); + + if (!isset($matches['placeholder'])) { + return []; + } + + $configs = []; + foreach ($matches['placeholder'] as $placeholder) { + $path = $this->placeholder->restore($placeholder); + $path = preg_replace('/^' . ScopeConfigInterface::SCOPE_TYPE_DEFAULT . '\//', '', $path); + $configs[$placeholder] = $path; + } + + return $configs; + } +} diff --git a/app/code/Magento/Config/Test/Unit/Model/Config/Parser/CommentTest.php b/app/code/Magento/Config/Test/Unit/Model/Config/Parser/CommentTest.php new file mode 100644 index 0000000000000000000000000000000000000000..933f18e1e2919ba442028feada14834313d7b617 --- /dev/null +++ b/app/code/Magento/Config/Test/Unit/Model/Config/Parser/CommentTest.php @@ -0,0 +1,80 @@ +<?php +/** + * Copyright © 2013-2017 Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Config\Test\Unit\Model\Config\Parser; + +use Magento\Config\Model\Config\Parser\Comment; +use Magento\Config\Model\Placeholder\PlaceholderInterface; +use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Filesystem; +use Magento\Framework\Filesystem\Directory\ReadInterface; +use PHPUnit_Framework_MockObject_MockObject as MockObject; + +class CommentTest extends \PHPUnit_Framework_TestCase +{ + /** + * @var PlaceholderInterface|MockObject + */ + private $placeholderMock; + + /** + * @var Filesystem|MockObject + */ + private $fileSystemMock; + + /** + * @var Comment + */ + private $model; + + protected function setUp() + { + $this->placeholderMock = $this->getMockBuilder(PlaceholderInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $this->fileSystemMock = $this->getMockBuilder(Filesystem::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->model = new Comment( + $this->fileSystemMock, + $this->placeholderMock + ); + } + + public function testExecute() + { + $fileName = 'config.local.php'; + $directoryReadMock = $this->getMockBuilder(ReadInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $directoryReadMock->expects($this->once()) + ->method('readFile') + ->with($fileName) + ->willReturn(file_get_contents(__DIR__ . '/../_files/' . $fileName)); + $this->fileSystemMock->expects($this->once()) + ->method('getDirectoryRead') + ->with(DirectoryList::CONFIG) + ->willReturn($directoryReadMock); + $this->placeholderMock->expects($this->any()) + ->method('restore') + ->withConsecutive( + ['CONFIG__DEFAULT__SOME__PAYMENT__PASSWORD'], + ['CONFIG__DEFAULT__SOME__PAYMENT__TOKEN'] + ) + ->willReturnOnConsecutiveCalls( + 'some/payment/password', + 'some/payment/token' + ); + + $this->assertEquals( + $this->model->execute($fileName), + [ + 'CONFIG__DEFAULT__SOME__PAYMENT__PASSWORD' => 'some/payment/password', + 'CONFIG__DEFAULT__SOME__PAYMENT__TOKEN' => 'some/payment/token' + ] + ); + } +} diff --git a/app/code/Magento/Config/Test/Unit/Model/Config/_files/config.local.php b/app/code/Magento/Config/Test/Unit/Model/Config/_files/config.local.php new file mode 100644 index 0000000000000000000000000000000000000000..6c4907567b0320c163e1b0ada2c71259b30a2641 --- /dev/null +++ b/app/code/Magento/Config/Test/Unit/Model/Config/_files/config.local.php @@ -0,0 +1,24 @@ +<?php +/** + * Copyright © 2013-2017 Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +return [ + 'scopes' => [ + 'websites' => [ + 'admin' => [ + 'website_id' => '0' + ], + ], + ], + /** + * The configuration file doesn't contain sensitive data for security reasons. + * Sensitive data can be stored in the following environment variables: + * CONFIG__DEFAULT__SOME__PAYMENT__PASSWORD for some/payment/password + */ + 'system' => [] + /** + * CONFIG__DEFAULT__SOME__PAYMENT__TOKEN for some/payment/token + * test phrase CONFIG__DEFAULT__SOME__PAYMENT__TOKEN for some/payment/token + */ +]; diff --git a/app/code/Magento/Config/etc/di.xml b/app/code/Magento/Config/etc/di.xml index 9579e1ca3191445edd6599eb7b8e779b36c9ab76..ab8973a1a15ceecf227d9de73450b8f906fa0c91 100644 --- a/app/code/Magento/Config/etc/di.xml +++ b/app/code/Magento/Config/etc/di.xml @@ -9,6 +9,7 @@ <preference for="Magento\Config\Model\Config\Structure\SearchInterface" type="Magento\Config\Model\Config\Structure" /> <preference for="Magento\Config\Model\Config\Backend\File\RequestData\RequestDataInterface" type="Magento\Config\Model\Config\Backend\File\RequestData" /> <preference for="Magento\Framework\App\Config\ConfigResource\ConfigInterface" type="Magento\Config\Model\ResourceModel\Config" /> + <preference for="Magento\Framework\App\Config\CommentParserInterface" type="Magento\Config\Model\Config\Parser\Comment" /> <virtualType name="Magento\Framework\View\TemplateEngine\Xhtml\ConfigCompiler" type="Magento\Framework\View\TemplateEngine\Xhtml\Compiler" shared="false"> <arguments> <argument name="compilerText" xsi:type="object">Magento\Framework\View\TemplateEngine\Xhtml\Compiler\Text</argument> @@ -184,4 +185,9 @@ </argument> </arguments> </type> + <type name="Magento\Config\Model\Config\Parser\Comment"> + <arguments> + <argument name="placeholder" xsi:type="object">Magento\Config\Model\Placeholder\Environment</argument> + </arguments> + </type> </config> diff --git a/app/code/Magento/Deploy/Console/Command/App/SensitiveConfigSet/CollectorFactory.php b/app/code/Magento/Deploy/Console/Command/App/SensitiveConfigSet/CollectorFactory.php new file mode 100644 index 0000000000000000000000000000000000000000..e78b4b71d31184a3f44439d4a168766475b9ad09 --- /dev/null +++ b/app/code/Magento/Deploy/Console/Command/App/SensitiveConfigSet/CollectorFactory.php @@ -0,0 +1,68 @@ +<?php +/** + * Copyright © 2013-2017 Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Deploy\Console\Command\App\SensitiveConfigSet; + +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\ObjectManagerInterface; + +/** + * Class CollectorFactory creates instance of CollectorInterface. + */ +class CollectorFactory +{ + /**#@+ + * Constant for collector types. + */ + const TYPE_INTERACTIVE = 'interactive'; + const TYPE_SIMPLE = 'simple'; + /**#@-*/ + + /** + * @var ObjectManagerInterface + */ + private $objectManager; + + /** + * @var array + */ + private $types; + + /** + * @param ObjectManagerInterface $objectManager + * @param array $types + */ + public function __construct( + ObjectManagerInterface $objectManager, + array $types = [] + ) { + $this->objectManager = $objectManager; + $this->types = $types; + } + + /** + * Create instance of CollectorInterface by given type. + * + * @param string $type + * @return CollectorInterface + * @throws LocalizedException If collector type not exist in registered types array. + */ + public function create($type) + { + if (!isset($this->types[$type])) { + throw new LocalizedException(__('Class for type "%1" was not declared', $type)); + } + + $object = $this->objectManager->create($this->types[$type]); + + if (!$object instanceof CollectorInterface) { + throw new LocalizedException( + __('%1 does not implement %2', get_class($object), CollectorInterface::class) + ); + } + + return $object; + } +} diff --git a/app/code/Magento/Deploy/Console/Command/App/SensitiveConfigSet/CollectorInterface.php b/app/code/Magento/Deploy/Console/Command/App/SensitiveConfigSet/CollectorInterface.php new file mode 100644 index 0000000000000000000000000000000000000000..6aecf23adf37d494ba02ae46f2e09bd52fd1079f --- /dev/null +++ b/app/code/Magento/Deploy/Console/Command/App/SensitiveConfigSet/CollectorInterface.php @@ -0,0 +1,27 @@ +<?php +/** + * Copyright © 2013-2017 Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Deploy\Console\Command\App\SensitiveConfigSet; + +use Magento\Framework\Exception\LocalizedException; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +/** + * Interface CollectorInterface + */ +interface CollectorInterface +{ + /** + * Collects values from user input and return result as array. + * + * @param InputInterface $input + * @param OutputInterface $output + * @param array $configPaths list of available config paths. + * @return array + * @throws LocalizedException + */ + public function getValues(InputInterface $input, OutputInterface $output, array $configPaths); +} diff --git a/app/code/Magento/Deploy/Console/Command/App/SensitiveConfigSet/InteractiveCollector.php b/app/code/Magento/Deploy/Console/Command/App/SensitiveConfigSet/InteractiveCollector.php new file mode 100644 index 0000000000000000000000000000000000000000..3f67a98ff1c58d49dba645e4987c833d840260d2 --- /dev/null +++ b/app/code/Magento/Deploy/Console/Command/App/SensitiveConfigSet/InteractiveCollector.php @@ -0,0 +1,67 @@ +<?php +/** + * Copyright © 2013-2017 Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Deploy\Console\Command\App\SensitiveConfigSet; + +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Question\QuestionFactory; +use Symfony\Component\Console\Helper\QuestionHelper; + +/** + * Class InteractiveCollector collects configuration values from user input. + */ +class InteractiveCollector implements CollectorInterface +{ + /** + * @var QuestionFactory + */ + private $questionFactory; + + /** + * @var QuestionHelper + */ + private $questionHelper; + + /** + * @param QuestionFactory $questionFactory + * @param QuestionHelper $questionHelper + */ + public function __construct( + QuestionFactory $questionFactory, + QuestionHelper $questionHelper + ) { + $this->questionFactory = $questionFactory; + $this->questionHelper = $questionHelper; + } + + /** + * Collect list of configuration values from user input. + * For example, this method will return + * + * ```php + * [ + * 'some/configuration/path1' => 'someValue1', + * 'some/configuration/path2' => 'someValue2', + * 'some/configuration/path3' => 'someValue3', + * ] + * ``` + * + * {@inheritdoc} + */ + public function getValues(InputInterface $input, OutputInterface $output, array $configPaths) + { + $output->writeln('<info>Please set configuration values or skip them by pressing [Enter]:</info>'); + $values = []; + foreach ($configPaths as $configPath) { + $question = $this->questionFactory->create([ + 'question' => $configPath . ': ' + ]); + $values[$configPath] = $this->questionHelper->ask($input, $output, $question); + } + + return $values; + } +} diff --git a/app/code/Magento/Deploy/Console/Command/App/SensitiveConfigSet/SimpleCollector.php b/app/code/Magento/Deploy/Console/Command/App/SensitiveConfigSet/SimpleCollector.php new file mode 100644 index 0000000000000000000000000000000000000000..828b4254a53085a986c1b0933912bae3efe7bc0c --- /dev/null +++ b/app/code/Magento/Deploy/Console/Command/App/SensitiveConfigSet/SimpleCollector.php @@ -0,0 +1,131 @@ +<?php +/** + * Copyright © 2013-2017 Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Deploy\Console\Command\App\SensitiveConfigSet; + +use Magento\Deploy\Console\Command\App\SensitiveConfigSetCommand; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Phrase; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Question\Question; +use Symfony\Component\Console\Question\QuestionFactory; +use Symfony\Component\Console\Helper\QuestionHelper; + +/** + * Class SimpleCollector collects configuration value from user input. + */ +class SimpleCollector implements CollectorInterface +{ + /** + * @var QuestionFactory + */ + private $questionFactory; + + /** + * @var QuestionHelper + */ + private $questionHelper; + + /** + * @param QuestionFactory $questionFactory + * @param QuestionHelper $questionHelper + */ + public function __construct( + QuestionFactory $questionFactory, + QuestionHelper $questionHelper + ) { + $this->questionFactory = $questionFactory; + $this->questionHelper = $questionHelper; + } + + /** + * Collects single configuration value from user input. + * For example, this method will return + * + * ```php + * ['some/configuration/path' => 'someValue'] + * ``` + * + * {@inheritdoc} + */ + public function getValues(InputInterface $input, OutputInterface $output, array $configPaths) + { + $inputPath = $input->getArgument(SensitiveConfigSetCommand::INPUT_ARGUMENT_PATH); + $configPathQuestion = $this->getConfigPathQuestion($configPaths); + $configPath = $inputPath === null ? + $this->questionHelper->ask($input, $output, $configPathQuestion) : + $inputPath; + + $this->validatePath($configPath, $configPaths); + + $inputValue = $input->getArgument(SensitiveConfigSetCommand::INPUT_ARGUMENT_VALUE); + $configValueQuestion = $this->getConfigValueQuestion(); + $configValue = $inputValue === null ? + $this->questionHelper->ask($input, $output, $configValueQuestion) : + $inputValue; + + return [$configPath => $configValue]; + } + + /** + * Get Question to fill configuration path with autocompletion in interactive mode. + * + * @param array $configPaths + * @return Question + */ + private function getConfigPathQuestion(array $configPaths) + { + /** @var Question $configPathQuestion */ + $configPathQuestion = $this->questionFactory->create([ + 'question' => 'Please enter config path: ' + ]); + $configPathQuestion->setAutocompleterValues($configPaths); + $configPathQuestion->setValidator(function ($configPath) use ($configPaths) { + $this->validatePath($configPath, $configPaths); + return $configPath; + }); + + return $configPathQuestion; + } + + /** + * Get Question to fill configuration value in interactive mode. + * + * @return Question + */ + private function getConfigValueQuestion() + { + /** @var Question $configValueQuestion */ + $configValueQuestion = $this->questionFactory->create([ + 'question' => 'Please enter value: ' + ]); + $configValueQuestion->setValidator(function ($interviewer) { + if (empty($interviewer)) { + throw new LocalizedException(new Phrase('Value can\'t be empty')); + } + return $interviewer; + }); + + return $configValueQuestion; + } + + /** + * Check if entered configuration path is valid, throw LocalizedException otherwise. + * + * @param string $configPath Path that should be validated. + * @param array $configPaths List of allowed paths. + * @return void + * @throws LocalizedException If config path not exist in allowed config paths. + */ + private function validatePath($configPath, array $configPaths) + { + if (!in_array($configPath, $configPaths)) { + throw new LocalizedException( + new Phrase('A configuration with this path does not exist or is not sensitive') + ); + } + } +} diff --git a/app/code/Magento/Deploy/Console/Command/App/SensitiveConfigSetCommand.php b/app/code/Magento/Deploy/Console/Command/App/SensitiveConfigSetCommand.php new file mode 100644 index 0000000000000000000000000000000000000000..e8555ceb9530f59561dce02ed264b3f0de3ee72b --- /dev/null +++ b/app/code/Magento/Deploy/Console/Command/App/SensitiveConfigSetCommand.php @@ -0,0 +1,196 @@ +<?php +/** + * Copyright © 2013-2017 Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Deploy\Console\Command\App; + +use Magento\Deploy\Console\Command\App\SensitiveConfigSet\CollectorFactory; +use Magento\Deploy\Model\ConfigWriter; +use Magento\Framework\App\Config\CommentParserInterface; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\App\Scope\ValidatorInterface; +use Magento\Framework\Config\File\ConfigFilePool; +use Magento\Framework\Console\Cli; +use Magento\Framework\Exception\FileSystemException; +use Magento\Framework\Exception\LocalizedException; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; + +/** + * Command for set sensitive variable through deploy process. + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class SensitiveConfigSetCommand extends Command +{ + /**#@+ + * Names of input arguments or options. + */ + const INPUT_OPTION_INTERACTIVE = 'interactive'; + const INPUT_OPTION_SCOPE = 'scope'; + const INPUT_OPTION_SCOPE_CODE = 'scope-code'; + const INPUT_ARGUMENT_PATH = 'path'; + const INPUT_ARGUMENT_VALUE = 'value'; + /**#@-*/ + + /** + * @var CommentParserInterface + */ + private $commentParser; + + /** + * @var ConfigFilePool + */ + private $configFilePool; + + /** + * @var ConfigWriter + */ + private $configWriter; + + /** + * @var ValidatorInterface + */ + private $scopeValidator; + + /** + * @var CollectorFactory + */ + private $collectorFactory; + + /** + * @param ConfigFilePool $configFilePool + * @param CommentParserInterface $commentParser + * @param ConfigWriter $configWriter + * @param ValidatorInterface $scopeValidator + * @param CollectorFactory $collectorFactory + */ + public function __construct( + ConfigFilePool $configFilePool, + CommentParserInterface $commentParser, + ConfigWriter $configWriter, + ValidatorInterface $scopeValidator, + CollectorFactory $collectorFactory + ) { + parent::__construct(); + $this->commentParser = $commentParser; + $this->configFilePool = $configFilePool; + $this->configWriter = $configWriter; + $this->scopeValidator = $scopeValidator; + $this->collectorFactory = $collectorFactory; + } + + /** + * {@inheritdoc} + */ + protected function configure() + { + $this->addArgument( + self::INPUT_ARGUMENT_PATH, + InputArgument::OPTIONAL, + 'Configuration path for example group/section/field_name' + ); + $this->addArgument( + self::INPUT_ARGUMENT_VALUE, + InputArgument::OPTIONAL, + 'Configuration value' + ); + $this->addOption( + self::INPUT_OPTION_INTERACTIVE, + 'i', + InputOption::VALUE_NONE, + 'Enable interactive mode to set all sensitive variables' + ); + $this->addOption( + self::INPUT_OPTION_SCOPE, + null, + InputOption::VALUE_OPTIONAL, + 'Scope for configuration, if not set use \'default\'', + ScopeConfigInterface::SCOPE_TYPE_DEFAULT + ); + $this->addOption( + self::INPUT_OPTION_SCOPE_CODE, + null, + InputOption::VALUE_OPTIONAL, + 'Scope code for configuration, empty string by default', + '' + ); + $this->setName('config:sensitive:set') + ->setDescription('Set sensitive configuration values'); + parent::configure(); + } + + /** + * {@inheritdoc} + */ + protected function execute(InputInterface $input, OutputInterface $output) + { + $scope = $input->getOption(self::INPUT_OPTION_SCOPE); + $scopeCode = $input->getOption(self::INPUT_OPTION_SCOPE_CODE); + + try { + $this->scopeValidator->isValid($scope, $scopeCode); + $configPaths = $this->getConfigPaths(); + $isInteractive = $input->getOption(self::INPUT_OPTION_INTERACTIVE); + $collector = $this->collectorFactory->create( + $isInteractive ? CollectorFactory::TYPE_INTERACTIVE : CollectorFactory::TYPE_SIMPLE + ); + $values = $collector->getValues($input, $output, $configPaths); + $this->configWriter->save($values, $scope, $scopeCode); + } catch (LocalizedException $e) { + $output->writeln(sprintf('<error>%s</error>', $e->getMessage())); + return Cli::RETURN_FAILURE; + } + + $this->writeSuccessMessage($output, $isInteractive); + + return Cli::RETURN_SUCCESS; + } + + /** + * Writes success message. + * + * @param OutputInterface $output + * @param boolean $isInteractive + * @return void + */ + private function writeSuccessMessage(OutputInterface $output, $isInteractive) + { + $output->writeln(sprintf( + '<info>Configuration value%s saved in app/etc/%s</info>', + $isInteractive ? 's' : '', + $this->configFilePool->getPath(ConfigFilePool::APP_CONFIG) + )); + } + + /** + * Get sensitive configuration paths. + * + * @return array + * @throws LocalizedException if configuration file not exists or sensitive configuration is empty + */ + private function getConfigPaths() + { + $configFilePath = $this->configFilePool->getPathsByPool(ConfigFilePool::LOCAL)[ConfigFilePool::APP_CONFIG]; + try { + $configPaths = $this->commentParser->execute($configFilePath); + } catch (FileSystemException $e) { + throw new LocalizedException(__( + 'File app/etc/%1 can\'t be read. Please check if it exists and has read permissions.', + [ + $configFilePath + ] + )); + } + + if (empty($configPaths)) { + throw new LocalizedException(__('There are no sensitive configurations to fill')); + } + + return $configPaths; + } +} diff --git a/app/code/Magento/Deploy/Model/ConfigWriter.php b/app/code/Magento/Deploy/Model/ConfigWriter.php new file mode 100644 index 0000000000000000000000000000000000000000..7a9b70c528ac9e5d9010e0683b651823ecb74c4a --- /dev/null +++ b/app/code/Magento/Deploy/Model/ConfigWriter.php @@ -0,0 +1,109 @@ +<?php +/** + * Copyright © 2013-2017 Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Deploy\Model; + +use Magento\Config\App\Config\Type\System; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\App\DeploymentConfig\Writer; +use Magento\Framework\Config\File\ConfigFilePool; +use Magento\Framework\Stdlib\ArrayManager; + +/** + * Class ConfigWriter. Save configuration values into config file. + */ +class ConfigWriter +{ + /** + * @var Writer + */ + private $writer; + + /** + * @var ArrayManager + */ + private $arrayManager; + + /** + * @param Writer $writer + * @param ArrayManager $arrayManager + */ + public function __construct( + Writer $writer, + ArrayManager $arrayManager + ) { + $this->writer = $writer; + $this->arrayManager = $arrayManager; + } + + /** + * Save given list of configuration values into config file. + * + * @param array $values the configuration values (path-value pairs) to be saved. + * @param string $scope scope in which configuration would be saved. + * @param string|null $scopeCode + * @return void + */ + public function save(array $values, $scope = ScopeConfigInterface::SCOPE_TYPE_DEFAULT, $scopeCode = null) + { + $config = []; + $pathPrefix = $this->getPathPrefix($scope, $scopeCode); + foreach ($values as $configPath => $configValue) { + $fullConfigPath = $pathPrefix . $configPath; + $config = $this->setConfig($config, $fullConfigPath, $configValue); + } + + $this->writer + ->saveConfig( + [ConfigFilePool::APP_CONFIG => $config] + ); + } + + /** + * Apply configuration value into configuration array by given path. + * Ignore values that equal to null. + * + * @param array $config + * @param string $configPath + * @param string $configValue + * @return array + */ + private function setConfig(array $config, $configPath, $configValue) + { + if ($configValue === null) { + return $config; + } + + $config = $this->arrayManager->set( + $configPath, + $config, + $configValue + ); + + return $config; + } + + /** + * Generate config prefix from given $scope and $scopeCode. + * If $scope isn't equal to 'default' and $scopeCode isn't empty put $scopeCode into prefix path, + * otherwise ignore $scopeCode. + * + * @param string $scope + * @param string $scopeCode + * @return string + */ + private function getPathPrefix($scope, $scopeCode) + { + $pathPrefixes = [System::CONFIG_TYPE, $scope]; + if ( + $scope !== ScopeConfigInterface::SCOPE_TYPE_DEFAULT + && !empty($scopeCode) + ) { + $pathPrefixes[] = $scopeCode; + } + + return implode('/', $pathPrefixes) . '/'; + } +} diff --git a/app/code/Magento/Deploy/Test/Unit/Console/Command/App/SensitiveConfigSet/CollectorFactoryTest.php b/app/code/Magento/Deploy/Test/Unit/Console/Command/App/SensitiveConfigSet/CollectorFactoryTest.php new file mode 100644 index 0000000000000000000000000000000000000000..c16fa1d7c176db14d525444a3f18e2231258a31f --- /dev/null +++ b/app/code/Magento/Deploy/Test/Unit/Console/Command/App/SensitiveConfigSet/CollectorFactoryTest.php @@ -0,0 +1,81 @@ +<?php +/** + * Copyright © 2013-2017 Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Deploy\Test\Unit\Console\Command\App\SensitiveConfigSet; + +use Magento\Deploy\Console\Command\App\SensitiveConfigSet\CollectorFactory; +use Magento\Deploy\Console\Command\App\SensitiveConfigSet\CollectorInterface; +use Magento\Deploy\Console\Command\App\SensitiveConfigSet\InteractiveCollector; +use Magento\Deploy\Console\Command\App\SensitiveConfigSet\SimpleCollector; +use Magento\Framework\ObjectManagerInterface; +use PHPUnit_Framework_MockObject_MockObject as MockObject; +use stdClass; + +class CollectorFactoryTest extends \PHPUnit_Framework_TestCase +{ + /** + * @var ObjectManagerInterface|MockObject + */ + private $objectManagerMock; + + /** + * @var CollectorFactory + */ + private $model; + + protected function setUp() + { + $this->objectManagerMock = $this->getMockBuilder(ObjectManagerInterface::class) + ->getMockForAbstractClass(); + + $this->model = new CollectorFactory( + $this->objectManagerMock, + [ + CollectorFactory::TYPE_SIMPLE => SimpleCollector::class, + CollectorFactory::TYPE_INTERACTIVE => InteractiveCollector::class, + 'wrongType' => stdClass::class, + ] + ); + } + + public function testCreate() + { + $collectorMock = $this->getMockBuilder(CollectorInterface::class) + ->getMockForAbstractClass(); + $this->objectManagerMock->expects($this->once()) + ->method('create') + ->with(SimpleCollector::class) + ->willReturn($collectorMock); + + $this->assertInstanceOf( + CollectorInterface::class, + $this->model->create(CollectorFactory::TYPE_SIMPLE) + ); + } + + /** + * @expectedException \Magento\Framework\Exception\LocalizedException + * @expectedExceptionMessage Class for type "dummyType" was not declared + */ + public function testCreateNonExisted() + { + $this->model->create('dummyType'); + } + + /** + * @expectedException \Magento\Framework\Exception\LocalizedException + * @expectedExceptionMessage stdClass does not implement + */ + public function testCreateWrongImplementation() + { + $type = 'wrongType'; + $this->objectManagerMock->expects($this->once()) + ->method('create') + ->with(stdClass::class) + ->willReturn(new stdClass()); + + $this->model->create($type); + } +} diff --git a/app/code/Magento/Deploy/Test/Unit/Console/Command/App/SensitiveConfigSet/InteractiveCollectorTest.php b/app/code/Magento/Deploy/Test/Unit/Console/Command/App/SensitiveConfigSet/InteractiveCollectorTest.php new file mode 100644 index 0000000000000000000000000000000000000000..91576c89463726db1e0f034e72ac96e418770083 --- /dev/null +++ b/app/code/Magento/Deploy/Test/Unit/Console/Command/App/SensitiveConfigSet/InteractiveCollectorTest.php @@ -0,0 +1,100 @@ +<?php +/** + * Copyright © 2013-2017 Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Deploy\Test\Unit\Console\Command\App\SensitiveConfigSet; + +use Magento\Deploy\Console\Command\App\SensitiveConfigSet\InteractiveCollector; +use PHPUnit_Framework_MockObject_MockObject as MockObject; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Question\Question; +use Symfony\Component\Console\Question\QuestionFactory; +use Symfony\Component\Console\Helper\QuestionHelper; + +class InteractiveCollectorTest extends \PHPUnit_Framework_TestCase +{ + /** + * @var QuestionFactory|MockObject + */ + private $questionFactoryMock; + + /** + * @var QuestionHelper|MockObject + */ + private $questionHelperMock; + + /** + * @var InputInterface|MockObject + */ + private $inputMock; + + /** + * @var OutputInterface|MockObject + */ + private $outputMock; + + /** + * @var InteractiveCollector + */ + private $model; + + protected function setUp() + { + $this->questionFactoryMock = $this->getMockBuilder(QuestionFactory::class) + ->disableOriginalConstructor() + ->setMethods(['create']) + ->getMock(); + $this->questionHelperMock = $this->getMockBuilder(QuestionHelper::class) + ->disableOriginalConstructor() + ->getMock(); + $this->inputMock = $this->getMockBuilder(InputInterface::class) + ->getMockForAbstractClass(); + $this->outputMock = $this->getMockBuilder(OutputInterface::class) + ->getMockForAbstractClass(); + + $this->model = new InteractiveCollector( + $this->questionFactoryMock, + $this->questionHelperMock + ); + } + + public function testGetValues() + { + $configPaths = [ + 'some/config/path1', + 'some/config/path2', + 'some/config/path3' + ]; + + $questionMock = $this->getMockBuilder(Question::class) + ->disableOriginalConstructor() + ->getMock(); + $this->questionHelperMock->expects($this->exactly(3)) + ->method('ask') + ->with($this->inputMock, $this->outputMock, $questionMock) + ->willReturn('someValue'); + $this->questionFactoryMock->expects($this->exactly(3)) + ->method('create') + ->withConsecutive( + [['question' => $configPaths[0] . ': ']], + [['question' => $configPaths[1] . ': ']], + [['question' => $configPaths[2] . ': ']] + ) + ->willReturn($questionMock); + + $this->assertEquals( + [ + 'some/config/path1' => 'someValue', + 'some/config/path2' => 'someValue', + 'some/config/path3' => 'someValue' + ], + $this->model->getValues( + $this->inputMock, + $this->outputMock, + $configPaths + ) + ); + } +} diff --git a/app/code/Magento/Deploy/Test/Unit/Console/Command/App/SensitiveConfigSet/SimpleCollectorTest.php b/app/code/Magento/Deploy/Test/Unit/Console/Command/App/SensitiveConfigSet/SimpleCollectorTest.php new file mode 100644 index 0000000000000000000000000000000000000000..124fc4b0119e377f508e8b1b22439446bd9d8fe4 --- /dev/null +++ b/app/code/Magento/Deploy/Test/Unit/Console/Command/App/SensitiveConfigSet/SimpleCollectorTest.php @@ -0,0 +1,187 @@ +<?php +/** + * Copyright © 2013-2017 Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Deploy\Test\Unit\Console\Command\App\SensitiveConfigSet; + +use Magento\Deploy\Console\Command\App\SensitiveConfigSet\SimpleCollector; +use Magento\Deploy\Console\Command\App\SensitiveConfigSetCommand; +use Magento\Framework\Exception\LocalizedException; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Question\Question; +use Symfony\Component\Console\Question\QuestionFactory; +use Symfony\Component\Console\Helper\QuestionHelper; +use PHPUnit_Framework_MockObject_MockObject as MockObject; + +class SimpleCollectorTest extends \PHPUnit_Framework_TestCase +{ + /** + * @var QuestionFactory|MockObject + */ + private $questionFactoryMock; + + /** + * @var QuestionHelper|MockObject + */ + private $questionHelperMock; + + /** + * @var InputInterface|MockObject + */ + private $inputMock; + + /** + * @var OutputInterface|MockObject + */ + private $outputMock; + + /** + * @var SimpleCollector + */ + private $model; + + protected function setUp() + { + $this->questionFactoryMock = $this->getMockBuilder(QuestionFactory::class) + ->disableOriginalConstructor() + ->setMethods(['create']) + ->getMock(); + $this->questionHelperMock = $this->getMockBuilder(QuestionHelper::class) + ->disableOriginalConstructor() + ->getMock(); + $this->inputMock = $this->getMockBuilder(InputInterface::class) + ->getMockForAbstractClass(); + $this->outputMock = $this->getMockBuilder(OutputInterface::class) + ->getMockForAbstractClass(); + + $this->model = new SimpleCollector( + $this->questionFactoryMock, + $this->questionHelperMock + ); + } + + public function testGetValues() + { + $configPaths = [ + 'some/config/path1', + 'some/config/path2' + ]; + + $pathQuestionMock = $this->getMockBuilder(Question::class) + ->disableOriginalConstructor() + ->getMock(); + $valueQuestionMock = $this->getMockBuilder(Question::class) + ->disableOriginalConstructor() + ->getMock(); + $this->inputMock->expects($this->exactly(2)) + ->method('getArgument') + ->withConsecutive( + [SensitiveConfigSetCommand::INPUT_ARGUMENT_PATH], + [SensitiveConfigSetCommand::INPUT_ARGUMENT_VALUE] + ) + ->willReturnOnConsecutiveCalls( + $configPaths[0], + 'someValue' + ); + $this->questionFactoryMock->expects($this->exactly(2)) + ->method('create') + ->withConsecutive( + [['question' => 'Please enter config path: ']], + [['question' => 'Please enter value: ']] + ) + ->willReturnOnConsecutiveCalls( + $pathQuestionMock, + $valueQuestionMock + ); + + $this->assertEquals( + ['some/config/path1' => 'someValue'], + $this->model->getValues( + $this->inputMock, + $this->outputMock, + $configPaths + ) + ); + } + + /** + * @expectedException \Magento\Framework\Exception\LocalizedException + * @expectedExceptionMessage A configuration with this path does not exist or is not sensitive + */ + public function testWrongConfigPath() + { + $configPaths = [ + 'some/config/path1', + 'some/config/path2' + ]; + + $pathQuestionMock = $this->getMockBuilder(Question::class) + ->disableOriginalConstructor() + ->getMock(); + $this->inputMock->expects($this->once()) + ->method('getArgument') + ->with(SensitiveConfigSetCommand::INPUT_ARGUMENT_PATH) + ->willReturn('some/not_exist/config'); + $this->questionFactoryMock->expects($this->once()) + ->method('create') + ->with(['question' => 'Please enter config path: ']) + ->willReturn($pathQuestionMock); + + $this->model->getValues( + $this->inputMock, + $this->outputMock, + $configPaths + ); + } + + /** + * @expectedException \Magento\Framework\Exception\LocalizedException + */ + public function testEmptyValue() + { + $configPaths = [ + 'some/config/path1', + 'some/config/path2' + ]; + $message = 'exception message'; + + $pathQuestionMock = $this->getMockBuilder(Question::class) + ->disableOriginalConstructor() + ->getMock(); + $valueQuestionMock = $this->getMockBuilder(Question::class) + ->disableOriginalConstructor() + ->getMock(); + $this->questionHelperMock->expects($this->once()) + ->method('ask') + ->with($this->inputMock, $this->outputMock, $valueQuestionMock) + ->willThrowException(new LocalizedException(__($message))); + $this->inputMock->expects($this->exactly(2)) + ->method('getArgument') + ->withConsecutive( + [SensitiveConfigSetCommand::INPUT_ARGUMENT_PATH], + [SensitiveConfigSetCommand::INPUT_ARGUMENT_VALUE] + ) + ->willReturnOnConsecutiveCalls( + $configPaths[0], + null + ); + $this->questionFactoryMock->expects($this->exactly(2)) + ->method('create') + ->withConsecutive( + [['question' => 'Please enter config path: ']], + [['question' => 'Please enter value: ']] + ) + ->willReturnOnConsecutiveCalls( + $pathQuestionMock, + $valueQuestionMock + ); + + $this->model->getValues( + $this->inputMock, + $this->outputMock, + $configPaths + ); + } +} diff --git a/app/code/Magento/Deploy/Test/Unit/Console/Command/App/SensitiveConfigSetCommandTest.php b/app/code/Magento/Deploy/Test/Unit/Console/Command/App/SensitiveConfigSetCommandTest.php new file mode 100644 index 0000000000000000000000000000000000000000..b305ed8249fbff8d80854385f1f3fa79d1204541 --- /dev/null +++ b/app/code/Magento/Deploy/Test/Unit/Console/Command/App/SensitiveConfigSetCommandTest.php @@ -0,0 +1,262 @@ +<?php +/** + * Copyright © 2013-2017 Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Deploy\Test\Unit\Console\Command\App; + +use Magento\Deploy\Console\Command\App\SensitiveConfigSet\CollectorFactory; +use Magento\Deploy\Console\Command\App\SensitiveConfigSet\CollectorInterface; +use Magento\Deploy\Console\Command\App\SensitiveConfigSetCommand; +use Magento\Deploy\Model\ConfigWriter; +use Magento\Framework\App\Config\CommentParserInterface; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\App\Scope\ValidatorInterface; +use Magento\Framework\Config\File\ConfigFilePool; +use Magento\Framework\Console\Cli; +use Magento\Framework\Exception\FileSystemException; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Phrase; +use PHPUnit_Framework_MockObject_MockObject as MockObject; +use Symfony\Component\Console\Tester\CommandTester; + +/** + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class SensitiveConfigSetCommandTest extends \PHPUnit_Framework_TestCase +{ + /** + * @var ConfigFilePool|MockObject + */ + private $configFilePoolMock; + + /** + * @var CommentParserInterface|MockObject + */ + private $commentParserMock; + + /** + * @var ConfigWriter|MockObject + */ + private $configWriterMock; + + /** + * @var ValidatorInterface|MockObject + */ + private $scopeValidatorMock; + + /** + * @var CollectorFactory|MockObject + */ + private $collectorFactoryMock; + + /** + * @var SensitiveConfigSetCommand + */ + private $command; + + public function setUp() + { + $this->configFilePoolMock = $this->getMockBuilder(ConfigFilePool::class) + ->disableOriginalConstructor() + ->getMock(); + $this->commentParserMock = $this->getMockBuilder(CommentParserInterface::class) + ->getMockForAbstractClass(); + $this->configWriterMock = $this->getMockBuilder(ConfigWriter::class) + ->disableOriginalConstructor() + ->getMock(); + $this->scopeValidatorMock = $this->getMockBuilder(ValidatorInterface::class) + ->getMockForAbstractClass(); + $this->collectorFactoryMock = $this->getMockBuilder(CollectorFactory::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->command = new SensitiveConfigSetCommand( + $this->configFilePoolMock, + $this->commentParserMock, + $this->configWriterMock, + $this->scopeValidatorMock, + $this->collectorFactoryMock + ); + } + + public function testConfigFileNotExist() + { + $this->configFilePoolMock->expects($this->once()) + ->method('getPathsByPool') + ->with(ConfigFilePool::LOCAL) + ->willReturn([ + ConfigFilePool::APP_CONFIG => 'config.local.php' + ]); + $this->scopeValidatorMock->expects($this->once()) + ->method('isValid') + ->with('default', '') + ->willReturn(true); + $this->commentParserMock->expects($this->any()) + ->method('execute') + ->willThrowException(new FileSystemException(new Phrase('some message'))); + + $tester = new CommandTester($this->command); + $tester->execute([ + 'path' => 'some/path', + 'value' => 'some value' + ]); + + $this->assertEquals( + Cli::RETURN_FAILURE, + $tester->getStatusCode() + ); + $this->assertContains( + 'File app/etc/config.local.php can\'t be read. ' + . 'Please check if it exists and has read permissions.', + $tester->getDisplay() + ); + } + + public function testWriterException() + { + $exceptionMessage = 'exception'; + $this->scopeValidatorMock->expects($this->once()) + ->method('isValid') + ->with('default', '') + ->willReturn(true); + $this->commentParserMock->expects($this->once()) + ->method('execute') + ->willReturn([ + 'some/config/path1', + 'some/config/path2' + ]); + $collectorMock = $this->getMockBuilder(CollectorInterface::class) + ->getMockForAbstractClass(); + $collectorMock->expects($this->once()) + ->method('getValues') + ->willReturn(['some/config/pathNotExist' => 'value']); + $this->collectorFactoryMock->expects($this->once()) + ->method('create') + ->with(CollectorFactory::TYPE_SIMPLE) + ->willReturn($collectorMock); + $this->configWriterMock->expects($this->once()) + ->method('save') + ->willThrowException(new LocalizedException(__($exceptionMessage))); + + $tester = new CommandTester($this->command); + $tester->execute([ + 'path' => 'some/config/pathNotExist', + 'value' => 'some value' + ]); + + $this->assertEquals( + Cli::RETURN_FAILURE, + $tester->getStatusCode() + ); + $this->assertContains( + $exceptionMessage, + $tester->getDisplay() + ); + } + + public function testEmptyConfigPaths() + { + $this->scopeValidatorMock->expects($this->once()) + ->method('isValid') + ->with('default', '') + ->willReturn(true); + $this->commentParserMock->expects($this->once()) + ->method('execute') + ->willReturn([]); + + $tester = new CommandTester($this->command); + $tester->execute([ + 'path' => 'some/config/pathNotExist', + 'value' => 'some value' + ]); + + $this->assertEquals( + Cli::RETURN_FAILURE, + $tester->getStatusCode() + ); + $this->assertContains( + 'There are no sensitive configurations to fill', + $tester->getDisplay() + ); + } + + public function testExecute() + { + $collectedValues = ['some/config/path1' => 'value']; + $this->scopeValidatorMock->expects($this->once()) + ->method('isValid') + ->with('default', '') + ->willReturn(true); + $this->commentParserMock->expects($this->once()) + ->method('execute') + ->willReturn([ + 'some/config/path1', + 'some/config/path2' + ]); + $collectorMock = $this->getMockBuilder(CollectorInterface::class) + ->getMockForAbstractClass(); + $collectorMock->expects($this->once()) + ->method('getValues') + ->willReturn($collectedValues); + $this->collectorFactoryMock->expects($this->once()) + ->method('create') + ->with(CollectorFactory::TYPE_SIMPLE) + ->willReturn($collectorMock); + $this->configWriterMock->expects($this->once()) + ->method('save') + ->with($collectedValues, ScopeConfigInterface::SCOPE_TYPE_DEFAULT, ''); + + $tester = new CommandTester($this->command); + $tester->execute([]); + + $this->assertEquals( + Cli::RETURN_SUCCESS, + $tester->getStatusCode() + ); + $this->assertContains( + 'Configuration value saved in', + $tester->getDisplay() + ); + } + + public function testExecuteInteractive() + { + $collectedValues = ['some/config/path1' => 'value']; + $this->scopeValidatorMock->expects($this->once()) + ->method('isValid') + ->with('default', '') + ->willReturn(true); + $this->commentParserMock->expects($this->once()) + ->method('execute') + ->willReturn([ + 'some/config/path1', + 'some/config/path2', + 'some/config/path3' + ]); + $collectorMock = $this->getMockBuilder(CollectorInterface::class) + ->getMockForAbstractClass(); + $collectorMock->expects($this->once()) + ->method('getValues') + ->willReturn($collectedValues); + $this->collectorFactoryMock->expects($this->once()) + ->method('create') + ->with(CollectorFactory::TYPE_INTERACTIVE) + ->willReturn($collectorMock); + $this->configWriterMock->expects($this->once()) + ->method('save') + ->with($collectedValues, ScopeConfigInterface::SCOPE_TYPE_DEFAULT, ''); + + $tester = new CommandTester($this->command); + $tester->execute(['--interactive' => true]); + + $this->assertEquals( + Cli::RETURN_SUCCESS, + $tester->getStatusCode() + ); + $this->assertContains( + 'Configuration values saved in', + $tester->getDisplay() + ); + } +} diff --git a/app/code/Magento/Deploy/Test/Unit/Model/ConfigWriterTest.php b/app/code/Magento/Deploy/Test/Unit/Model/ConfigWriterTest.php new file mode 100644 index 0000000000000000000000000000000000000000..9ac0097d5f73b69ea9bf02cafa7cd4773d9a810e --- /dev/null +++ b/app/code/Magento/Deploy/Test/Unit/Model/ConfigWriterTest.php @@ -0,0 +1,93 @@ +<?php +/** + * Copyright © 2013-2017 Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Deploy\Test\Unit\Model; + +use Magento\Deploy\Model\ConfigWriter; +use Magento\Framework\App\DeploymentConfig\Writer; +use Magento\Framework\Config\File\ConfigFilePool; +use Magento\Framework\Stdlib\ArrayManager; +use PHPUnit_Framework_MockObject_MockObject as MockObject; + +class ConfigWriterTest extends \PHPUnit_Framework_TestCase +{ + /** + * @var Writer|MockObject + */ + private $writerMock; + + /** + * @var ArrayManager|MockObject + */ + private $arrayManagerMock; + + /** + * @var ConfigWriter + */ + private $model; + + public function setUp() + { + $this->arrayManagerMock = $this->getMockBuilder(ArrayManager::class) + ->disableOriginalConstructor() + ->getMock(); + $this->writerMock = $this->getMockBuilder(Writer::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->model = new ConfigWriter( + $this->writerMock, + $this->arrayManagerMock + ); + } + + public function testSave() + { + $values = [ + 'some1/config1/path1' => 'someValue1', + 'some2/config2/path2' => 'someValue2', + 'some3/config3/path3' => 'someValue3' + ]; + $config = ['system' => []]; + + $this->arrayManagerMock->expects($this->exactly(3)) + ->method('set') + ->withConsecutive( + ['system/scope/scope_code/some1/config1/path1', $this->anything(), 'someValue1'], + ['system/scope/scope_code/some2/config2/path2', $this->anything(), 'someValue2'], + ['system/scope/scope_code/some3/config3/path3', $this->anything(), 'someValue3'] + ) + ->willReturn($config); + $this->writerMock->expects($this->once()) + ->method('saveConfig') + ->with([ConfigFilePool::APP_CONFIG => $config]); + + $this->model->save($values, 'scope', 'scope_code'); + } + + public function testSaveDefaultScope() + { + $values = [ + 'some1/config1/path1' => 'someValue1', + 'some2/config2/path2' => 'someValue2', + 'some3/config3/path3' => 'someValue3' + ]; + $config = ['system' => []]; + + $this->arrayManagerMock->expects($this->exactly(3)) + ->method('set') + ->withConsecutive( + ['system/default/some1/config1/path1', $this->anything(), 'someValue1'], + ['system/default/some2/config2/path2', $this->anything(), 'someValue2'], + ['system/default/some3/config3/path3', $this->anything(), 'someValue3'] + ) + ->willReturn($config); + $this->writerMock->expects($this->once()) + ->method('saveConfig') + ->with([ConfigFilePool::APP_CONFIG => $config]); + + $this->model->save($values); + } +} diff --git a/app/code/Magento/Deploy/composer.json b/app/code/Magento/Deploy/composer.json index ed0b8520ca645afa8af2506616f9dccde6965f08..436c3a6451c39b3e65ae65e3740b402bbc75fe80 100644 --- a/app/code/Magento/Deploy/composer.json +++ b/app/code/Magento/Deploy/composer.json @@ -6,7 +6,8 @@ "magento/framework": "100.2.*", "magento/module-store": "100.2.*", "magento/module-require-js": "100.2.*", - "magento/module-user": "100.2.*" + "magento/module-user": "100.2.*", + "magento/module-config": "100.2.*" }, "type": "magento2-module", "version": "100.2.0-dev", diff --git a/app/code/Magento/Deploy/etc/di.xml b/app/code/Magento/Deploy/etc/di.xml index 68fa7d909e4df8a3b13f3ef01a8f178d804feb4d..6751dd1e0b161dfee948436818a7f38c50025daf 100644 --- a/app/code/Magento/Deploy/etc/di.xml +++ b/app/code/Magento/Deploy/etc/di.xml @@ -26,6 +26,7 @@ <item name="setModeCommand" xsi:type="object">Magento\Deploy\Console\Command\SetModeCommand</item> <item name="showModeCommand" xsi:type="object">Magento\Deploy\Console\Command\ShowModeCommand</item> <item name="dumpApplicationCommand" xsi:type="object">\Magento\Deploy\Console\Command\App\ApplicationDumpCommand</item> + <item name="sensitiveConfigSetCommand" xsi:type="object">\Magento\Deploy\Console\Command\App\SensitiveConfigSetCommand</item> </argument> </arguments> </type> @@ -34,4 +35,12 @@ <argument name="shell" xsi:type="object">Magento\Framework\App\Shell</argument> </arguments> </type> + <type name="Magento\Deploy\Console\Command\App\SensitiveConfigSet\CollectorFactory"> + <arguments> + <argument name="types" xsi:type="array"> + <item name="interactive" xsi:type="string">Magento\Deploy\Console\Command\App\SensitiveConfigSet\InteractiveCollector</item> + <item name="simple" xsi:type="string">Magento\Deploy\Console\Command\App\SensitiveConfigSet\SimpleCollector</item> + </argument> + </arguments> + </type> </config> diff --git a/app/code/Magento/Fedex/etc/di.xml b/app/code/Magento/Fedex/etc/di.xml new file mode 100644 index 0000000000000000000000000000000000000000..e2f5142cde96b5f82c10175da290e680e79f419c --- /dev/null +++ b/app/code/Magento/Fedex/etc/di.xml @@ -0,0 +1,19 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © 2013-2017 Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> + <type name="Magento\Config\Model\Config\Export\ExcludeList"> + <arguments> + <argument name="configs" xsi:type="array"> + <item name="carriers/fedex/account" xsi:type="string">1</item> + <item name="carriers/fedex/key" xsi:type="string">1</item> + <item name="carriers/fedex/meter_number" xsi:type="string">1</item> + <item name="carriers/fedex/password" xsi:type="string">1</item> + </argument> + </arguments> + </type> +</config> \ No newline at end of file diff --git a/app/code/Magento/Store/Model/Scope/Validator.php b/app/code/Magento/Store/Model/Scope/Validator.php new file mode 100644 index 0000000000000000000000000000000000000000..f2222fdc476a9a61ee9ed37c285332561f6dd43b --- /dev/null +++ b/app/code/Magento/Store/Model/Scope/Validator.php @@ -0,0 +1,88 @@ +<?php +/** + * Copyright © 2013-2017 Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Store\Model\Scope; + +use InvalidArgumentException; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\App\Scope\ValidatorInterface; +use Magento\Framework\App\ScopeResolverPool; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Exception\NoSuchEntityException; + +/** + * Class Validator validates scope and scope code. + */ +class Validator implements ValidatorInterface +{ + /** + * @var ScopeResolverPool + */ + private $scopeResolverPool; + + /** + * @param ScopeResolverPool $scopeResolverPool + */ + public function __construct(ScopeResolverPool $scopeResolverPool) + { + $this->scopeResolverPool = $scopeResolverPool; + } + + /** + * {@inheritdoc} + */ + public function isValid($scope, $scopeCode = null) + { + if ($scope === ScopeConfigInterface::SCOPE_TYPE_DEFAULT && empty($scopeCode)) { + return true; + } + + if ($scope === ScopeConfigInterface::SCOPE_TYPE_DEFAULT && !empty($scopeCode)) { + throw new LocalizedException(__( + 'The "%1" scope can\'t include a scope code. Try again without entering a scope code.', + ScopeConfigInterface::SCOPE_TYPE_DEFAULT + )); + } + + if (empty($scope)) { + throw new LocalizedException(__('Enter a scope before proceeding.')); + } + + $this->validateScopeCode($scopeCode); + + try { + $scopeResolver = $this->scopeResolverPool->get($scope); + $scopeResolver->getScope($scopeCode)->getId(); + } catch (InvalidArgumentException $e) { + throw new LocalizedException(__('The "%1" value doesn\'t exist. Enter another value.', $scope)); + } catch (NoSuchEntityException $e) { + throw new LocalizedException(__('The "%1" value doesn\'t exist. Enter another value.', $scopeCode)); + } + + return true; + } + + /** + * Validate scope code + * Throw exception if not valid. + * + * @param string $scopeCode + * @return void + * @throws LocalizedException if scope code is empty or has a wrong format + */ + private function validateScopeCode($scopeCode) + { + if (empty($scopeCode)) { + throw new LocalizedException(__('Enter a scope code before proceeding.')); + } + + if (!preg_match('/^[a-z]+[a-z0-9_]*$/', $scopeCode)) { + throw new LocalizedException(__( + 'The scope code can include only lowercase letters (a-z), numbers (0-9) and underscores (_). ' + . 'Also, the first character must be a letter.' + )); + } + } +} diff --git a/app/code/Magento/Store/Test/Unit/Model/Scope/ValidatorTest.php b/app/code/Magento/Store/Test/Unit/Model/Scope/ValidatorTest.php new file mode 100644 index 0000000000000000000000000000000000000000..51c58739f5ff5c10f19ebcbef9d7a92df33611b0 --- /dev/null +++ b/app/code/Magento/Store/Test/Unit/Model/Scope/ValidatorTest.php @@ -0,0 +1,138 @@ +<?php +/** + * Copyright © 2013-2017 Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Store\Test\Unit\Model\Scope; + +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\App\ScopeInterface; +use Magento\Framework\App\ScopeResolverInterface; +use Magento\Framework\App\ScopeResolverPool; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Store\Model\Scope\Validator; +use PHPUnit_Framework_MockObject_MockObject as MockObject; + +class ValidatorTest extends \PHPUnit_Framework_TestCase +{ + /** + * @var Validator + */ + private $model; + + /** + * @var ScopeResolverPool|MockObject + */ + private $scopeResolverPoolMock; + + protected function setUp() + { + $this->scopeResolverPoolMock = $this->getMockBuilder(ScopeResolverPool::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->model = new Validator( + $this->scopeResolverPoolMock + ); + } + + public function testIsValid() + { + $scope = 'not_default_scope'; + $scopeCode = 'not_exist_scope_code'; + + $scopeResolver = $this->getMockBuilder(ScopeResolverInterface::class) + ->getMockForAbstractClass(); + $scopeObject = $this->getMockBuilder(ScopeInterface::class) + ->getMockForAbstractClass(); + $scopeResolver->expects($this->once()) + ->method('getScope') + ->with($scopeCode) + ->willReturn($scopeObject); + $this->scopeResolverPoolMock->expects($this->once()) + ->method('get') + ->with($scope) + ->willReturn($scopeResolver); + + $this->assertTrue($this->model->isValid($scope, $scopeCode)); + } + + public function testIsValidDefault() + { + $this->assertTrue($this->model->isValid(ScopeConfigInterface::SCOPE_TYPE_DEFAULT)); + } + + /** + * @expectedException \Magento\Framework\Exception\LocalizedException + * @expectedExceptionMessage The "default" scope can't include a scope code. Try again without entering a scope + */ + public function testNotEmptyScopeCodeForDefaultScope() + { + $this->model->isValid(ScopeConfigInterface::SCOPE_TYPE_DEFAULT, 'some_code'); + } + + /** + * @expectedException \Magento\Framework\Exception\LocalizedException + * @expectedExceptionMessage Enter a scope before proceeding. + */ + public function testEmptyScope() + { + $this->model->isValid('', 'some_code'); + } + + /** + * @expectedException \Magento\Framework\Exception\LocalizedException + * @expectedExceptionMessage Enter a scope code before proceeding. + */ + public function testEmptyScopeCode() + { + $this->model->isValid('not_default_scope', ''); + } + + /** + * @expectedException \Magento\Framework\Exception\LocalizedException + * @expectedExceptionMessage The scope code can include only lowercase letters (a-z), numbers (0-9) and underscores + */ + public function testWrongScopeCodeFormat() + { + $this->model->isValid('not_default_scope', '123'); + } + + /** + * @expectedException \Magento\Framework\Exception\LocalizedException + * @expectedExceptionMessage The "not_default_scope" value doesn't exist. Enter another value. + */ + public function testScopeNotExist() + { + $scope = 'not_default_scope'; + $this->scopeResolverPoolMock->expects($this->once()) + ->method('get') + ->with($scope) + ->willThrowException(new \InvalidArgumentException()); + + $this->model->isValid($scope, 'scope_code'); + } + + /** + * @expectedException \Magento\Framework\Exception\LocalizedException + * @expectedExceptionMessage The "not_exist_scope_code" value doesn't exist. Enter another value. + */ + public function testScopeCodeNotExist() + { + $scope = 'not_default_scope'; + $scopeCode = 'not_exist_scope_code'; + + $scopeResolver = $this->getMockBuilder(ScopeResolverInterface::class) + ->getMockForAbstractClass(); + $scopeResolver->expects($this->once()) + ->method('getScope') + ->with($scopeCode) + ->willThrowException(new NoSuchEntityException()); + $this->scopeResolverPoolMock->expects($this->once()) + ->method('get') + ->with($scope) + ->willReturn($scopeResolver); + + $this->model->isValid($scope, $scopeCode); + } +} diff --git a/app/code/Magento/Store/etc/di.xml b/app/code/Magento/Store/etc/di.xml index 48a37dec7d73a9ae9fda07cce9c709b3d0b94284..d3b91a57ef3141fb054239ebd919cb48d77ad5e8 100644 --- a/app/code/Magento/Store/etc/di.xml +++ b/app/code/Magento/Store/etc/di.xml @@ -25,6 +25,7 @@ <preference for="Magento\Framework\App\ScopeFallbackResolverInterface" type="Magento\Store\Model\ScopeFallbackResolver"/> <preference for="Magento\Framework\App\ScopeTreeProviderInterface" type="Magento\Store\Model\ScopeTreeProvider"/> <preference for="Magento\Framework\App\ScopeValidatorInterface" type="Magento\Store\Model\ScopeValidator"/> + <preference for="Magento\Framework\App\Scope\ValidatorInterface" type="Magento\Store\Model\Scope\Validator"/> <type name="Magento\Framework\App\Response\Http"> <plugin name="genericHeaderPlugin" type="Magento\Framework\App\Response\HeaderManager"/> </type> diff --git a/dev/tests/integration/testsuite/Magento/Deploy/Console/Command/App/SensitiveConfigSetCommandTest.php b/dev/tests/integration/testsuite/Magento/Deploy/Console/Command/App/SensitiveConfigSetCommandTest.php new file mode 100644 index 0000000000000000000000000000000000000000..1b0fe2de866e817ab78e646087b9ebe248a60818 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Deploy/Console/Command/App/SensitiveConfigSetCommandTest.php @@ -0,0 +1,291 @@ +<?php +/** + * Copyright © 2013-2017 Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Deploy\Console\Command\App; + +use Magento\Deploy\Console\Command\App\SensitiveConfigSet\CollectorFactory; +use Magento\Deploy\Console\Command\App\SensitiveConfigSet\InteractiveCollector; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\App\DeploymentConfig\Reader; +use Magento\Framework\App\DeploymentConfig\Writer; +use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Config\File\ConfigFilePool; +use Magento\Framework\Filesystem; +use Magento\Framework\ObjectManagerInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Symfony\Component\Console\Helper\QuestionHelper; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +/** + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class SensitiveConfigSetCommandTest extends \PHPUnit_Framework_TestCase +{ + /** + * @var ObjectManagerInterface + */ + private $objectManager; + + /** + * @var Reader + */ + private $reader; + + /** + * @var ConfigFilePool + */ + private $configFilePool; + + /** + * @var array + */ + private $config; + + /** + * @var Filesystem + */ + private $filesystem; + + public function setUp() + { + $this->objectManager = Bootstrap::getObjectManager(); + $this->reader = $this->objectManager->get(Reader::class); + $this->configFilePool = $this->objectManager->get(ConfigFilePool::class); + $this->config = $this->loadConfig(); + $this->filesystem = $this->objectManager->get(Filesystem::class); + $this->filesystem->getDirectoryWrite(DirectoryList::CONFIG)->writeFile( + $this->getFileName(), + file_get_contents(__DIR__ . '/../../../_files/_config.local.php') + ); + } + + /** + * @param $scope + * @param $scopeCode + * @param callable $assertCallback + * @magentoDataFixture Magento/Store/_files/website.php + * @magentoDbIsolation enabled + * @dataProvider testExecuteDataProvider + */ + public function testExecute($scope, $scopeCode, callable $assertCallback) + { + $outputMock = $this->getMock(OutputInterface::class); + $outputMock->expects($this->at(0)) + ->method('writeln') + ->with('<info>Configuration value saved in app/etc/config.php</info>'); + + $inputMock = $this->getMock(InputInterface::class); + $inputMock->expects($this->exactly(2)) + ->method('getArgument') + ->withConsecutive( + [SensitiveConfigSetCommand::INPUT_ARGUMENT_PATH], + [SensitiveConfigSetCommand::INPUT_ARGUMENT_VALUE] + ) + ->willReturnOnConsecutiveCalls( + 'some/config/path_two', + 'sensitiveValue' + ); + $inputMock->expects($this->exactly(3)) + ->method('getOption') + ->withConsecutive( + [SensitiveConfigSetCommand::INPUT_OPTION_SCOPE], + [SensitiveConfigSetCommand::INPUT_OPTION_SCOPE_CODE], + [SensitiveConfigSetCommand::INPUT_OPTION_INTERACTIVE] + ) + ->willReturnOnConsecutiveCalls( + $scope, + $scopeCode, + null + ); + + /** @var SensitiveConfigSetCommand command */ + $command = $this->objectManager->create(SensitiveConfigSetCommand::class); + $command->run($inputMock, $outputMock); + + $config = $this->loadConfig(); + + $assertCallback($config); + } + + public function testExecuteDataProvider() + { + return [ + [ + ScopeConfigInterface::SCOPE_TYPE_DEFAULT, + null, + function (array $config) { + $this->assertTrue(isset($config['system']['default']['some']['config']['path_two'])); + $this->assertEquals( + 'sensitiveValue', + $config['system']['default']['some']['config']['path_two'] + ); + } + ], + [ + 'website', + 'test', + function (array $config) { + $this->assertTrue(isset($config['system']['website']['test']['some']['config']['path_two'])); + $this->assertEquals( + 'sensitiveValue', + $config['system']['website']['test']['some']['config']['path_two'] + ); + } + ] + ]; + } + + /** + * @param $scope + * @param $scopeCode + * @param callable $assertCallback + * @magentoDataFixture Magento/Store/_files/website.php + * @magentoDbIsolation enabled + * @dataProvider testExecuteInteractiveDataProvider + */ + public function testExecuteInteractive($scope, $scopeCode, callable $assertCallback) + { + $inputMock = $this->getMock(InputInterface::class); + $outputMock = $this->getMock(OutputInterface::class); + $outputMock->expects($this->at(0)) + ->method('writeln') + ->with('<info>Please set configuration values or skip them by pressing [Enter]:</info>'); + $outputMock->expects($this->at(1)) + ->method('writeln') + ->with('<info>Configuration values saved in app/etc/config.php</info>'); + $inputMock->expects($this->exactly(3)) + ->method('getOption') + ->withConsecutive( + [SensitiveConfigSetCommand::INPUT_OPTION_SCOPE], + [SensitiveConfigSetCommand::INPUT_OPTION_SCOPE_CODE], + [SensitiveConfigSetCommand::INPUT_OPTION_INTERACTIVE] + ) + ->willReturnOnConsecutiveCalls( + $scope, + $scopeCode, + true + ); + + $questionHelperMock = $this->getMock(QuestionHelper::class); + $questionHelperMock->expects($this->exactly(3)) + ->method('ask') + ->willReturn('sensitiveValue'); + + $interactiveCollectorMock = $this->objectManager->create( + InteractiveCollector::class, + [ + 'questionHelper' => $questionHelperMock + ] + ); + $collectorFactoryMock = $this->getMockBuilder(CollectorFactory::class) + ->disableOriginalConstructor() + ->getMock(); + + $collectorFactoryMock->expects($this->once()) + ->method('create') + ->with(CollectorFactory::TYPE_INTERACTIVE) + ->willReturn($interactiveCollectorMock); + + /** @var SensitiveConfigSetCommand command */ + $command = $this->objectManager->create( + SensitiveConfigSetCommand::class, + [ + 'collectorFactory' => $collectorFactoryMock + ] + ); + $command->run($inputMock, $outputMock); + + $config = $this->loadConfig(); + + $assertCallback($config); + } + + public function testExecuteInteractiveDataProvider() + { + return [ + [ + ScopeConfigInterface::SCOPE_TYPE_DEFAULT, + null, + function (array $config) { + $this->assertTrue(isset($config['system']['default']['some']['config']['path_one'])); + $this->assertTrue(isset($config['system']['default']['some']['config']['path_two'])); + $this->assertTrue(isset($config['system']['default']['some']['config']['path_three'])); + $this->assertEquals( + 'sensitiveValue', + $config['system']['default']['some']['config']['path_one'] + ); + $this->assertEquals( + 'sensitiveValue', + $config['system']['default']['some']['config']['path_two'] + ); + $this->assertEquals( + 'sensitiveValue', + $config['system']['default']['some']['config']['path_three'] + ); + } + ], + [ + 'website', + 'test', + function (array $config) { + $this->assertTrue(isset($config['system']['website']['test']['some']['config']['path_one'])); + $this->assertTrue(isset($config['system']['website']['test']['some']['config']['path_two'])); + $this->assertTrue(isset($config['system']['website']['test']['some']['config']['path_three'])); + $this->assertEquals( + 'sensitiveValue', + $config['system']['website']['test']['some']['config']['path_one'] + ); + $this->assertEquals( + 'sensitiveValue', + $config['system']['website']['test']['some']['config']['path_two'] + ); + $this->assertEquals( + 'sensitiveValue', + $config['system']['website']['test']['some']['config']['path_three'] + ); + } + ] + ]; + } + + public function tearDown() + { + $this->filesystem->getDirectoryWrite(DirectoryList::CONFIG)->delete( + $this->getFileName() + ); + $this->filesystem->getDirectoryWrite(DirectoryList::CONFIG)->writeFile( + $this->configFilePool->getPath(ConfigFilePool::APP_CONFIG), + "<?php\n return array();\n" + ); + /** @var Writer $writer */ + $writer = $this->objectManager->get(Writer::class); + $writer->saveConfig([ConfigFilePool::APP_CONFIG => $this->config]); + } + + /** + * @return string + */ + private function getFileName() + { + /** @var ConfigFilePool $configFilePool */ + $configFilePool = $this->objectManager->get(ConfigFilePool::class); + $filePool = $configFilePool->getInitialFilePools(); + + return $filePool[ConfigFilePool::LOCAL][ConfigFilePool::APP_CONFIG]; + } + + /** + * @return array + */ + private function loadConfig() + { + return $this->reader->loadConfigFile( + ConfigFilePool::APP_CONFIG, + $this->configFilePool->getPath(ConfigFilePool::APP_CONFIG), + true + ); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Deploy/_files/_config.local.php b/dev/tests/integration/testsuite/Magento/Deploy/_files/_config.local.php new file mode 100644 index 0000000000000000000000000000000000000000..ae4630b1a2b07e021ba1e7e1e409f4e1e81a99a6 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Deploy/_files/_config.local.php @@ -0,0 +1,23 @@ +<?php +/** + * Copyright © 2013-2017 Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +return [ + 'scopes' => [ + 'websites' => [] + ], + /** + * The configuration file doesn't contain sensitive data for security reasons. + * Sensitive data can be stored in the following environment variables: + * CONFIG__DEFAULT__SOME__CONFIG__PATH_ONE for some/config/path_one + * CONFIG__DEFAULT__SOME__CONFIG__PATH_TWO for some/config/path_two + * CONFIG__DEFAULT__SOME__CONFIG__PATH_THREE for some/config/path_three + */ + 'system' => [ + 'default' => [ + 'web' => [], + 'general' => [] + ] + ] +]; diff --git a/lib/internal/Magento/Framework/App/Config/CommentParserInterface.php b/lib/internal/Magento/Framework/App/Config/CommentParserInterface.php new file mode 100644 index 0000000000000000000000000000000000000000..efc917d31b0c70a1c0ee6a4b8a070f71fdbb322e --- /dev/null +++ b/lib/internal/Magento/Framework/App/Config/CommentParserInterface.php @@ -0,0 +1,23 @@ +<?php +/** + * Copyright © 2013-2017 Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Framework\App\Config; + +use Magento\Framework\Exception\FileSystemException; + +/** + * Interface CommentParserInterface + */ +interface CommentParserInterface +{ + /** + * Retrieve config list from file comments. + * + * @param string $fileName + * @return array + * @throws FileSystemException + */ + public function execute($fileName); +} diff --git a/lib/internal/Magento/Framework/App/DeploymentConfig/Writer/PhpFormatter.php b/lib/internal/Magento/Framework/App/DeploymentConfig/Writer/PhpFormatter.php index ae8a9bd4dc64e66ecf8ab865f61541d9bdc94a6b..28fcbd3692c9e1dffb84b0a2ef98b1ffb0633d96 100644 --- a/lib/internal/Magento/Framework/App/DeploymentConfig/Writer/PhpFormatter.php +++ b/lib/internal/Magento/Framework/App/DeploymentConfig/Writer/PhpFormatter.php @@ -25,7 +25,10 @@ class PhpFormatter implements FormatterInterface foreach ($data as $key => $value) { $comment = ' '; if (!empty($comments[$key])) { - $comment = " /**\n * " . str_replace("\n", "\n * ", var_export($comments[$key], true)) . "\n */\n"; + $exportedComment = is_string($comments[$key]) + ? $comments[$key] + : var_export($comments[$key], true); + $comment = " /**\n * " . str_replace("\n", "\n * ", $exportedComment) . "\n */\n"; } $space = is_array($value) ? " \n" : ' '; $elements[] = $comment . var_export($key, true) . ' =>' . $space . var_export($value, true); diff --git a/lib/internal/Magento/Framework/App/Scope/ValidatorInterface.php b/lib/internal/Magento/Framework/App/Scope/ValidatorInterface.php new file mode 100644 index 0000000000000000000000000000000000000000..8063d61d8f5b29395183b9742dc45d6084957955 --- /dev/null +++ b/lib/internal/Magento/Framework/App/Scope/ValidatorInterface.php @@ -0,0 +1,22 @@ +<?php +/** + * Copyright © 2013-2017 Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Framework\App\Scope; + +use Magento\Framework\Exception\LocalizedException; + +interface ValidatorInterface +{ + /** + * Validate if exists given scope and scope code + * otherwise, throws an exception with appropriate message. + * + * @param string $scope + * @param string $scopeCode + * @return boolean + * @throws LocalizedException + */ + public function isValid($scope, $scopeCode = null); +} diff --git a/lib/internal/Magento/Framework/App/Test/Unit/DeploymentConfig/Writer/PhpFormatterTest.php b/lib/internal/Magento/Framework/App/Test/Unit/DeploymentConfig/Writer/PhpFormatterTest.php index 0f86b778a04d2d1c52535adf4f6eafb7d5af2795..9f6894c0a68350e86b9e96a8111de123ac11d574 100644 --- a/lib/internal/Magento/Framework/App/Test/Unit/DeploymentConfig/Writer/PhpFormatterTest.php +++ b/lib/internal/Magento/Framework/App/Test/Unit/DeploymentConfig/Writer/PhpFormatterTest.php @@ -3,7 +3,6 @@ * Copyright © 2013-2017 Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - namespace Magento\Framework\App\Test\Unit\DeploymentConfig\Writer; use \Magento\Framework\App\DeploymentConfig\Writer\PhpFormatter; @@ -48,9 +47,9 @@ class PhpFormatterTest extends \PHPUnit_Framework_TestCase ]; $comments1 = ['ns2' => 'comment for namespace 2']; $comments2 = [ - 'ns1' => 'comment for namespace 1', - 'ns2' => "comment for namespace 2.\nNext comment for namespace 2", - 'ns3' => 'comment for namespace 3', + 'ns1' => 'comment for\' namespace 1', + 'ns2' => "comment for namespace 2.\nNext comment for' namespace 2", + 'ns3' => 'comment for" namespace 3', 'ns4' => 'comment for namespace 4', 'ns5' => 'comment for unexisted namespace 5', ]; @@ -71,7 +70,7 @@ return array ( ), ), /** - * 'comment for namespace 2' + * comment for namespace 2 */ 'ns2' => array ( @@ -89,7 +88,7 @@ TEXT; <?php return array ( /** - * 'comment for namespace 1' + * comment for' namespace 1 */ 'ns1' => array ( @@ -105,8 +104,8 @@ return array ( ), ), /** - * 'comment for namespace 2. - * Next comment for namespace 2' + * comment for namespace 2. + * Next comment for' namespace 2 */ 'ns2' => array ( @@ -116,11 +115,11 @@ return array ( ), ), /** - * 'comment for namespace 3' + * comment for" namespace 3 */ 'ns3' => 'just text', /** - * 'comment for namespace 4' + * comment for namespace 4 */ 'ns4' => 'just text' );