diff --git a/CHANGELOG.md b/CHANGELOG.md index c10a8d8c518..2c02d77927f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,7 @@ CHANGELOG for 1.10.0 =================== This changelog references the relevant changes (new features, changes and bugs) done in 1.10.0 versions. - * The application has been upgraded to Symfony 2.8 + * The application has been upgraded to Symfony 2.8 (Symfony 2.8.10 doesn't supported because of [Symfony issue](https://github.com/symfony/symfony/issues/19840)) * Added support php 7 * Changed minimum required php version to 5.5.9 diff --git a/UPGRADE-1.10.1.md b/UPGRADE-1.10.1.md new file mode 100644 index 00000000000..a8970d57d3a --- /dev/null +++ b/UPGRADE-1.10.1.md @@ -0,0 +1,19 @@ +UPGRADE FROM 1.10.0 to 1.10.1 +============================= + +####EntityExtendBundle +- `Oro\Bundle\EntityExtendBundle\Migration\EntityMetadataHelper` + - `getEntityClassByTableName` deprecated, use `getEntityClassesByTableName` instead + - removed property `tableToClassMap` in favour of `tableToClassesMap` +- `Oro\Bundle\EntityExtendBundle\Migration\ExtendOptionsBuilder + - construction signature was changed now it takes next arguments: + `EntityMetadataHelper` $entityMetadataHelper, + `FieldTypeHelper` $fieldTypeHelper, + `ConfigManager` $configManager + - removed property `tableToEntityMap` in favour of `tableToEntitiesMap` + - renamed method `getEntityClassName` in favour of `getEntityClassNames` +- `Oro\Bundle\EntityExtendBundle\Migration\ExtendOptionsParser` + - construction signature was changed now it takes next arguments: + `EntityMetadataHelper` $entityMetadataHelper, + `FieldTypeHelper` $fieldTypeHelper, + `ConfigManager` $configManager diff --git a/UPGRADE-1.9.9.md b/UPGRADE-1.9.9.md new file mode 100644 index 00000000000..b5c43a7093e --- /dev/null +++ b/UPGRADE-1.9.9.md @@ -0,0 +1,19 @@ +UPGRADE FROM 1.9.8 to 1.9.9 +=========================== + +####EntityExtendBundle +- `Oro\Bundle\EntityExtendBundle\Migration\EntityMetadataHelper` + - `getEntityClassByTableName` deprecated, use `getEntityClassesByTableName` instead + - removed property `tableToClassMap` in favour of `tableToClassesMap` +- `Oro\Bundle\EntityExtendBundle\Migration\ExtendOptionsBuilder + - construction signature was changed now it takes next arguments: + `EntityMetadataHelper` $entityMetadataHelper, + `FieldTypeHelper` $fieldTypeHelper, + `ConfigManager` $configManager + - removed property `tableToEntityMap` in favour of `tableToEntitiesMap` + - renamed method `getEntityClassName` in favour of `getEntityClassNames` +- `Oro\Bundle\EntityExtendBundle\Migration\ExtendOptionsParser` + - construction signature was changed now it takes next arguments: + `EntityMetadataHelper` $entityMetadataHelper, + `FieldTypeHelper` $fieldTypeHelper, + `ConfigManager` $configManager diff --git a/composer.json b/composer.json index 54f6a149044..d5c7cef31fc 100644 --- a/composer.json +++ b/composer.json @@ -21,7 +21,7 @@ "ext-mbstring": "*", "ext-gd": "*", "ext-xml": "*", - "symfony/symfony": "2.8.*", + "symfony/symfony": "2.8.*, !=2.8.10", "twig/twig": "1.24.*", "doctrine/orm": "2.5.1", "doctrine/doctrine-bundle": "1.6.3", diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 65bf38cd4d5..ae2661b8956 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -37,5 +37,4 @@ - diff --git a/src/Oro/Bundle/ActionBundle/Datagrid/Extension/DeleteMassActionExtension.php b/src/Oro/Bundle/ActionBundle/Datagrid/Extension/DeleteMassActionExtension.php index 75b648ddf35..3d391168fab 100644 --- a/src/Oro/Bundle/ActionBundle/Datagrid/Extension/DeleteMassActionExtension.php +++ b/src/Oro/Bundle/ActionBundle/Datagrid/Extension/DeleteMassActionExtension.php @@ -96,7 +96,7 @@ protected function getDatagridContext(DatagridConfiguration $config) { return [ ContextHelper::ENTITY_CLASS_PARAM => $this->gridConfigurationHelper->getEntity($config), - ContextHelper::DATAGRID_PARAM => $config->offsetGetByPath('[name]'), + ContextHelper::DATAGRID_PARAM => $config->getName(), ContextHelper::GROUP_PARAM => $this->groups, ]; } diff --git a/src/Oro/Bundle/ActionBundle/Resources/config/services.yml b/src/Oro/Bundle/ActionBundle/Resources/config/services.yml index 2ccadd6ef25..ff4e1f096fe 100644 --- a/src/Oro/Bundle/ActionBundle/Resources/config/services.yml +++ b/src/Oro/Bundle/ActionBundle/Resources/config/services.yml @@ -162,7 +162,6 @@ services: - [setGroups, [['', 'datagridRowAction']]] tags: - { name: oro_datagrid.extension } - lazy: true oro_action.datagrid.action.action_widget_action: class: %oro_action.datagrid.action.action_widget_action.class% diff --git a/src/Oro/Bundle/ActivityBundle/composer.json b/src/Oro/Bundle/ActivityBundle/composer.json index 956744dee36..de6d0386911 100644 --- a/src/Oro/Bundle/ActivityBundle/composer.json +++ b/src/Oro/Bundle/ActivityBundle/composer.json @@ -6,7 +6,7 @@ "license": "MIT", "require": { "php": ">=5.5.9", - "symfony/symfony": "2.8.*", + "symfony/symfony": "2.8.*, !=2.8.10", "oro/ui-bundle": "dev-master", "oro/entity-config-bundle": "dev-master", "oro/entity-extend-bundle": "dev-master", diff --git a/src/Oro/Bundle/ActivityListBundle/Entity/Manager/ActivityListManager.php b/src/Oro/Bundle/ActivityListBundle/Entity/Manager/ActivityListManager.php index d68f1951707..02a14c2aba3 100644 --- a/src/Oro/Bundle/ActivityListBundle/Entity/Manager/ActivityListManager.php +++ b/src/Oro/Bundle/ActivityListBundle/Entity/Manager/ActivityListManager.php @@ -255,7 +255,8 @@ protected function getListDataIdsForInheritances(QueryBuilder $qb, $entityClass, $inheritanceQb, $inheritanceTarget, $key, - ':entityId' + ':entityId', + $this->config->get('oro_activity_list.grouping') ); $this->activityListFilterHelper->addFiltersToQuery($inheritanceQb, $filter); diff --git a/src/Oro/Bundle/ActivityListBundle/Helper/ActivityInheritanceTargetsHelper.php b/src/Oro/Bundle/ActivityListBundle/Helper/ActivityInheritanceTargetsHelper.php index 49196f19d63..2997984ba38 100644 --- a/src/Oro/Bundle/ActivityListBundle/Helper/ActivityInheritanceTargetsHelper.php +++ b/src/Oro/Bundle/ActivityListBundle/Helper/ActivityInheritanceTargetsHelper.php @@ -54,8 +54,9 @@ public function hasInheritances($entityClass) * @param array $inheritanceTarget * @param string $aliasSuffix * @param string $entityIdExpr + * @param bool $head Head activity only */ - public function applyInheritanceActivity(QueryBuilder $qb, $inheritanceTarget, $aliasSuffix, $entityIdExpr) + public function applyInheritanceActivity(QueryBuilder $qb, $inheritanceTarget, $aliasSuffix, $entityIdExpr, $head) { $alias = 'ta_' . $aliasSuffix; $qb->leftJoin('activity.' . $inheritanceTarget['targetClassAlias'], $alias); @@ -68,6 +69,9 @@ public function applyInheritanceActivity(QueryBuilder $qb, $inheritanceTarget, $ $aliasSuffix )->getDQL() )); + if ($head) { + $qb->andWhere($qb->expr()->andX('activity.head = true')); + } } /** diff --git a/src/Oro/Bundle/ActivityListBundle/Model/ActivityListProviderInterface.php b/src/Oro/Bundle/ActivityListBundle/Model/ActivityListProviderInterface.php index 0cba3721920..63aa95ddea7 100644 --- a/src/Oro/Bundle/ActivityListBundle/Model/ActivityListProviderInterface.php +++ b/src/Oro/Bundle/ActivityListBundle/Model/ActivityListProviderInterface.php @@ -30,6 +30,8 @@ public function isApplicableTarget($entityClass, $accessible = true); public function getSubject($entity); /** + * Return text representation. Should be a plain text. + * * @param object $entity * * @return string|null diff --git a/src/Oro/Bundle/ActivityListBundle/Provider/ActivityListChainProvider.php b/src/Oro/Bundle/ActivityListBundle/Provider/ActivityListChainProvider.php index 661c1d9d9b6..178697a1dee 100644 --- a/src/Oro/Bundle/ActivityListBundle/Provider/ActivityListChainProvider.php +++ b/src/Oro/Bundle/ActivityListBundle/Provider/ActivityListChainProvider.php @@ -417,8 +417,7 @@ protected function getActivityListEntityForEntity( } $list->setSubject($provider->getSubject($entity)); - //do not use htmlTagHelper->purify - it leads to a huge performance degradation - $list->setDescription(trim(strip_tags($provider->getDescription($entity)))); + $list->setDescription($provider->getDescription($entity)); $this->setDate($entity, $provider, $list); $list->setOwner($provider->getOwner($entity)); if ($provider instanceof ActivityListUpdatedByProviderInterface) { diff --git a/src/Oro/Bundle/ActivityListBundle/Tests/Unit/Helper/ActivityInheritanceTargetsHelperTest.php b/src/Oro/Bundle/ActivityListBundle/Tests/Unit/Helper/ActivityInheritanceTargetsHelperTest.php index c2da6c4106f..719db4db9cb 100644 --- a/src/Oro/Bundle/ActivityListBundle/Tests/Unit/Helper/ActivityInheritanceTargetsHelperTest.php +++ b/src/Oro/Bundle/ActivityListBundle/Tests/Unit/Helper/ActivityInheritanceTargetsHelperTest.php @@ -113,25 +113,35 @@ public function testHasInheritancesConfigured() public function testApplyInheritanceActivity() { - $em = $this->getMockBuilder('Doctrine\ORM\EntityManager') - ->disableOriginalConstructor() - ->getMock(); - $expr = new Expr(); - $em->expects($this->any())->method('getExpressionBuilder')->willReturn($expr); + $mainQb = $this->prepareMock(); + $this->activityInheritanceTargetsHelper->applyInheritanceActivity( + $mainQb, + [ + 'targetClass' => 'Acme\Bundle\AcmeBundle\Entity\Contact', + 'targetClassAlias' => 'contact_e8d5b2ba', + 'path' => [ + 'accounts' + ] + ], + 0, + ':entityId', + false + ); - $mainQb = new QueryBuilder($em); - $inheritedQb = clone $mainQb; + $expectedDQL = 'SELECT activity.id, activity.updatedAt ' + . 'FROM ActivityList activity ' + . 'LEFT JOIN activity.contact_e8d5b2ba ta_0 ' + . 'WHERE ta_0.id IN(SELECT inherit_0.id' + . ' FROM Acme\Bundle\AcmeBundle\Entity\Contact inherit_0' + . ' INNER JOIN inherit_0.accounts t_0_0 WHERE t_0_0.id = :entityId)'; - $mainQb->select('activity.id, activity.updatedAt'); - $mainQb->from('ActivityList', 'activity'); + $this->assertSame($expectedDQL, $mainQb->getDQL()); + } - $em->expects($this->any()) - ->method('createQueryBuilder') - ->willReturn($inheritedQb); - $this->registry->expects($this->any()) - ->method('getManagerForClass') - ->willReturn($em); + public function testApplyInheritanceActivityHeadOnly() + { + $mainQb = $this->prepareMock(); $this->activityInheritanceTargetsHelper->applyInheritanceActivity( $mainQb, @@ -143,7 +153,8 @@ public function testApplyInheritanceActivity() ] ], 0, - ':entityId' + ':entityId', + true ); $expectedDQL = 'SELECT activity.id, activity.updatedAt ' @@ -151,8 +162,35 @@ public function testApplyInheritanceActivity() . 'LEFT JOIN activity.contact_e8d5b2ba ta_0 ' . 'WHERE ta_0.id IN(SELECT inherit_0.id' . ' FROM Acme\Bundle\AcmeBundle\Entity\Contact inherit_0' - . ' INNER JOIN inherit_0.accounts t_0_0 WHERE t_0_0.id = :entityId)'; + . ' INNER JOIN inherit_0.accounts t_0_0 WHERE t_0_0.id = :entityId) AND activity.head = true'; $this->assertSame($expectedDQL, $mainQb->getDQL()); } + + /** + * @return QueryBuilder + */ + protected function prepareMock() + { + $em = $this->getMockBuilder('Doctrine\ORM\EntityManager') + ->disableOriginalConstructor() + ->getMock(); + $expr = new Expr(); + $em->expects($this->any())->method('getExpressionBuilder')->willReturn($expr); + + $mainQb = new QueryBuilder($em); + $inheritedQb = clone $mainQb; + + $mainQb->select('activity.id, activity.updatedAt'); + $mainQb->from('ActivityList', 'activity'); + + $em->expects($this->any()) + ->method('createQueryBuilder') + ->willReturn($inheritedQb); + $this->registry->expects($this->any()) + ->method('getManagerForClass') + ->willReturn($em); + + return $mainQb; + } } diff --git a/src/Oro/Bundle/ActivityListBundle/composer.json b/src/Oro/Bundle/ActivityListBundle/composer.json index 37bc6faa7be..53093d1a258 100644 --- a/src/Oro/Bundle/ActivityListBundle/composer.json +++ b/src/Oro/Bundle/ActivityListBundle/composer.json @@ -6,7 +6,7 @@ "license": "MIT", "require": { "php": ">=5.5.9", - "symfony/symfony": "2.8.*", + "symfony/symfony": "2.8.*, !=2.8.10", "oro/ui-bundle": "dev-master", "oro/organization-bundle": "dev-master", "oro/entity-config-bundle": "dev-master", diff --git a/src/Oro/Bundle/AddressBundle/composer.json b/src/Oro/Bundle/AddressBundle/composer.json index 0df57c1b249..618653b7298 100644 --- a/src/Oro/Bundle/AddressBundle/composer.json +++ b/src/Oro/Bundle/AddressBundle/composer.json @@ -7,7 +7,7 @@ "license": "MIT", "require": { "php": ">=5.5.9", - "symfony/symfony": "2.8.*", + "symfony/symfony": "2.8.*, !=2.8.10", "doctrine/orm": ">=2.2.3,<2.5-dev", "doctrine/doctrine-bundle": "1.1.*", "friendsofsymfony/rest-bundle": "1.5.0-RC2", diff --git a/src/Oro/Bundle/ApiBundle/DependencyInjection/Compiler/ConfigurationCompilerPass.php b/src/Oro/Bundle/ApiBundle/DependencyInjection/Compiler/ConfigurationCompilerPass.php index bd5a8b1de67..39c3fef91ab 100644 --- a/src/Oro/Bundle/ApiBundle/DependencyInjection/Compiler/ConfigurationCompilerPass.php +++ b/src/Oro/Bundle/ApiBundle/DependencyInjection/Compiler/ConfigurationCompilerPass.php @@ -297,7 +297,7 @@ protected function getApiFormTypeGuessers(ContainerBuilder $container) public function getTagKeyForExtension() { return method_exists('Symfony\Component\Form\AbstractType', 'getBlockPrefix') - ? 'extended-type' + ? 'extended_type' : 'alias'; } } diff --git a/src/Oro/Bundle/ApiBundle/DependencyInjection/Compiler/ConstraintTextExtractorConfigurationCompilerPass.php b/src/Oro/Bundle/ApiBundle/DependencyInjection/Compiler/ConstraintTextExtractorConfigurationCompilerPass.php new file mode 100644 index 00000000000..b5d505dc0e2 --- /dev/null +++ b/src/Oro/Bundle/ApiBundle/DependencyInjection/Compiler/ConstraintTextExtractorConfigurationCompilerPass.php @@ -0,0 +1,27 @@ +setStatusCode(Response::HTTP_BAD_REQUEST); + return self::create($title, $detail)->setStatusCode($statusCode); } /** diff --git a/src/Oro/Bundle/ApiBundle/OroApiBundle.php b/src/Oro/Bundle/ApiBundle/OroApiBundle.php index 332ed75c4f8..7f4303d00dd 100644 --- a/src/Oro/Bundle/ApiBundle/OroApiBundle.php +++ b/src/Oro/Bundle/ApiBundle/OroApiBundle.php @@ -10,6 +10,7 @@ use Oro\Component\ChainProcessor\DependencyInjection\LoadProcessorsCompilerPass; use Oro\Bundle\ApiBundle\DependencyInjection\Compiler\ApiDocConfigurationCompilerPass; use Oro\Bundle\ApiBundle\DependencyInjection\Compiler\ConfigurationCompilerPass; +use Oro\Bundle\ApiBundle\DependencyInjection\Compiler\ConstraintTextExtractorConfigurationCompilerPass; use Oro\Bundle\ApiBundle\DependencyInjection\Compiler\DataTransformerConfigurationCompilerPass; use Oro\Bundle\ApiBundle\DependencyInjection\Compiler\EntityAliasesConfigurationCompilerPass; use Oro\Bundle\ApiBundle\DependencyInjection\Compiler\ExceptionTextExtractorConfigurationCompilerPass; @@ -29,6 +30,7 @@ public function build(ContainerBuilder $container) $container->addCompilerPass(new EntityAliasesConfigurationCompilerPass()); $container->addCompilerPass(new ExclusionProviderConfigurationCompilerPass()); $container->addCompilerPass(new ExceptionTextExtractorConfigurationCompilerPass()); + $container->addCompilerPass(new ConstraintTextExtractorConfigurationCompilerPass()); $container->addCompilerPass( new LoadProcessorsCompilerPass( 'oro_api.processor_bag', diff --git a/src/Oro/Bundle/ApiBundle/Processor/Shared/CollectFormErrors.php b/src/Oro/Bundle/ApiBundle/Processor/Shared/CollectFormErrors.php index b7afe8410c4..d32abfe6e61 100644 --- a/src/Oro/Bundle/ApiBundle/Processor/Shared/CollectFormErrors.php +++ b/src/Oro/Bundle/ApiBundle/Processor/Shared/CollectFormErrors.php @@ -10,17 +10,27 @@ use Oro\Component\ChainProcessor\ContextInterface; use Oro\Component\ChainProcessor\ProcessorInterface; use Oro\Bundle\ApiBundle\Processor\FormContext; +use Oro\Bundle\ApiBundle\Request\ConstraintTextExtractorInterface; use Oro\Bundle\ApiBundle\Model\Error; use Oro\Bundle\ApiBundle\Model\ErrorSource; use Oro\Bundle\ApiBundle\Request\Constraint; -use Oro\Bundle\ApiBundle\Validator\Constraints\ConstraintWithStatusCodeInterface; -use Oro\Bundle\ApiBundle\Util\ValueNormalizerUtil; /** * Collects errors occurred during the the form submit and adds them into the Context. */ class CollectFormErrors implements ProcessorInterface { + /** @var ConstraintTextExtractorInterface */ + protected $constraintTextExtractor; + + /** + * @param ConstraintTextExtractorInterface $constraintTextExtractor + */ + public function __construct(ConstraintTextExtractorInterface $constraintTextExtractor) + { + $this->constraintTextExtractor = $constraintTextExtractor; + } + /** * {@inheritdoc} */ @@ -176,7 +186,7 @@ protected function getFormErrorTitle(FormError $formError) return Constraint::EXTRA_FIELDS; } - return ValueNormalizerUtil::humanizeClassName(get_class($cause->getConstraint()), 'Constraint'); + return $this->constraintTextExtractor->getConstraintType($cause->getConstraint()); } // undefined constraint type @@ -193,9 +203,7 @@ protected function getFormErrorStatusCode(FormError $formError) $cause = $formError->getCause(); if ($cause instanceof ConstraintViolation) { $constraint = $cause->getConstraint(); - if ($constraint instanceof ConstraintWithStatusCodeInterface) { - return $constraint->getStatusCode(); - } + return $this->constraintTextExtractor->getConstraintStatusCode($constraint); } return null; diff --git a/src/Oro/Bundle/ApiBundle/Processor/Shared/JsonApi/ValidateRequestData.php b/src/Oro/Bundle/ApiBundle/Processor/Shared/JsonApi/ValidateRequestData.php index ef16a039571..14214c25454 100644 --- a/src/Oro/Bundle/ApiBundle/Processor/Shared/JsonApi/ValidateRequestData.php +++ b/src/Oro/Bundle/ApiBundle/Processor/Shared/JsonApi/ValidateRequestData.php @@ -2,6 +2,8 @@ namespace Oro\Bundle\ApiBundle\Processor\Shared\JsonApi; +use Symfony\Component\HttpFoundation\Response; + use Oro\Component\ChainProcessor\ContextInterface; use Oro\Component\ChainProcessor\ProcessorInterface; use Oro\Component\PhpUtils\ArrayUtil; @@ -288,10 +290,11 @@ protected function buildPointer($parentPath, $property) /** * @param string $pointer * @param string $message + * @param integer|null $statusCode */ - protected function addError($pointer, $message) + protected function addError($pointer, $message, $statusCode = Response::HTTP_BAD_REQUEST) { - $error = Error::createValidationError(Constraint::REQUEST_DATA, $message) + $error = Error::createValidationError(Constraint::REQUEST_DATA, $message, $statusCode) ->setSource(ErrorSource::createByPointer($pointer)); $this->context->addError($error); diff --git a/src/Oro/Bundle/ApiBundle/Processor/Update/JsonApi/ValidateRequestData.php b/src/Oro/Bundle/ApiBundle/Processor/Update/JsonApi/ValidateRequestData.php index 7065522497a..86679a943d8 100644 --- a/src/Oro/Bundle/ApiBundle/Processor/Update/JsonApi/ValidateRequestData.php +++ b/src/Oro/Bundle/ApiBundle/Processor/Update/JsonApi/ValidateRequestData.php @@ -4,6 +4,7 @@ use Oro\Bundle\ApiBundle\Processor\Shared\JsonApi\ValidateRequestData as BaseProcessor; use Oro\Bundle\ApiBundle\Request\JsonApi\JsonApiDocumentBuilder as JsonApiDoc; +use Symfony\Component\HttpFoundation\Response; /** * Validates that the request data contains valid JSON.API object. diff --git a/src/Oro/Bundle/ApiBundle/Request/ChainConstraintTextExtractor.php b/src/Oro/Bundle/ApiBundle/Request/ChainConstraintTextExtractor.php new file mode 100644 index 00000000000..c4268a95f92 --- /dev/null +++ b/src/Oro/Bundle/ApiBundle/Request/ChainConstraintTextExtractor.php @@ -0,0 +1,71 @@ +extractors[] = $extractor; + } + + /** + * {@inheritdoc} + */ + public function getConstraintStatusCode(Validator\Constraint $constraint) + { + $result = null; + foreach ($this->extractors as $extractor) { + $result = $extractor->getConstraintStatusCode($constraint); + if (null !== $result) { + break; + } + } + + return $result; + } + + /** + * {@inheritdoc} + */ + public function getConstraintCode(Validator\Constraint $constraint) + { + $result = null; + foreach ($this->extractors as $extractor) { + $result = $extractor->getConstraintCode($constraint); + if (null !== $result) { + break; + } + } + + return $result; + } + + /** + * {@inheritdoc} + */ + public function getConstraintType(Validator\Constraint $constraint) + { + $result = null; + foreach ($this->extractors as $extractor) { + $result = $extractor->getConstraintType($constraint); + if (null !== $result) { + break; + } + } + + return $result; + } +} diff --git a/src/Oro/Bundle/ApiBundle/Request/ConstraintTextExtractor.php b/src/Oro/Bundle/ApiBundle/Request/ConstraintTextExtractor.php new file mode 100644 index 00000000000..b5a18922f5e --- /dev/null +++ b/src/Oro/Bundle/ApiBundle/Request/ConstraintTextExtractor.php @@ -0,0 +1,40 @@ +getStatusCode(); + } + + return Response::HTTP_BAD_REQUEST; + } + + /** + * {@inheritdoc} + */ + public function getConstraintCode(Validator\Constraint $constraint) + { + return null; + } + + /** + * {@inheritdoc} + */ + public function getConstraintType(Validator\Constraint $constraint) + { + return ValueNormalizerUtil::humanizeClassName(get_class($constraint), 'Constraint'); + } +} diff --git a/src/Oro/Bundle/ApiBundle/Request/ConstraintTextExtractorInterface.php b/src/Oro/Bundle/ApiBundle/Request/ConstraintTextExtractorInterface.php new file mode 100644 index 00000000000..9b6d8d3bedd --- /dev/null +++ b/src/Oro/Bundle/ApiBundle/Request/ConstraintTextExtractorInterface.php @@ -0,0 +1,35 @@ +context->setClassName('Oro\Bundle\ApiBundle\Tests\Unit\Fixtures\Entity\Product'); $this->context->setRequestData($requestData); @@ -77,7 +77,7 @@ public function testProcessWithInvalidRequestData($requestData, $expectedErrorSt $errors = $this->context->getErrors(); $this->assertCount(1, $errors); $error = $errors[0]; - $this->assertEquals(400, $error->getStatusCode()); + $this->assertEquals($expectedCode, $error->getStatusCode()); $this->assertEquals('request data constraint', $error->getTitle()); $this->assertEquals($expectedErrorString, $error->getDetail()); $this->assertEquals($pointer, $error->getSource()->getPointer()); diff --git a/src/Oro/Bundle/ApiBundle/Tests/Unit/Processor/Shared/CollectFormErrorsTest.php b/src/Oro/Bundle/ApiBundle/Tests/Unit/Processor/Shared/CollectFormErrorsTest.php index f6563fb032d..27a50a56893 100644 --- a/src/Oro/Bundle/ApiBundle/Tests/Unit/Processor/Shared/CollectFormErrorsTest.php +++ b/src/Oro/Bundle/ApiBundle/Tests/Unit/Processor/Shared/CollectFormErrorsTest.php @@ -8,6 +8,7 @@ use Oro\Bundle\ApiBundle\Model\Error; use Oro\Bundle\ApiBundle\Model\ErrorSource; use Oro\Bundle\ApiBundle\Processor\Shared\CollectFormErrors; +use Oro\Bundle\ApiBundle\Request\ConstraintTextExtractor; use Oro\Bundle\ApiBundle\Tests\Unit\Fixtures\FormType\NameValuePairType; use Oro\Bundle\ApiBundle\Tests\Unit\Processor\FormProcessorTestCase; @@ -23,7 +24,7 @@ public function setUp() { parent::setUp(); - $this->processor = new CollectFormErrors(); + $this->processor = new CollectFormErrors(new ConstraintTextExtractor()); } public function testProcessWithoutForm() diff --git a/src/Oro/Bundle/ApiBundle/Tests/Unit/Processor/Subresource/Shared/CollectFormErrorsTest.php b/src/Oro/Bundle/ApiBundle/Tests/Unit/Processor/Subresource/Shared/CollectFormErrorsTest.php index f1c33cf3090..048f9bc9e61 100644 --- a/src/Oro/Bundle/ApiBundle/Tests/Unit/Processor/Subresource/Shared/CollectFormErrorsTest.php +++ b/src/Oro/Bundle/ApiBundle/Tests/Unit/Processor/Subresource/Shared/CollectFormErrorsTest.php @@ -2,6 +2,7 @@ namespace Oro\Bundle\ApiBundle\Tests\Unit\Processor\Subresource\Shared; +use Oro\Bundle\ApiBundle\Request\ConstraintTextExtractor; use Symfony\Component\Validator\Constraints; use Oro\Bundle\ApiBundle\Model\Error; @@ -21,7 +22,7 @@ public function setUp() { parent::setUp(); - $this->processor = new CollectFormErrors(); + $this->processor = new CollectFormErrors(new ConstraintTextExtractor()); } public function testErrorPropertyPathShouldBeEmptyStringForToOneAssociationRelatedError() diff --git a/src/Oro/Bundle/ApiBundle/Tests/Unit/Request/ChainConstraintTextExtractorTest.php b/src/Oro/Bundle/ApiBundle/Tests/Unit/Request/ChainConstraintTextExtractorTest.php new file mode 100644 index 00000000000..39a336bea66 --- /dev/null +++ b/src/Oro/Bundle/ApiBundle/Tests/Unit/Request/ChainConstraintTextExtractorTest.php @@ -0,0 +1,166 @@ +extractor = new ChainConstraintTextExtractor(); + + $firstExtractor = $this->getMock('Oro\Bundle\ApiBundle\Request\ConstraintTextExtractorInterface'); + $secondExtractor = $this->getMock('Oro\Bundle\ApiBundle\Request\ConstraintTextExtractorInterface'); + + $this->extractor->addExtractor($firstExtractor); + $this->extractor->addExtractor($secondExtractor); + + $this->extractors = [$firstExtractor, $secondExtractor]; + } + + public function testGetConstraintStatusCodeByFirstExtractor() + { + $constraint = new AccessGranted(); + + $this->extractors[0]->expects($this->once()) + ->method('getConstraintStatusCode') + ->with($this->identicalTo($constraint)) + ->willReturn(400); + $this->extractors[1]->expects($this->never()) + ->method('getConstraintStatusCode'); + + $this->assertEquals(400, $this->extractor->getConstraintStatusCode($constraint)); + } + + public function testGetConstraintStatusCodeBySecondExtractor() + { + $constraint = new AccessGranted(); + + $this->extractors[0]->expects($this->once()) + ->method('getConstraintStatusCode') + ->with($this->identicalTo($constraint)) + ->willReturn(null); + $this->extractors[1]->expects($this->once()) + ->method('getConstraintStatusCode') + ->with($this->identicalTo($constraint)) + ->willReturn(401); + + $this->assertEquals(401, $this->extractor->getConstraintStatusCode($constraint)); + } + + public function testGetConstraintStatusCodeWithNullResult() + { + $constraint = new AccessGranted(); + + $this->extractors[0]->expects($this->once()) + ->method('getConstraintStatusCode') + ->with($this->identicalTo($constraint)) + ->willReturn(null); + $this->extractors[1]->expects($this->once()) + ->method('getConstraintStatusCode') + ->with($this->identicalTo($constraint)) + ->willReturn(null); + + $this->assertNull($this->extractor->getConstraintStatusCode($constraint)); + } + + public function testGetConstraintCodeByFirstExtractor() + { + $constraint = new AccessGranted(); + + $this->extractors[0]->expects($this->once()) + ->method('getConstraintCode') + ->with($this->identicalTo($constraint)) + ->willReturn(645); + $this->extractors[1]->expects($this->never()) + ->method('getConstraintCode'); + + $this->assertEquals(645, $this->extractor->getConstraintCode($constraint)); + } + + public function testGetConstraintCodeBySecondExtractor() + { + $constraint = new AccessGranted(); + + $this->extractors[0]->expects($this->once()) + ->method('getConstraintCode') + ->with($this->identicalTo($constraint)) + ->willReturn(null); + $this->extractors[1]->expects($this->once()) + ->method('getConstraintCode') + ->with($this->identicalTo($constraint)) + ->willReturn(8456); + + $this->assertEquals(8456, $this->extractor->getConstraintCode($constraint)); + } + + public function testGetConstraintCodeWithNullResult() + { + $constraint = new AccessGranted(); + + $this->extractors[0]->expects($this->once()) + ->method('getConstraintCode') + ->with($this->identicalTo($constraint)) + ->willReturn(null); + $this->extractors[1]->expects($this->once()) + ->method('getConstraintCode') + ->with($this->identicalTo($constraint)) + ->willReturn(null); + + $this->assertNull($this->extractor->getConstraintCode($constraint)); + } + + public function testGetConstraintTypeByFirstExtractor() + { + $constraint = new AccessGranted(); + + $this->extractors[0]->expects($this->once()) + ->method('getConstraintType') + ->with($this->identicalTo($constraint)) + ->willReturn('first extractor type'); + $this->extractors[1]->expects($this->never()) + ->method('getConstraintType'); + + $this->assertEquals('first extractor type', $this->extractor->getConstraintType($constraint)); + } + + public function testGetConstraintTypeBySecondExtractor() + { + $constraint = new AccessGranted(); + + $this->extractors[0]->expects($this->once()) + ->method('getConstraintType') + ->with($this->identicalTo($constraint)) + ->willReturn(null); + $this->extractors[1]->expects($this->once()) + ->method('getConstraintType') + ->with($this->identicalTo($constraint)) + ->willReturn('second extractor type'); + + $this->assertEquals('second extractor type', $this->extractor->getConstraintType($constraint)); + } + + public function testGetConstraintTypWithNullResult() + { + $constraint = new AccessGranted(); + + $this->extractors[0]->expects($this->once()) + ->method('getConstraintType') + ->with($this->identicalTo($constraint)) + ->willReturn(null); + $this->extractors[1]->expects($this->once()) + ->method('getConstraintType') + ->with($this->identicalTo($constraint)) + ->willReturn(null); + + $this->assertNull($this->extractor->getConstraintType($constraint)); + } +} diff --git a/src/Oro/Bundle/ApiBundle/Tests/Unit/Request/ConstraintTextExtractorTest.php b/src/Oro/Bundle/ApiBundle/Tests/Unit/Request/ConstraintTextExtractorTest.php new file mode 100644 index 00000000000..9b3185a2410 --- /dev/null +++ b/src/Oro/Bundle/ApiBundle/Tests/Unit/Request/ConstraintTextExtractorTest.php @@ -0,0 +1,66 @@ +constraintTextExtractor = new ConstraintTextExtractor(); + } + + /** + * @dataProvider getConstraintStatusCodeDataProvider() + */ + public function testGetConstraintStatusCode(Constraint $constraint, $expectedStatusCode) + { + $this->assertEquals( + $expectedStatusCode, + $this->constraintTextExtractor->getConstraintStatusCode($constraint) + ); + } + + public function getConstraintStatusCodeDataProvider() + { + return [ + [new Blank(), 400], + [new HasAdderAndRemover(['class' => 'Test\Class', 'property' => 'test']), 501], + ]; + } + + public function testGetConstraintCode() + { + $this->assertNull($this->constraintTextExtractor->getConstraintCode(new Blank())); + } + + /** + * @dataProvider getConstraintTypeDataProvider + */ + public function testConstraintType(Constraint $constraint, $expectedType) + { + $this->assertEquals( + $expectedType, + $this->constraintTextExtractor->getConstraintType($constraint) + ); + } + + public function getConstraintTypeDataProvider() + { + return [ + [new Blank(), 'blank constraint'], + [ + new HasAdderAndRemover(['class' => 'Test\Class', 'property' => 'test']), + 'has adder and remover constraint' + ], + ]; + } +} diff --git a/src/Oro/Bundle/AsseticBundle/composer.json b/src/Oro/Bundle/AsseticBundle/composer.json index ccecf4c0dc3..5487c91d84f 100644 --- a/src/Oro/Bundle/AsseticBundle/composer.json +++ b/src/Oro/Bundle/AsseticBundle/composer.json @@ -7,7 +7,7 @@ "license": "MIT", "require": { "php": ">=5.5.9", - "symfony/symfony": "2.8.*", + "symfony/symfony": "2.8.*, !=2.8.10", "kriswallsmith/assetic": "1.1.*@dev", "oro/config": "dev-master" }, diff --git a/src/Oro/Bundle/AttachmentBundle/composer.json b/src/Oro/Bundle/AttachmentBundle/composer.json index 81bd39d39b3..bbd68a4dac0 100644 --- a/src/Oro/Bundle/AttachmentBundle/composer.json +++ b/src/Oro/Bundle/AttachmentBundle/composer.json @@ -6,7 +6,7 @@ "license": "MIT", "require": { "php": ">=5.5.9", - "symfony/symfony": "2.8.*", + "symfony/symfony": "2.8.*, !=2.8.10", "liip/imagine-bundle": "dev-master", "knplabs/knp-gaufrette-bundle": "dev-master" }, diff --git a/src/Oro/Bundle/BatchBundle/ORM/QueryBuilder/CountQueryBuilderOptimizer.php b/src/Oro/Bundle/BatchBundle/ORM/QueryBuilder/CountQueryBuilderOptimizer.php index 7a7866f76bb..56cb89ed8eb 100644 --- a/src/Oro/Bundle/BatchBundle/ORM/QueryBuilder/CountQueryBuilderOptimizer.php +++ b/src/Oro/Bundle/BatchBundle/ORM/QueryBuilder/CountQueryBuilderOptimizer.php @@ -110,12 +110,12 @@ protected function buildCountQueryBuilder() ); } - if ($originalQueryParts['join']) { - $this->addJoins($optimizedQueryBuilder, $originalQueryParts); - } if (!$originalQueryParts['groupBy']) { $fieldsToSelect = $this->getFieldsToSelect($originalQueryParts); } + if ($originalQueryParts['join']) { + $this->addJoins($optimizedQueryBuilder, $originalQueryParts, $this->useNonSymmetricJoins($fieldsToSelect)); + } if ($originalQueryParts['where']) { $optimizedQueryBuilder->where( @@ -129,14 +129,30 @@ protected function buildCountQueryBuilder() return $optimizedQueryBuilder; } + /** + * Method to check if using of non symmetric joins is required (if they will affect number of rows or not). + * + * @param array $fieldsToSelect + * + * @return bool + */ + protected function useNonSymmetricJoins(array $fieldsToSelect) + { + return count($fieldsToSelect) !== 1 || stripos(reset($fieldsToSelect), 'DISTINCT(') !== 0; + } + /** * Add required JOINs to resulting Query Builder. * * @param QueryBuilder $optimizedQueryBuilder * @param array $originalQueryParts + * @param bool $useNonSymmetricJoins */ - protected function addJoins(QueryBuilder $optimizedQueryBuilder, array $originalQueryParts) - { + protected function addJoins( + QueryBuilder $optimizedQueryBuilder, + array $originalQueryParts, + $useNonSymmetricJoins = true + ) { // Collect list of tables which should be added to new query $whereAliases = $this->qbTools->getUsedTableAliases($originalQueryParts['where']); $groupByAliases = $this->qbTools->getUsedTableAliases($originalQueryParts['groupBy']); @@ -147,14 +163,16 @@ protected function addJoins(QueryBuilder $optimizedQueryBuilder, array $original // this joins cannot be removed outside of this class $requiredJoinAliases = $joinAliases; - $joinAliases = array_merge( - $joinAliases, - $this->getNonSymmetricJoinAliases( - $originalQueryParts['from'], - $originalQueryParts['join'], - $groupByAliases - ) - ); + if ($useNonSymmetricJoins) { + $joinAliases = array_merge( + $joinAliases, + $this->getNonSymmetricJoinAliases( + $originalQueryParts['from'], + $originalQueryParts['join'], + $groupByAliases + ) + ); + } $rootAliases = []; /** @var Expr\From $from */ diff --git a/src/Oro/Bundle/BatchBundle/composer.json b/src/Oro/Bundle/BatchBundle/composer.json index d4dbed4dfc2..c70a30d904e 100644 --- a/src/Oro/Bundle/BatchBundle/composer.json +++ b/src/Oro/Bundle/BatchBundle/composer.json @@ -6,7 +6,7 @@ "license": "MIT", "require": { "php": ">=5.5.9", - "symfony/symfony": "2.8.*", + "symfony/symfony": "2.8.*, !=2.8.10", "oro/config": "dev-master", "oro/form-bundle": "dev-master" }, diff --git a/src/Oro/Bundle/BusinessEntitiesBundle/composer.json b/src/Oro/Bundle/BusinessEntitiesBundle/composer.json index b0cd8485db1..8dc9aec8b9a 100644 --- a/src/Oro/Bundle/BusinessEntitiesBundle/composer.json +++ b/src/Oro/Bundle/BusinessEntitiesBundle/composer.json @@ -6,7 +6,7 @@ "license": "MIT", "require": { "php": ">=5.5.9", - "symfony/symfony": "2.8.*", + "symfony/symfony": "2.8.*, !=2.8.10", "doctrine/orm": ">=2.3,<2.4-dev" }, "autoload": { diff --git a/src/Oro/Bundle/CacheBundle/composer.json b/src/Oro/Bundle/CacheBundle/composer.json index 239cd72b4d3..c32b23ba077 100644 --- a/src/Oro/Bundle/CacheBundle/composer.json +++ b/src/Oro/Bundle/CacheBundle/composer.json @@ -6,7 +6,7 @@ "license": "MIT", "require": { "php": ">=5.5.9", - "symfony/symfony": "2.8.*" + "symfony/symfony": "2.8.*, !=2.8.10" }, "autoload": { "psr-0": { "Oro\\Bundle\\CacheBundle": "" } diff --git a/src/Oro/Bundle/CalendarBundle/Model/Recurrence/YearNthStrategy.php b/src/Oro/Bundle/CalendarBundle/Model/Recurrence/YearNthStrategy.php index 043563c164b..7b546599bda 100644 --- a/src/Oro/Bundle/CalendarBundle/Model/Recurrence/YearNthStrategy.php +++ b/src/Oro/Bundle/CalendarBundle/Model/Recurrence/YearNthStrategy.php @@ -117,7 +117,8 @@ protected function getNextOccurrence($interval, $dayOfWeek, $monthOfYear, $insta $occurrenceDate = new \DateTime("+{$interval} month {$date->format('c')}"); $instanceRelativeValue = $this->getInstanceRelativeValue($instance); - $month = date('M', mktime(0, 0, 0, $monthOfYear)); + //the 1st day is used to avoid situations like '31-06-2016' == '01-07-2016' (31 == current day by default) + $month = date('M', mktime(0, 0, 0, $monthOfYear, 1)); $year = $occurrenceDate->format('Y'); $time = $occurrenceDate->format('H:i:s.u'); $nextDays = []; diff --git a/src/Oro/Bundle/CalendarBundle/Provider/CalendarEventActivityListProvider.php b/src/Oro/Bundle/CalendarBundle/Provider/CalendarEventActivityListProvider.php index 5d604142c64..4229c923425 100644 --- a/src/Oro/Bundle/CalendarBundle/Provider/CalendarEventActivityListProvider.php +++ b/src/Oro/Bundle/CalendarBundle/Provider/CalendarEventActivityListProvider.php @@ -100,7 +100,7 @@ public function getSubject($entity) public function getDescription($entity) { /** @var $entity CalendarEvent */ - return $entity->getDescription(); + return trim(strip_tags($entity->getDescription())); } /** diff --git a/src/Oro/Bundle/CalendarBundle/Resources/config/requirejs.yml b/src/Oro/Bundle/CalendarBundle/Resources/config/requirejs.yml index f2a96e87936..b155d7194a4 100644 --- a/src/Oro/Bundle/CalendarBundle/Resources/config/requirejs.yml +++ b/src/Oro/Bundle/CalendarBundle/Resources/config/requirejs.yml @@ -1,11 +1,6 @@ config: - shim: - 'jquery.fullcalendar': - deps: - - 'jquery' - - 'moment' paths: - 'jquery.fullcalendar': 'bundles/orocalendar/lib/fullcalendar/fullcalendar.js' + 'fullcalendar': 'bundles/orocalendar/lib/fullcalendar/fullcalendar.js' 'orocalendar/js/calendar-view': 'bundles/orocalendar/js/calendar-view.js' 'orocalendar/js/calendar/connection/collection': 'bundles/orocalendar/js/calendar/connection/collection.js' 'orocalendar/js/calendar/connection/model': 'bundles/orocalendar/js/calendar/connection/model.js' diff --git a/src/Oro/Bundle/CalendarBundle/Resources/public/js/calendar-view.js b/src/Oro/Bundle/CalendarBundle/Resources/public/js/calendar-view.js index f5836b8ee52..a307b1cd779 100644 --- a/src/Oro/Bundle/CalendarBundle/Resources/public/js/calendar-view.js +++ b/src/Oro/Bundle/CalendarBundle/Resources/public/js/calendar-view.js @@ -23,7 +23,7 @@ define(function(require) { var PluginManager = require('oroui/js/app/plugins/plugin-manager'); var GuestsPlugin = require('orocalendar/js/app/plugins/calendar/guests-plugin'); var persistentStorage = require('oroui/js/persistent-storage'); - require('jquery.fullcalendar'); + require('fullcalendar'); CalendarView = BaseView.extend({ MOMENT_BACKEND_FORMAT: dateTimeFormatter.getBackendDateTimeFormat(), diff --git a/src/Oro/Bundle/CalendarBundle/composer.json b/src/Oro/Bundle/CalendarBundle/composer.json index d425023e2e7..30f474f2750 100644 --- a/src/Oro/Bundle/CalendarBundle/composer.json +++ b/src/Oro/Bundle/CalendarBundle/composer.json @@ -6,7 +6,7 @@ "license": "MIT", "require": { "php": ">=5.5.9", - "symfony/symfony": "2.8.*", + "symfony/symfony": "2.8.*, !=2.8.10", "oro/ui-bundle": "dev-master", "oro/windows-bundle": "dev-master", "oro/form-bundle": "dev-master", diff --git a/src/Oro/Bundle/ChartBundle/composer.json b/src/Oro/Bundle/ChartBundle/composer.json index 6d0846e1fa7..7a7fb49674b 100644 --- a/src/Oro/Bundle/ChartBundle/composer.json +++ b/src/Oro/Bundle/ChartBundle/composer.json @@ -6,7 +6,7 @@ "license": "MIT", "require": { "php": ">=5.5.9", - "symfony/symfony": "2.8.*", + "symfony/symfony": "2.8.*, !=2.8.10", "oro/ui-bundle": "dev-master", "oro/locale-bundle": "dev-master", "oro/requirejs-bundle": "dev-master", diff --git a/src/Oro/Bundle/CommentBundle/composer.json b/src/Oro/Bundle/CommentBundle/composer.json index c8c6b7e4266..2ced4c211ac 100644 --- a/src/Oro/Bundle/CommentBundle/composer.json +++ b/src/Oro/Bundle/CommentBundle/composer.json @@ -6,7 +6,7 @@ "license": "MIT", "require": { "php": ">=5.5.9", - "symfony/symfony": "2.8.*", + "symfony/symfony": "2.8.*, !=2.8.10", "oro/ui-bundle": "dev-master" }, "autoload": { diff --git a/src/Oro/Bundle/ConfigBundle/composer.json b/src/Oro/Bundle/ConfigBundle/composer.json index dc52962409d..7915da79893 100644 --- a/src/Oro/Bundle/ConfigBundle/composer.json +++ b/src/Oro/Bundle/ConfigBundle/composer.json @@ -7,7 +7,7 @@ "license": "MIT", "require": { "php": ">=5.5.9", - "symfony/symfony": "2.8.*", + "symfony/symfony": "2.8.*, !=2.8.10", "oro/config": "dev-master", "oro/ui-bundle": "dev-master", "oro/user-bundle": "dev-master", diff --git a/src/Oro/Bundle/CronBundle/Entity/Repository/JobRepository.php b/src/Oro/Bundle/CronBundle/Entity/Repository/JobRepository.php new file mode 100644 index 00000000000..0598cb26f0b --- /dev/null +++ b/src/Oro/Bundle/CronBundle/Entity/Repository/JobRepository.php @@ -0,0 +1,67 @@ +getJobIdsOfIncomingDependencies($job); + if (empty($jobIds)) { + return []; + } + $query = "SELECT j, d FROM JMSJobQueueBundle:Job j LEFT JOIN j.dependencies d WHERE j.id IN (:ids)"; + return $this->_em->createQuery($query) + ->setParameter('ids', $jobIds) + ->getResult(); + } + + /** + * @param Job $job + * + * @return Job[] + */ + public function getIncomingDependencies(Job $job) + { + $jobIds = $this->getJobIdsOfIncomingDependencies($job); + if (empty($jobIds)) { + return []; + } + return $this->_em->createQuery("SELECT j FROM JMSJobQueueBundle:Job j WHERE j.id IN (:ids)") + ->setParameter('ids', $jobIds) + ->getResult(); + } + + /** + * @param Job $job + * + * @return array + */ + protected function getJobIdsOfIncomingDependencies(Job $job) + { + $query = "SELECT source_job_id FROM jms_job_dependencies WHERE dest_job_id = :id"; + $jobIds = $this->_em->getConnection() + ->executeQuery($query, ['id' => $job->getId()]) + ->fetchAll(\PDO::FETCH_COLUMN); + return $jobIds; + } +} diff --git a/src/Oro/Bundle/CronBundle/EventListener/ClassMetadataListener.php b/src/Oro/Bundle/CronBundle/EventListener/ClassMetadataListener.php new file mode 100644 index 00000000000..f07340ef8cd --- /dev/null +++ b/src/Oro/Bundle/CronBundle/EventListener/ClassMetadataListener.php @@ -0,0 +1,31 @@ +getClassMetadata(); + if ('JMS\JobQueueBundle\Entity\Job' === $classMetadata->name) { + $classMetadata->customRepositoryClassName = 'Oro\Bundle\CronBundle\Entity\Repository\JobRepository'; + } + } +} diff --git a/src/Oro/Bundle/CronBundle/Resources/config/services.yml b/src/Oro/Bundle/CronBundle/Resources/config/services.yml index a81e02d8779..f131bf34539 100644 --- a/src/Oro/Bundle/CronBundle/Resources/config/services.yml +++ b/src/Oro/Bundle/CronBundle/Resources/config/services.yml @@ -60,3 +60,16 @@ services: oro_cron.helper.cron: class: 'Oro\Bundle\CronBundle\Helper\CronHelper' + + # @deprecated Since 1.11, will be removed after 1.13. + # @TODO + # Remove this service after BAP-10703 implementation or + # after migration from jms/job-queue-bundle 1.2.* to jms/job-queue-bundle 1.3.* + # + # This fix brings performance optimization of JobRepository which was introduced in + # jms/job-queue-bundle 1.3.0. As of there are other stories to upgrade jms/job-queue-bundle version + # or replace it, this solution is temporary. + oro_cron.event_listener.class_metadata_listener: + class: 'Oro\Bundle\CronBundle\EventListener\ClassMetadataListener' + tags: + - { name: doctrine.event_listener, event: loadClassMetadata } diff --git a/src/Oro/Bundle/CronBundle/composer.json b/src/Oro/Bundle/CronBundle/composer.json index 76ed23e708a..dd9070a54ce 100644 --- a/src/Oro/Bundle/CronBundle/composer.json +++ b/src/Oro/Bundle/CronBundle/composer.json @@ -7,7 +7,7 @@ "license": "MIT", "require": { "php": ">=5.5.9", - "symfony/symfony": "2.8.*", + "symfony/symfony": "2.8.*, !=2.8.10", "jms/job-queue-bundle": "dev-master", "mtdowling/cron-expression": "1.0.*", "oro/log": "dev-master", diff --git a/src/Oro/Bundle/DashboardBundle/composer.json b/src/Oro/Bundle/DashboardBundle/composer.json index 83413305050..8d1e5d00a76 100644 --- a/src/Oro/Bundle/DashboardBundle/composer.json +++ b/src/Oro/Bundle/DashboardBundle/composer.json @@ -6,7 +6,7 @@ "license": "MIT", "require": { "php": ">=5.5.9", - "symfony/symfony": "2.8.*", + "symfony/symfony": "2.8.*, !=2.8.10", "oro/config": "dev-master", "oro/ui-bundle": "dev-master", "oro/requirejs-bundle": "dev-master", diff --git a/src/Oro/Bundle/DataAuditBundle/composer.json b/src/Oro/Bundle/DataAuditBundle/composer.json index 7d5aa67d56b..944df5fd74c 100644 --- a/src/Oro/Bundle/DataAuditBundle/composer.json +++ b/src/Oro/Bundle/DataAuditBundle/composer.json @@ -7,7 +7,7 @@ "license": "MIT", "require": { "php": ">=5.5.9", - "symfony/symfony": "2.8.*", + "symfony/symfony": "2.8.*, !=2.8.10", "stof/doctrine-extensions-bundle": "dev-master", "friendsofsymfony/rest-bundle": "1.5.0-RC2", "nelmio/api-doc-bundle": "dev-master", diff --git a/src/Oro/Bundle/DataGridBundle/Datagrid/Builder.php b/src/Oro/Bundle/DataGridBundle/Datagrid/Builder.php index 5f207f5bfa7..3e4a5deb4f7 100644 --- a/src/Oro/Bundle/DataGridBundle/Datagrid/Builder.php +++ b/src/Oro/Bundle/DataGridBundle/Datagrid/Builder.php @@ -182,6 +182,7 @@ protected function createAcceptor(DatagridConfiguration $config, ParameterBag $p $acceptor->addExtension($extension); } } + $acceptor->sortExtensionsByPriority(); return $acceptor; } diff --git a/src/Oro/Bundle/DataGridBundle/Datagrid/ParameterBag.php b/src/Oro/Bundle/DataGridBundle/Datagrid/ParameterBag.php index bc43182770c..6509489914a 100644 --- a/src/Oro/Bundle/DataGridBundle/Datagrid/ParameterBag.php +++ b/src/Oro/Bundle/DataGridBundle/Datagrid/ParameterBag.php @@ -73,7 +73,7 @@ public function add(array $parameters = array()) */ public function get($key, $default = null) { - return $this->has($key) ? $this->parameters[$key] : $default; + return array_key_exists($key, $this->parameters) ? $this->parameters[$key] : $default; } /** diff --git a/src/Oro/Bundle/DataGridBundle/Event/OrmResultAfter.php b/src/Oro/Bundle/DataGridBundle/Event/OrmResultAfter.php index 4d9c423a433..63b30c1aa17 100644 --- a/src/Oro/Bundle/DataGridBundle/Event/OrmResultAfter.php +++ b/src/Oro/Bundle/DataGridBundle/Event/OrmResultAfter.php @@ -62,6 +62,14 @@ public function getRecords() return $this->records; } + /** + * @param array $records + */ + public function setRecords(array $records) + { + $this->records = $records; + } + /** * @return AbstractQuery */ diff --git a/src/Oro/Bundle/DataGridBundle/Extension/Acceptor.php b/src/Oro/Bundle/DataGridBundle/Extension/Acceptor.php index 29e1f5f21a8..bcab26d94fb 100644 --- a/src/Oro/Bundle/DataGridBundle/Extension/Acceptor.php +++ b/src/Oro/Bundle/DataGridBundle/Extension/Acceptor.php @@ -60,7 +60,7 @@ public function acceptMetadata(MetadataObject $data) } /** - * Add extension that applicable to datagrid and resort all added extensions + * Adds an extension that applicable to datagrid * * @param ExtensionVisitorInterface $extension * @@ -70,6 +70,14 @@ public function addExtension(ExtensionVisitorInterface $extension) { $this->extensions[] = $extension; + return $this; + } + + /** + * Sorts extensions by priority + */ + public function sortExtensionsByPriority() + { $comparisonClosure = function (ExtensionVisitorInterface $a, ExtensionVisitorInterface $b) { if ($a->getPriority() === $b->getPriority()) { return 0; diff --git a/src/Oro/Bundle/DataGridBundle/Extension/Appearance/AppearanceExtension.php b/src/Oro/Bundle/DataGridBundle/Extension/Appearance/AppearanceExtension.php index 7cd55f749d0..dd244819aa6 100644 --- a/src/Oro/Bundle/DataGridBundle/Extension/Appearance/AppearanceExtension.php +++ b/src/Oro/Bundle/DataGridBundle/Extension/Appearance/AppearanceExtension.php @@ -40,14 +40,13 @@ public function __construct( } /** - * {@inheritDoc} + * {@inheritdoc} */ public function isApplicable(DatagridConfiguration $config) { $options = $config->offsetGetOr(static::APPEARANCE_CONFIG_PATH, []); - $hasOptions = count($options) > 0; - return $hasOptions; + return count($options) > 0; } /** diff --git a/src/Oro/Bundle/DataGridBundle/Extension/Appearance/Configuration.php b/src/Oro/Bundle/DataGridBundle/Extension/Appearance/Configuration.php index adb1110f903..bdec162980e 100644 --- a/src/Oro/Bundle/DataGridBundle/Extension/Appearance/Configuration.php +++ b/src/Oro/Bundle/DataGridBundle/Extension/Appearance/Configuration.php @@ -2,12 +2,9 @@ namespace Oro\Bundle\DataGridBundle\Extension\Appearance; -use Symfony\Component\Config\Definition\Builder\NodeBuilder; use Symfony\Component\Config\Definition\Builder\TreeBuilder; use Symfony\Component\Config\Definition\ConfigurationInterface; -use Oro\Bundle\DataGridBundle\Entity\Manager\AppearanceTypeManager; - class Configuration implements ConfigurationInterface { const GRID_APPEARANCE_TYPE = 'grid'; diff --git a/src/Oro/Bundle/DataGridBundle/Extension/Export/ExportExtension.php b/src/Oro/Bundle/DataGridBundle/Extension/Export/ExportExtension.php index 1f6eac22bdb..fbb234edc48 100644 --- a/src/Oro/Bundle/DataGridBundle/Extension/Export/ExportExtension.php +++ b/src/Oro/Bundle/DataGridBundle/Extension/Export/ExportExtension.php @@ -5,9 +5,7 @@ use Symfony\Component\Translation\TranslatorInterface; use Oro\Bundle\DataGridBundle\Extension\AbstractExtension; -use Oro\Bundle\DataGridBundle\Datagrid\Common\MetadataObject; use Oro\Bundle\DataGridBundle\Datagrid\Common\DatagridConfiguration; -use Oro\Bundle\DataGridBundle\Extension\Toolbar\ToolbarExtension; class ExportExtension extends AbstractExtension { @@ -27,7 +25,7 @@ public function __construct(TranslatorInterface $translator) } /** - * {@inheritDoc} + * {@inheritdoc} */ public function isApplicable(DatagridConfiguration $config) { diff --git a/src/Oro/Bundle/DataGridBundle/Extension/GridParams/GridParamsExtension.php b/src/Oro/Bundle/DataGridBundle/Extension/GridParams/GridParamsExtension.php index 69ff37d3994..fb66bc49a07 100644 --- a/src/Oro/Bundle/DataGridBundle/Extension/GridParams/GridParamsExtension.php +++ b/src/Oro/Bundle/DataGridBundle/Extension/GridParams/GridParamsExtension.php @@ -17,7 +17,7 @@ class GridParamsExtension extends AbstractExtension */ public function isApplicable(DatagridConfiguration $config) { - return $config->getDatasourceType() == OrmDatasource::TYPE; + return $config->getDatasourceType() === OrmDatasource::TYPE; } /** diff --git a/src/Oro/Bundle/DataGridBundle/Extension/InlineEditing/InlineEditColumnOptionsGuesser.php b/src/Oro/Bundle/DataGridBundle/Extension/InlineEditing/InlineEditColumnOptionsGuesser.php index 2b4d8fbf676..2061955e9aa 100644 --- a/src/Oro/Bundle/DataGridBundle/Extension/InlineEditing/InlineEditColumnOptionsGuesser.php +++ b/src/Oro/Bundle/DataGridBundle/Extension/InlineEditing/InlineEditColumnOptionsGuesser.php @@ -4,24 +4,17 @@ use Symfony\Component\Validator\Mapping\ClassMetadataInterface; use Symfony\Component\Validator\Mapping\Loader\AbstractLoader; +use Symfony\Component\Validator\Mapping\PropertyMetadataInterface; use Symfony\Component\Validator\Validator\ValidatorInterface; use Oro\Bundle\DataGridBundle\Extension\InlineEditing\InlineEditColumnOptions\GuesserInterface; -/** - * Class InlineEditColumnOptionsGuesser - * @package Oro\Bundle\DataGridBundle\Extension\InlineEditing - */ class InlineEditColumnOptionsGuesser { - /** - * @var ValidatorInterface - */ + /** @var ValidatorInterface */ protected $validator; - /** - * @var GuesserInterface[] - */ + /** @var GuesserInterface[] */ protected $guessers; /** @@ -51,7 +44,7 @@ public function addGuesser(GuesserInterface $guesser) */ public function getColumnOptions($columnName, $entityName, $column, $behaviour) { - /** @var ValidatorInterface $validatorMetadata */ + /** @var ClassMetadataInterface $validatorMetadata */ $validatorMetadata = $this->validator->getMetadataFor($entityName); $isEnabledInline = isset($column[Configuration::BASE_CONFIG_KEY][Configuration::CONFIG_ENABLE_KEY]) && @@ -93,6 +86,7 @@ public function getColumnOptions($columnName, $entityName, $column, $behaviour) */ protected function getValidationRules($validatorMetadata, $columnName) { + /** @var PropertyMetadataInterface $metadata */ $metadata = $validatorMetadata->getPropertyMetadata($columnName); $metadata = is_array($metadata) && isset($metadata[0]) ? $metadata[0] : $metadata; diff --git a/src/Oro/Bundle/DataGridBundle/Extension/InlineEditing/InlineEditingExtension.php b/src/Oro/Bundle/DataGridBundle/Extension/InlineEditing/InlineEditingExtension.php index d7fffd1e793..ba3733d10ca 100644 --- a/src/Oro/Bundle/DataGridBundle/Extension/InlineEditing/InlineEditingExtension.php +++ b/src/Oro/Bundle/DataGridBundle/Extension/InlineEditing/InlineEditingExtension.php @@ -32,6 +32,7 @@ class InlineEditingExtension extends AbstractExtension * @param InlineEditColumnOptionsGuesser $inlineEditColumnOptionsGuesser * @param SecurityFacade $securityFacade * @param EntityClassNameHelper $entityClassNameHelper + * @param AuthorizationCheckerInterface $authorizationChecker */ public function __construct( InlineEditColumnOptionsGuesser $inlineEditColumnOptionsGuesser, @@ -93,6 +94,7 @@ public function processConfigs(DatagridConfiguration $config) $columns = $config->offsetGetOr(FormatterConfiguration::COLUMNS_KEY, []); $blackList = $configuration->getBlackList(); $behaviour = $config->offsetGetByPath(Configuration::BEHAVIOUR_CONFIG_PATH); + $objectIdentity = new ObjectIdentity('entity', $configItems['entity_name']); foreach ($columns as $columnName => &$column) { if (!in_array($columnName, $blackList, true)) { @@ -101,7 +103,7 @@ public function processConfigs(DatagridConfiguration $config) $dadaFieldName = $this->getColummFieldName($columnName, $column); if (!$this->authChecker->isGranted( 'EDIT', - new FieldVote(new ObjectIdentity('entity', $configItems['entity_name']), $dadaFieldName) + new FieldVote($objectIdentity, $dadaFieldName) ) ) { if (array_key_exists(Configuration::BASE_CONFIG_KEY, $column)) { diff --git a/src/Oro/Bundle/DataGridBundle/Extension/MassAction/DeleteMassActionExtension.php b/src/Oro/Bundle/DataGridBundle/Extension/MassAction/DeleteMassActionExtension.php index e76eb465b3e..dc78b00a4ab 100644 --- a/src/Oro/Bundle/DataGridBundle/Extension/MassAction/DeleteMassActionExtension.php +++ b/src/Oro/Bundle/DataGridBundle/Extension/MassAction/DeleteMassActionExtension.php @@ -43,19 +43,22 @@ public function __construct(DoctrineHelper $doctrineHelper, GridConfigurationHel */ public function isApplicable(DatagridConfiguration $config) { - // validate configuration and fill default values - $options = $this->validateConfiguration( - new DeleteMassActionConfiguration(), - ['delete' => $config->offsetGetByPath(self::MASS_ACTION_OPTION_PATH, true)] - ); + if (!$this->isDeleteActionExists($config, static::MASS_ACTION_KEY) // is 'mass delete action' do not exists + && $this->isDeleteActionExists($config, static::ACTION_KEY) // is 'delete action' exists + && $this->isApplicableForEntity($config) + ) { + // validate configuration and fill default values + $options = $this->validateConfiguration( + new DeleteMassActionConfiguration(), + ['delete' => $config->offsetGetByPath(self::MASS_ACTION_OPTION_PATH, true)] + ); + + if ($options['enabled']) { + return true; + } + } - return - // Checks if mass delete action does not exists - !$this->isDeleteActionExists($config, static::MASS_ACTION_KEY) && - // Checks if delete action exists - $this->isDeleteActionExists($config, static::ACTION_KEY) && - $this->isApplicableForEntity($config) && - $options['enabled']; + return false; } /** @@ -88,7 +91,7 @@ protected function isDeleteActionExists(DatagridConfiguration $config, $key) { $actions = $config->offsetGetOr($key, []); foreach ($actions as $action) { - if ($action[static::ACTION_TYPE_KEY] == static::ACTION_TYPE_DELETE) { + if ($action[static::ACTION_TYPE_KEY] === static::ACTION_TYPE_DELETE) { return true; } } diff --git a/src/Oro/Bundle/DataGridBundle/Extension/Pager/OrmPagerExtension.php b/src/Oro/Bundle/DataGridBundle/Extension/Pager/OrmPagerExtension.php index 9820c64963b..8567105883a 100644 --- a/src/Oro/Bundle/DataGridBundle/Extension/Pager/OrmPagerExtension.php +++ b/src/Oro/Bundle/DataGridBundle/Extension/Pager/OrmPagerExtension.php @@ -14,9 +14,6 @@ use Oro\Bundle\DataGridBundle\Extension\Toolbar\ToolbarExtension; /** - * Class OrmPagerExtension - * @package Oro\Bundle\DataGridBundle\Extension\Pager - * * Responsibility of this extension is to apply pagination on query for ORM datasource */ class OrmPagerExtension extends AbstractExtension @@ -45,11 +42,10 @@ public function __clone() */ public function isApplicable(DatagridConfiguration $config) { - // enabled by default for ORM datasource - $disabled = $this->getOr(PagerInterface::DISABLED_PARAM, false) - || $config->offsetGetByPath(ToolbarExtension::TOOLBAR_PAGINATION_HIDE_OPTION_PATH, false); - - return !$disabled && $config->getDatasourceType() == OrmDatasource::TYPE; + return + $config->getDatasourceType() === OrmDatasource::TYPE + && !$this->getOr(PagerInterface::DISABLED_PARAM, false) + && !$config->offsetGetByPath(ToolbarExtension::TOOLBAR_PAGINATION_HIDE_OPTION_PATH, false); } /** diff --git a/src/Oro/Bundle/DataGridBundle/Extension/Sorter/OrmSorterExtension.php b/src/Oro/Bundle/DataGridBundle/Extension/Sorter/OrmSorterExtension.php index 69eca1b3713..6685764a894 100644 --- a/src/Oro/Bundle/DataGridBundle/Extension/Sorter/OrmSorterExtension.php +++ b/src/Oro/Bundle/DataGridBundle/Extension/Sorter/OrmSorterExtension.php @@ -35,11 +35,9 @@ class OrmSorterExtension extends AbstractExtension */ public function isApplicable(DatagridConfiguration $config) { - $columns = $config->offsetGetByPath(Configuration::COLUMNS_PATH); - $isApplicable = $config->getDatasourceType() === OrmDatasource::TYPE - && is_array($columns); - - return $isApplicable; + return + $config->getDatasourceType() === OrmDatasource::TYPE + && is_array($config->offsetGetByPath(Configuration::COLUMNS_PATH)); } /** @@ -252,6 +250,12 @@ protected function normalizeDirection($direction) switch (true) { case in_array($direction, [self::DIRECTION_ASC, self::DIRECTION_DESC], true): break; + case ($direction === 1): + $direction = self::DIRECTION_DESC; + break; + case ($direction === -1): + $direction = self::DIRECTION_ASC; + break; case ($direction === false): $direction = self::DIRECTION_DESC; break; diff --git a/src/Oro/Bundle/DataGridBundle/Extension/Totals/OrmTotalsExtension.php b/src/Oro/Bundle/DataGridBundle/Extension/Totals/OrmTotalsExtension.php index b062f403f2c..abe238125c1 100644 --- a/src/Oro/Bundle/DataGridBundle/Extension/Totals/OrmTotalsExtension.php +++ b/src/Oro/Bundle/DataGridBundle/Extension/Totals/OrmTotalsExtension.php @@ -2,13 +2,12 @@ namespace Oro\Bundle\DataGridBundle\Extension\Totals; -use Doctrine\Common\Collections\ArrayCollection; use Doctrine\ORM\Query\Expr; use Doctrine\ORM\QueryBuilder; -use Oro\Component\DoctrineUtils\ORM\QueryUtils; use Symfony\Component\Translation\TranslatorInterface; +use Oro\Component\DoctrineUtils\ORM\QueryUtils; use Oro\Component\PhpUtils\ArrayUtil; use Oro\Bundle\DataGridBundle\Datagrid\Common\DatagridConfiguration; diff --git a/src/Oro/Bundle/DataGridBundle/Resources/config/extensions.yml b/src/Oro/Bundle/DataGridBundle/Resources/config/extensions.yml index a2d5280dd5a..0b0ff807bbd 100644 --- a/src/Oro/Bundle/DataGridBundle/Resources/config/extensions.yml +++ b/src/Oro/Bundle/DataGridBundle/Resources/config/extensions.yml @@ -24,7 +24,6 @@ services: - '@translator' tags: - { name: oro_datagrid.extension } - lazy: true oro_datagrid.extension.orm_pager: class: %oro_datagrid.extension.orm_pager.class% diff --git a/src/Oro/Bundle/DataGridBundle/Resources/public/css/less/oro.grid.less b/src/Oro/Bundle/DataGridBundle/Resources/public/css/less/oro.grid.less index 83028c9794f..94873f08cd5 100644 --- a/src/Oro/Bundle/DataGridBundle/Resources/public/css/less/oro.grid.less +++ b/src/Oro/Bundle/DataGridBundle/Resources/public/css/less/oro.grid.less @@ -191,7 +191,6 @@ table.grid { .integer-cell { text-align: right; } - .datetime-cell, .date-cell { white-space: nowrap; } @@ -362,3 +361,44 @@ td > .nowrap-ellipsis { .grid .action-column >.dropdown { margin-left: 0; } +.with-floating-header { + .grid-header-cell{ + padding-right: 0; + } + .grid-header-cell-link { + width: 100%; + display: flex; + } + .grid-header-cell-label { + text-overflow: ellipsis; + display: block; + overflow: hidden; + float: left; + } + &.floatThead .grid-header.thead-sizing .grid-header-cell:not(.abbreviated) .grid-header-cell-label { + width: 0 !important; + } + &:not(.floatThead) .grid-header .grid-header-cell:not(.abbreviated) .grid-header-cell-label, + .grid-header.thead-sizing .grid-header-cell:not(.abbreviated) .grid-header-cell-label { + width: 40px !important; + } +} +.grid-header-cell-hint { + position: absolute; + display: block; + background: #fff; + padding: 0 7px; + border-radius: 3px; + box-shadow: 1px 1px 3px rgba(0,0,0,0.2); + z-index: 10000; + &:after{ + content: ''; + display: inline-block; + border-left: 6px solid rgba(0, 0, 0, 0); + border-right: 6px solid rgba(0, 0, 0, 0); + border-bottom: 6px solid #fff; + position: absolute; + top: -4px; + left: 6px; + } +} diff --git a/src/Oro/Bundle/DataGridBundle/Resources/public/js/app/plugins/grid/floating-header-plugin.js b/src/Oro/Bundle/DataGridBundle/Resources/public/js/app/plugins/grid/floating-header-plugin.js index b43c98f5a01..b65d0c9f01a 100644 --- a/src/Oro/Bundle/DataGridBundle/Resources/public/js/app/plugins/grid/floating-header-plugin.js +++ b/src/Oro/Bundle/DataGridBundle/Resources/public/js/app/plugins/grid/floating-header-plugin.js @@ -39,6 +39,8 @@ define(function(require) { this.setupCache(); + this.$el.addClass('with-floating-header'); + this.isHeaderCellWidthFixed = false; this.rescrollCb = this.enableOtherScroll(); if (!this.isHeaderCellWidthFixed) { @@ -69,6 +71,9 @@ define(function(require) { this.domCache.headerCells.attr('style', ''); this.domCache.firstRowCells.attr('style', ''); } + + this.$el.removeClass('with-floating-header'); + FloatingHeaderPlugin.__super__.disable.call(this); }, @@ -164,10 +169,12 @@ define(function(require) { headerCells.each(function(i, headerCell) { var cellWidth = widths[i] - widthDecrement; headerCell.style.width = cellWidth + 'px'; + headerCell.style.maxWidth = cellWidth + 'px'; headerCell.style.minWidth = cellWidth + 'px'; headerCell.style.boxSizing = 'border-box'; if (firstRowCells[i]) { firstRowCells[i].style.width = cellWidth + 'px'; + firstRowCells[i].style.maxWidth = cellWidth + 'px'; firstRowCells[i].style.minWidth = cellWidth + 'px'; firstRowCells[i].style.boxSizing = 'border-box'; } diff --git a/src/Oro/Bundle/DataGridBundle/Resources/public/js/datagrid/cell/html-cell.js b/src/Oro/Bundle/DataGridBundle/Resources/public/js/datagrid/cell/html-cell.js index 94663dea117..6ded23855f8 100644 --- a/src/Oro/Bundle/DataGridBundle/Resources/public/js/datagrid/cell/html-cell.js +++ b/src/Oro/Bundle/DataGridBundle/Resources/public/js/datagrid/cell/html-cell.js @@ -18,7 +18,7 @@ define([ * model's raw value for this cell's column. */ render: function() { - this.$el.empty().html(this.formatter.fromRaw(this.model.get(this.column.get('name')))); + this.$el.empty().html(this.model.get(this.column.get('name'))); return this; } }); diff --git a/src/Oro/Bundle/DataGridBundle/Resources/public/js/datagrid/cell/select-cell.js b/src/Oro/Bundle/DataGridBundle/Resources/public/js/datagrid/cell/select-cell.js index 504901598d8..0538d1dc5f7 100644 --- a/src/Oro/Bundle/DataGridBundle/Resources/public/js/datagrid/cell/select-cell.js +++ b/src/Oro/Bundle/DataGridBundle/Resources/public/js/datagrid/cell/select-cell.js @@ -1,8 +1,9 @@ define([ 'underscore', 'backgrid', - 'orodatagrid/js/datagrid/editor/select-cell-radio-editor' -], function(_, Backgrid, SelectCellRadioEditor) { + 'orodatagrid/js/datagrid/editor/select-cell-radio-editor', + 'oroui/js/tools/text-util' +], function(_, Backgrid, SelectCellRadioEditor, textUtil) { 'use strict'; var SelectCell; @@ -26,7 +27,7 @@ define([ if (options.column.get('metadata').choices) { this.optionValues = []; _.each(options.column.get('metadata').choices, function(value, key) { - this.optionValues.push([value, key]); + this.optionValues.push([textUtil.prepareText(value), key]); }, this); } else { throw new Error('Column metadata must have choices specified'); diff --git a/src/Oro/Bundle/DataGridBundle/Resources/public/js/datagrid/formatter/cell-formatter.js b/src/Oro/Bundle/DataGridBundle/Resources/public/js/datagrid/formatter/cell-formatter.js index 88f50d6d542..1cc31bedf21 100644 --- a/src/Oro/Bundle/DataGridBundle/Resources/public/js/datagrid/formatter/cell-formatter.js +++ b/src/Oro/Bundle/DataGridBundle/Resources/public/js/datagrid/formatter/cell-formatter.js @@ -1,5 +1,5 @@ -define(['underscore', 'backgrid' - ], function(_, Backgrid) { +define(['underscore', 'backgrid', 'oroui/js/tools/text-util' + ], function(_, Backgrid, textUtil) { 'use strict'; /** @@ -21,7 +21,8 @@ define(['underscore', 'backgrid' if (rawData === null) { return ''; } - return Backgrid.CellFormatter.prototype.fromRaw.apply(this, arguments); + var result = Backgrid.CellFormatter.prototype.fromRaw.apply(this, arguments); + return textUtil.prepareText(result); } }); diff --git a/src/Oro/Bundle/DataGridBundle/Resources/public/js/datagrid/formatter/datetime-formatter.js b/src/Oro/Bundle/DataGridBundle/Resources/public/js/datagrid/formatter/datetime-formatter.js index d0bad088596..20a535aec50 100644 --- a/src/Oro/Bundle/DataGridBundle/Resources/public/js/datagrid/formatter/datetime-formatter.js +++ b/src/Oro/Bundle/DataGridBundle/Resources/public/js/datagrid/formatter/datetime-formatter.js @@ -30,7 +30,8 @@ define(['underscore', 'backgrid', 'orolocale/js/formatter/datetime' return ''; } // Call one of formatDate formatTime formatDateTime - return this._getFormatterFunction('format').call(DateTimeFormatter, rawData); + return this._getFormatterFunction('format', this.type === 'dateTime' ? 'NBSP' : undefined) + .call(DateTimeFormatter, rawData); }, /** diff --git a/src/Oro/Bundle/DataGridBundle/Resources/public/js/datagrid/formatter/phone-formatter.js b/src/Oro/Bundle/DataGridBundle/Resources/public/js/datagrid/formatter/phone-formatter.js index 9f97c133805..e9301758146 100644 --- a/src/Oro/Bundle/DataGridBundle/Resources/public/js/datagrid/formatter/phone-formatter.js +++ b/src/Oro/Bundle/DataGridBundle/Resources/public/js/datagrid/formatter/phone-formatter.js @@ -33,7 +33,8 @@ define(['underscore', 'backgrid' * @return {string} */ generateLinkHTML: function(phoneNumber) { - return '' + _.escape(phoneNumber) + ''; + var number = phoneNumber.trim(); + return '' + _.escape(number) + ''; } }); diff --git a/src/Oro/Bundle/DataGridBundle/Resources/public/js/datagrid/grid-views/view.js b/src/Oro/Bundle/DataGridBundle/Resources/public/js/datagrid/grid-views/view.js index a805c2e468f..d4d4756e5b6 100644 --- a/src/Oro/Bundle/DataGridBundle/Resources/public/js/datagrid/grid-views/view.js +++ b/src/Oro/Bundle/DataGridBundle/Resources/public/js/datagrid/grid-views/view.js @@ -214,6 +214,7 @@ define(function(require) { var self = this; model.save({ + icon: void 0, label: model.get('label'), filters: this.collection.state.filters, sorters: this.collection.state.sorters, diff --git a/src/Oro/Bundle/DataGridBundle/Resources/public/js/datagrid/header-cell/header-cell.js b/src/Oro/Bundle/DataGridBundle/Resources/public/js/datagrid/header-cell/header-cell.js index 6bea9e8685b..9efa7aa75ca 100644 --- a/src/Oro/Bundle/DataGridBundle/Resources/public/js/datagrid/header-cell/header-cell.js +++ b/src/Oro/Bundle/DataGridBundle/Resources/public/js/datagrid/header-cell/header-cell.js @@ -1,7 +1,9 @@ define([ 'underscore', - 'backgrid' -], function(_, Backgrid) { + 'jquery', + 'backgrid', + 'oroui/js/tools/text-util' +], function(_, $, Backgrid, textUtil) { 'use strict'; var HeaderCell; @@ -18,20 +20,31 @@ define([ /** @property */ template: _.template( '<% if (sortable) { %>' + - '' + - '<%- label %> ' + + '' + + '<%- label %>' + '' + '' + '<% } else { %>' + - '<%- label %>' + // wrap label into span otherwise underscore will not render it + '' + + '<%- label %>' + + '' + '<% } %>' ), /** @property {Boolean} */ allowNoSorting: true, + /** @property {Number} */ + minWordsToAbbreviate: 4, + keepElement: false, + events: { + mouseenter: 'onMouseEnter', + mouseleave: 'onMouseLeave', + click: 'onClick' + }, + /** * Initialize. * @@ -95,8 +108,15 @@ define([ render: function() { this.$el.empty(); + var label = this.column.get('label'); + var abbreviation = textUtil.abbreviate(label, this.minWordsToAbbreviate); + + this.isLabelAbbreviated = abbreviation !== label; + + this.$el.toggleClass('abbreviated', this.isLabelAbbreviated); + this.$el.append(this.template({ - label: this.column.get('label'), + label: abbreviation, sortable: this.column.get('sortable') })); @@ -155,6 +175,49 @@ define([ cycleSort(this, column); } } + }, + + onMouseEnter: function(e) { + var _this = this; + var $label = this.$('.grid-header-cell-label'); + + // measure text content + var realWidth = $label[0].clientWidth; + $label.css({overflow: 'visible'}); + var fullWidth = $label[0].clientWidth; + $label.css({overflow: ''}); + + if (!this.isLabelAbbreviated && fullWidth === realWidth) { + // hint is not required all text is visible + return; + } + + this.popoverAdded = true; + + $label.popover({ + content: _this.column.get('label'), + trigger: 'manual', + placement: 'bottom', + animation: 'false', + container: 'body', + template: '' + }); + + this.hintTimeout = setTimeout(function addHeaderCellHint() { + $label.popover('show'); + }, 300); + }, + + onMouseLeave: function(e) { + clearTimeout(this.hintTimeout); + var $label = this.$('.grid-header-cell-label'); + $label.popover('hide'); + $label.popover('destroy'); + this.popoverAdded = false; } }); diff --git a/src/Oro/Bundle/DataGridBundle/Resources/public/js/pageable-collection.js b/src/Oro/Bundle/DataGridBundle/Resources/public/js/pageable-collection.js index f35912cce88..d2aa1b26219 100644 --- a/src/Oro/Bundle/DataGridBundle/Resources/public/js/pageable-collection.js +++ b/src/Oro/Bundle/DataGridBundle/Resources/public/js/pageable-collection.js @@ -99,7 +99,8 @@ define([ filters: {}, sorters: {}, columns: {}, - appearanceType: 'grid' + appearanceType: 'grid', + appearanceData: {} }, /** @@ -108,7 +109,8 @@ define([ * @property */ state: _.extend({ - appearanceType: 'grid' + appearanceType: 'grid', + appearanceData: {} }, BackbonePageableCollection.prototype.state), /** diff --git a/src/Oro/Bundle/DataGridBundle/Tests/Unit/Extension/AcceptorTest.php b/src/Oro/Bundle/DataGridBundle/Tests/Unit/Extension/AcceptorTest.php index 297b317dac5..1e78c5ad6ff 100644 --- a/src/Oro/Bundle/DataGridBundle/Tests/Unit/Extension/AcceptorTest.php +++ b/src/Oro/Bundle/DataGridBundle/Tests/Unit/Extension/AcceptorTest.php @@ -39,7 +39,10 @@ public function testExtension() $extMock1->expects($this->any())->method('getPriority')->will($this->returnValue(-100)); $extMock2->expects($this->any())->method('getPriority')->will($this->returnValue(250)); - $this->acceptor->addExtension($extMock1)->addExtension($extMock2); + $this->acceptor + ->addExtension($extMock1) + ->addExtension($extMock2) + ->sortExtensionsByPriority(); $results = $this->acceptor->getExtensions(); diff --git a/src/Oro/Bundle/DataGridBundle/Tools/GridConfigurationHelper.php b/src/Oro/Bundle/DataGridBundle/Tools/GridConfigurationHelper.php index 72e2a692e59..1d47e31bf69 100644 --- a/src/Oro/Bundle/DataGridBundle/Tools/GridConfigurationHelper.php +++ b/src/Oro/Bundle/DataGridBundle/Tools/GridConfigurationHelper.php @@ -25,7 +25,7 @@ public function __construct(EntityClassResolver $entityClassResolver) */ public function getEntity(DatagridConfiguration $config) { - $entityClassName = $config->offsetGetByPath('[extended_entity_name]'); + $entityClassName = $config->offsetGetOr('extended_entity_name'); if ($entityClassName) { return $entityClassName; } diff --git a/src/Oro/Bundle/DataGridBundle/composer.json b/src/Oro/Bundle/DataGridBundle/composer.json index 8a747cd8e49..159891747d2 100644 --- a/src/Oro/Bundle/DataGridBundle/composer.json +++ b/src/Oro/Bundle/DataGridBundle/composer.json @@ -6,7 +6,7 @@ "license": "MIT", "require": { "php": ">=5.5.9", - "symfony/symfony": "2.8.*", + "symfony/symfony": "2.8.*, !=2.8.10", "oro/config": "dev-master", "oro/ui-bundle": "dev-master", "oro/requirejs-bundle": "dev-master", diff --git a/src/Oro/Bundle/DistributionBundle/composer.json b/src/Oro/Bundle/DistributionBundle/composer.json index c1f09106f9f..374e8bb649d 100644 --- a/src/Oro/Bundle/DistributionBundle/composer.json +++ b/src/Oro/Bundle/DistributionBundle/composer.json @@ -7,7 +7,7 @@ "license": "MIT", "require": { "php": ">=5.5.9", - "symfony/symfony": "2.8.*", + "symfony/symfony": "2.8.*, !=2.8.10", "oro/config": "dev-master" }, "autoload": { diff --git a/src/Oro/Bundle/EmailBundle/Datagrid/OriginFolderFilterProvider.php b/src/Oro/Bundle/EmailBundle/Datagrid/OriginFolderFilterProvider.php index aec0709fefe..4d2ae13c4ef 100644 --- a/src/Oro/Bundle/EmailBundle/Datagrid/OriginFolderFilterProvider.php +++ b/src/Oro/Bundle/EmailBundle/Datagrid/OriginFolderFilterProvider.php @@ -52,13 +52,21 @@ public function getListTypeChoices($extended = false) */ protected function getOrigins() { - $criteria = [ - 'owner' => $this->securityFacade->getLoggedUser(), - 'organization' => $this->securityFacade->getOrganization(), - 'isActive' => true, - ]; - - return $this->doctrine->getRepository(self::EMAIL_ORIGIN)->findBy($criteria); + return $this->doctrine->getRepository(self::EMAIL_ORIGIN) + ->createQueryBuilder('eo') + ->select('eo, f, m') + ->leftJoin('eo.folders', 'f') + ->leftJoin('eo.mailbox', 'm') + ->andWhere('eo.owner = :owner') + ->andWhere('eo.organization = :organization') + ->andWhere('eo.isActive = :isActive') + ->setParameters([ + 'owner' => $this->securityFacade->getLoggedUser(), + 'organization' => $this->securityFacade->getOrganization(), + 'isActive' => true, + ]) + ->getQuery() + ->getResult(); } /** diff --git a/src/Oro/Bundle/EmailBundle/EventListener/Datagrid/EmailGridListener.php b/src/Oro/Bundle/EmailBundle/EventListener/Datagrid/EmailGridListener.php index 35e5a41c785..aa8e5c5ba6e 100644 --- a/src/Oro/Bundle/EmailBundle/EventListener/Datagrid/EmailGridListener.php +++ b/src/Oro/Bundle/EmailBundle/EventListener/Datagrid/EmailGridListener.php @@ -2,20 +2,40 @@ namespace Oro\Bundle\EmailBundle\EventListener\Datagrid; +use Doctrine\Bundle\DoctrineBundle\Registry; use Doctrine\ORM\QueryBuilder; +use Oro\Bundle\DataGridBundle\Datagrid\ParameterBag; use Oro\Bundle\DataGridBundle\Datasource\Orm\OrmDatasource; +use Oro\Bundle\DataGridBundle\Datasource\ResultRecord; use Oro\Bundle\DataGridBundle\Event\BuildAfter; +use Oro\Bundle\DataGridBundle\Event\OrmResultAfter; +use Oro\Bundle\DataGridBundle\Event\OrmResultBeforeQuery; use Oro\Bundle\EmailBundle\Datagrid\EmailQueryFactory; -use Oro\Bundle\DataGridBundle\Datagrid\ParameterBag; +use Oro\Component\DoctrineUtils\ORM\QueryUtils; class EmailGridListener { + /** + * @var Registry + */ + protected $registry; + /** * @var EmailQueryFactory */ protected $factory; + /** + * @var QueryBuilder|null + */ + protected $qb; + + /** + * @var string|null + */ + protected $select; + /** * @param EmailQueryFactory $factory */ @@ -49,6 +69,57 @@ public function onBuildAfter(BuildAfter $event) $this->prepareQueryToFilter($parameters, $queryBuilder); } + /** + * @param OrmResultBeforeQuery $event + */ + public function onResultBeforeQuery(OrmResultBeforeQuery $event) + { + $this->qb = $event->getQueryBuilder(); + + $selectParts = $this->qb->getDQLPart('select'); + $stringSelectParts = []; + foreach ($selectParts as $selectPart) { + $stringSelectParts[] = (string) $selectPart; + } + $this->select = implode(', ', $stringSelectParts); + + $this->qb->select('eu.id'); + } + + /** + * @param OrmResultAfter $event + */ + public function onResultAfter(OrmResultAfter $event) + { + $originalRecords = $event->getRecords(); + if (!$originalRecords) { + return; + } + + $ids = []; + foreach ($originalRecords as $record) { + $ids[] = $record->getValue('id'); + } + + $this->qb + ->select($this->select) + ->resetDQLPart('groupBy') + ->where($this->qb->expr()->in('eu.id', ':ids')) + ->setMaxResults(null) + ->setFirstResult(null) + ->setParameter('ids', $ids); + QueryUtils::removeUnusedParameters($this->qb); + $result = $this->qb + ->getQuery() + ->getResult(); + + $records = []; + foreach ($result as $row) { + $records[] = new ResultRecord($row); + } + $event->setRecords($records); + } + /** * Add join for query just if filter used. For performance optimization - BAP-10674 * diff --git a/src/Oro/Bundle/EmailBundle/Manager/EmailNotificationManager.php b/src/Oro/Bundle/EmailBundle/Manager/EmailNotificationManager.php index 073583ad2c0..14d0e3f3737 100644 --- a/src/Oro/Bundle/EmailBundle/Manager/EmailNotificationManager.php +++ b/src/Oro/Bundle/EmailBundle/Manager/EmailNotificationManager.php @@ -8,6 +8,7 @@ use Symfony\Component\Routing\Exception\RouteNotFoundException; use Oro\Bundle\EmailBundle\Entity\Email; +use Oro\Bundle\EmailBundle\Tools\EmailBodyHelper; use Oro\Bundle\EntityConfigBundle\Config\ConfigManager; use Oro\Bundle\OrganizationBundle\Entity\Organization; use Oro\Bundle\UIBundle\Tools\HtmlTagHelper; @@ -22,6 +23,9 @@ class EmailNotificationManager /** @var HtmlTagHelper */ protected $htmlTagHelper; + /** @var EmailBodyHelper */ + protected $emailBodyHelper; + /** @var Router */ protected $router; @@ -36,17 +40,20 @@ class EmailNotificationManager * @param HtmlTagHelper $htmlTagHelper * @param Router $router * @param ConfigManager $configManager + * @param EmailBodyHelper $emailBodyHelper */ public function __construct( EntityManager $entityManager, HtmlTagHelper $htmlTagHelper, Router $router, - ConfigManager $configManager + ConfigManager $configManager, + EmailBodyHelper $emailBodyHelper ) { $this->em = $entityManager; $this->htmlTagHelper = $htmlTagHelper; $this->router = $router; $this->configManager = $configManager; + $this->emailBodyHelper = $emailBodyHelper; } /** @@ -74,9 +81,7 @@ public function getEmails(User $user, Organization $organization, $maxEmailsDisp $emailBody = $email->getEmailBody(); if ($emailBody) { $bodyContent = $this->htmlTagHelper->shorten( - $this->htmlTagHelper->stripTags( - $this->htmlTagHelper->purify($emailBody->getBodyContent()) - ) + $this->emailBodyHelper->getClearBody($emailBody->getBodyContent()) ); } diff --git a/src/Oro/Bundle/EmailBundle/Provider/EmailActivityListProvider.php b/src/Oro/Bundle/EmailBundle/Provider/EmailActivityListProvider.php index 60289f9f537..d4a5b7b6cff 100644 --- a/src/Oro/Bundle/EmailBundle/Provider/EmailActivityListProvider.php +++ b/src/Oro/Bundle/EmailBundle/Provider/EmailActivityListProvider.php @@ -22,6 +22,7 @@ use Oro\Bundle\EmailBundle\Entity\EmailOwnerInterface; use Oro\Bundle\EmailBundle\Entity\EmailUser; use Oro\Bundle\EmailBundle\Entity\Provider\EmailThreadProvider; +use Oro\Bundle\EmailBundle\Tools\EmailBodyHelper; use Oro\Bundle\EntityBundle\ORM\DoctrineHelper; use Oro\Bundle\EntityBundle\Provider\EntityNameResolver; use Oro\Bundle\EntityConfigBundle\Config\ConfigManager; @@ -83,6 +84,9 @@ class EmailActivityListProvider implements /** @var CommentAssociationHelper */ protected $commentAssociationHelper; + /** @var EmailBodyHelper */ + protected $emailBodyHelper; + /** * @param DoctrineHelper $doctrineHelper * @param ServiceLink $doctrineRegistryLink @@ -95,6 +99,7 @@ class EmailActivityListProvider implements * @param ServiceLink $mailboxProcessStorageLink * @param ActivityAssociationHelper $activityAssociationHelper * @param CommentAssociationHelper $commentAssociationHelper + * @param EmailBodyHelper $emailBodyHelper * * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ @@ -109,7 +114,8 @@ public function __construct( ServiceLink $securityFacadeLink, ServiceLink $mailboxProcessStorageLink, ActivityAssociationHelper $activityAssociationHelper, - CommentAssociationHelper $commentAssociationHelper + CommentAssociationHelper $commentAssociationHelper, + EmailBodyHelper $emailBodyHelper ) { $this->doctrineHelper = $doctrineHelper; $this->doctrineRegistryLink = $doctrineRegistryLink; @@ -122,6 +128,7 @@ public function __construct( $this->mailboxProcessStorageLink = $mailboxProcessStorageLink; $this->activityAssociationHelper = $activityAssociationHelper; $this->commentAssociationHelper = $commentAssociationHelper; + $this->emailBodyHelper = $emailBodyHelper; } /** @@ -188,8 +195,7 @@ public function getDescription($entity) /** @var $entity Email */ if ($entity->getEmailBody()) { $body = $entity->getEmailBody()->getBodyContent(); - $content = $this->htmlTagHelper->purify($body); - $content = $this->htmlTagHelper->stripTags($content); + $content = $this->emailBodyHelper->getClearBody($body); $content = $this->htmlTagHelper->shorten($content); return $content; diff --git a/src/Oro/Bundle/EmailBundle/Resources/config/datagrid.yml b/src/Oro/Bundle/EmailBundle/Resources/config/datagrid.yml index 77e2c9ce152..a390962f948 100644 --- a/src/Oro/Bundle/EmailBundle/Resources/config/datagrid.yml +++ b/src/Oro/Bundle/EmailBundle/Resources/config/datagrid.yml @@ -81,6 +81,27 @@ datagrid: query: select: - partial eu.{id, email} + - e + - partial f.{id, type} + - eb.bodyContent AS body_content + - r_to + - > + (SELECT COUNT(_ec.id) + FROM OroEmailBundle:Email _ec + WHERE _ec.thread = e.thread) AS thread_email_count + - > + CASE + WHEN e.thread IS NULL THEN eb.hasAttachments + WHEN EXISTS( + SELECT 1 + FROM OroEmailBundle:EmailAttachment _ea + JOIN _ea.emailBody _eb + JOIN _eb.email _e + JOIN _e.thread _t + WHERE _t = e.thread + ) THEN true + ELSE false + END AS has_attachments - CASE WHEN eu.seen = true THEN 0 ELSE 1 END as is_new from: - { table: OroEmailBundle:EmailUser, alias: eu } @@ -101,10 +122,14 @@ datagrid: - join: f.origin alias: o + - + join: e.emailBody + alias: eb where: and: - o.isActive = true - groupBy: eu.id, e.sentAt + groupBy: e.sentAt, eu.id + columns: contacts: data_name: email.contacts @@ -119,7 +144,7 @@ datagrid: frontend_type: html template: OroEmailBundle:Email:Datagrid/Property/subject.html.twig attachments: - data_name: email.attachments + data_name: has_attachments type: twig label: frontend_type: html @@ -273,6 +298,7 @@ datagrid: select: - partial e.{ id, subject, sentAt } - partial eu.{ id, receivedAt, email } + - eb.bodyContent AS body_content - a - CASE WHEN eu.seen = true THEN 0 ELSE 1 END as is_new from: @@ -282,6 +308,9 @@ datagrid: - join: eu.email alias: e + - + join: e.emailBody + alias: eb inner: - { join: eu.folders, alias: f } - { join: f.origin, alias: o } diff --git a/src/Oro/Bundle/EmailBundle/Resources/config/services.yml b/src/Oro/Bundle/EmailBundle/Resources/config/services.yml index 4a679d9174b..8ba1875d3b2 100644 --- a/src/Oro/Bundle/EmailBundle/Resources/config/services.yml +++ b/src/Oro/Bundle/EmailBundle/Resources/config/services.yml @@ -560,6 +560,8 @@ services: - '@oro_email.datagrid_query_factory' tags: - { name: kernel.event_listener, event: oro_datagrid.datagrid.build.after.base-email-grid, method: onBuildAfter } + - { name: kernel.event_listener, event: oro_datagrid.orm_datasource.result.before_query.base-email-grid, method: onResultBeforeQuery, priority: -255 } + - { name: kernel.event_listener, event: oro_datagrid.orm_datasource.result.after.base-email-grid, method: onResultAfter, priority: 255 } oro_email.listener.datagrid.activity: class: %oro_email.listener.datagrid.activity.class% @@ -647,6 +649,7 @@ services: - '@oro_email.mailbox.process_storage' - '@oro_security.security_facade' - '@oro_email.related_emails.provider' + - '@oro_email.tools.email_body_helper' tags: - { name: twig.extension } @@ -677,6 +680,7 @@ services: - '@oro_email.mailbox.process_storage.link' - '@oro_activity.association_helper' - '@oro_comment.association_helper' + - '@oro_email.tools.email_body_helper' calls: - [ setSecurityContextLink, ['@security.context.link'] ] tags: @@ -860,6 +864,7 @@ services: - '@oro_ui.html_tag_helper' - '@router' - '@oro_entity_config.config_manager' + - '@oro_email.tools.email_body_helper' oro_email.datagrid.origin_folder.provider: class: %oro_email.datagrid.origin_folder.provider.class% @@ -1054,3 +1059,8 @@ services: - '@oro_email.email.activity.manager' - '@oro_email.provider.emailowners.provider' - '@oro_email.email.manager' + + oro_email.tools.email_body_helper: + class: Oro\Bundle\EmailBundle\Tools\EmailBodyHelper + arguments: + - '@oro_ui.html_tag_helper' diff --git a/src/Oro/Bundle/EmailBundle/Resources/views/Email/Datagrid/Property/attachments.html.twig b/src/Oro/Bundle/EmailBundle/Resources/views/Email/Datagrid/Property/attachments.html.twig index 94cd28e84fa..d2e03835b01 100644 --- a/src/Oro/Bundle/EmailBundle/Resources/views/Email/Datagrid/Property/attachments.html.twig +++ b/src/Oro/Bundle/EmailBundle/Resources/views/Email/Datagrid/Property/attachments.html.twig @@ -1,12 +1,4 @@ -{% set thread = record.getValue('email.thread') %} -{% set emailBody = record.getValue('email.emailBody') %} -{% if thread is not null %} - {% set hasAttachments = oro_get_email_thread_attachments(thread)|length %} -{% elseif emailBody is not null %} - {% set hasAttachments = emailBody.hasAttachments %} -{% endif %} - -{% if hasAttachments is defined and hasAttachments > 0 %} +{% if value %} diff --git a/src/Oro/Bundle/EmailBundle/Resources/views/Email/Datagrid/Property/contacts.html.twig b/src/Oro/Bundle/EmailBundle/Resources/views/Email/Datagrid/Property/contacts.html.twig index 3bd56dd363a..4752ed49bc3 100644 --- a/src/Oro/Bundle/EmailBundle/Resources/views/Email/Datagrid/Property/contacts.html.twig +++ b/src/Oro/Bundle/EmailBundle/Resources/views/Email/Datagrid/Property/contacts.html.twig @@ -1,9 +1,8 @@ {% import 'OroEmailBundle::macros.html.twig' as EA %} -{% set folders = record.getValue('folders') %} {% set thread = record.getValue('email.thread') %} {% set isNew = record.getValue('is_new') %} -{% set folderType = folders is not null and folders[0] is defined ? folders[0].type : null %} +{% set threadEmailCount = record.getValue('thread_email_count') %} {% if record.getValue('incoming') %} {% set sender = record.getValue('email.from_name') %} @@ -14,7 +13,6 @@ {% endif %} -{% if folders is not null %} {% if record.getValue('incoming') %} @@ -24,7 +22,6 @@ {% endif %} -{% endif %} {% if sender is defined %} {{ EA.wrapTextToTag(sender|truncate(22, false, '...'), isNew) }} {% elseif (recipients is defined and recipients|length > 0 and recipients|length < 3) %} @@ -41,7 +38,7 @@ {{ EA.wrapTextToTag(firstLastRecipients, isNew) }} {% endif %} {% endif %} -{% if thread is not null and thread.emails|length > 1 %} - {{ EA.wrapTextToTag('(' ~ thread.emails|length ~ ')', isNew) }} +{% if threadEmailCount > 1 %} + {{ EA.wrapTextToTag('(' ~ threadEmailCount ~ ')', isNew) }} {% endif %} diff --git a/src/Oro/Bundle/EmailBundle/Resources/views/Email/Datagrid/Property/subject.html.twig b/src/Oro/Bundle/EmailBundle/Resources/views/Email/Datagrid/Property/subject.html.twig index 934c0453019..c3917d2b6bf 100644 --- a/src/Oro/Bundle/EmailBundle/Resources/views/Email/Datagrid/Property/subject.html.twig +++ b/src/Oro/Bundle/EmailBundle/Resources/views/Email/Datagrid/Property/subject.html.twig @@ -1,5 +1,5 @@ {% import 'OroEmailBundle::macros.html.twig' as EA %} -{% set emailBody = record.getValue('email.emailBody') %} +{% set emailBody = {bodyContent: record.getValue('body_content')} %} {% set isNew = record.getValue('is_new') %} {% set valueToShow = value ? value : 'oro.email.subject.no_subject.label'|trans %} diff --git a/src/Oro/Bundle/EmailBundle/Resources/views/macros.html.twig b/src/Oro/Bundle/EmailBundle/Resources/views/macros.html.twig index f9f32cc65b4..98ab67d2fb3 100644 --- a/src/Oro/Bundle/EmailBundle/Resources/views/macros.html.twig +++ b/src/Oro/Bundle/EmailBundle/Resources/views/macros.html.twig @@ -582,7 +582,7 @@ #} {% macro email_short_body(emailBody, length) %} {%- set length = length|default(150) -%} - {{ emailBody.bodyContent|oro_html_purify|striptags|oro_preg_replace('/\-{2,}/', '--')[:length]|replace({'--': '—'})|raw }} + {{ emailBody.bodyContent|oro_cleanup_email_body|oro_preg_replace('/\-{2,}/', '--')[:length]|replace({'--': '—'})|raw }} {% endmacro %} {# diff --git a/src/Oro/Bundle/EmailBundle/Sync/AbstractEmailSynchronizationProcessor.php b/src/Oro/Bundle/EmailBundle/Sync/AbstractEmailSynchronizationProcessor.php index aa1781d8359..bdb10fa21dc 100644 --- a/src/Oro/Bundle/EmailBundle/Sync/AbstractEmailSynchronizationProcessor.php +++ b/src/Oro/Bundle/EmailBundle/Sync/AbstractEmailSynchronizationProcessor.php @@ -16,6 +16,7 @@ use Oro\Bundle\EmailBundle\Exception\SyncFolderTimeoutException; use Oro\Bundle\EmailBundle\Model\EmailHeader; use Oro\Bundle\EmailBundle\Model\FolderType; +use Oro\Bundle\EmailBundle\Sync\Model\SynchronizationProcessorSettings; use Oro\Bundle\EmailBundle\Tools\EmailAddressHelper; use Oro\Bundle\UserBundle\Entity\User; use Oro\Bundle\OrganizationBundle\Entity\OrganizationInterface; @@ -51,6 +52,9 @@ abstract class AbstractEmailSynchronizationProcessor implements LoggerAwareInter /** @var OrganizationInterface */ protected $currentOrganization; + /** @var SynchronizationProcessorSettings */ + protected $settings; + /** * Constructor * @@ -66,6 +70,7 @@ protected function __construct( $this->em = $em; $this->emailEntityBuilder = $emailEntityBuilder; $this->knownEmailAddressChecker = $knownEmailAddressChecker; + $this->settings = new SynchronizationProcessorSettings(); } /** @@ -451,4 +456,20 @@ private function isMailboxSender($mailboxId, $email) $email->getFrom() ); } + + /** + * @param SynchronizationProcessorSettings $settings + */ + public function setSettings(SynchronizationProcessorSettings $settings) + { + $this->settings = $settings; + } + + /** + * @return SynchronizationProcessorSettings + */ + public function getSettings() + { + return $this->settings; + } } diff --git a/src/Oro/Bundle/EmailBundle/Sync/AbstractEmailSynchronizer.php b/src/Oro/Bundle/EmailBundle/Sync/AbstractEmailSynchronizer.php index 91aac4cf440..d78cf9571a4 100644 --- a/src/Oro/Bundle/EmailBundle/Sync/AbstractEmailSynchronizer.php +++ b/src/Oro/Bundle/EmailBundle/Sync/AbstractEmailSynchronizer.php @@ -17,6 +17,7 @@ use Oro\Bundle\CronBundle\Entity\Manager\JobManager; use Oro\Bundle\EmailBundle\Entity\EmailOrigin; use Oro\Bundle\EmailBundle\Exception\SyncFolderTimeoutException; +use Oro\Bundle\EmailBundle\Sync\Model\SynchronizationProcessorSettings; use Oro\Bundle\OrganizationBundle\Entity\Organization; use Oro\Bundle\SecurityBundle\Authentication\Token\OrganizationToken; @@ -106,6 +107,7 @@ abstract public function supports(EmailOrigin $origin); * @param int $maxTasks The maximum number of email origins which can be synchronized * Set -1 to unlimited * Defaults to 1 + * * @return int * * @throws \Exception @@ -153,7 +155,7 @@ public function sync($maxConcurrentTasks, $minExecIntervalInMin, $maxExecTimeInM $processedOrigins[$origin->getId()] = true; try { - $this->doSyncOrigin($origin); + $this->doSyncOrigin($origin, new SynchronizationProcessorSettings()); } catch (SyncFolderTimeoutException $ex) { break; } catch (\Exception $ex) { @@ -175,9 +177,11 @@ public function sync($maxConcurrentTasks, $minExecIntervalInMin, $maxExecTimeInM * Performs a synchronization of emails for the given email origins. * * @param int[] $originIds + * @param SynchronizationProcessorSettings $settings + * * @throws \Exception */ - public function syncOrigins(array $originIds) + public function syncOrigins(array $originIds, SynchronizationProcessorSettings $settings = null) { if ($this->logger === null) { $this->logger = new NullLogger(); @@ -192,7 +196,7 @@ public function syncOrigins(array $originIds) $origin = $this->findOrigin($originId); if ($origin !== null) { try { - $this->doSyncOrigin($origin); + $this->doSyncOrigin($origin, $settings); } catch (SyncFolderTimeoutException $ex) { break; } catch (\Exception $ex) { @@ -258,9 +262,11 @@ protected function checkConfiguration() * Performs a synchronization of emails for the given email origin. * * @param EmailOrigin $origin + * @param SynchronizationProcessorSettings $settings + * * @throws \Exception */ - protected function doSyncOrigin(EmailOrigin $origin) + protected function doSyncOrigin(EmailOrigin $origin, SynchronizationProcessorSettings $settings = null) { $this->impersonateOrganization($origin->getOrganization()); try { @@ -271,12 +277,17 @@ protected function doSyncOrigin(EmailOrigin $origin) } catch (\Exception $ex) { $this->logger->error(sprintf('Skip origin synchronization. Error: %s', $ex->getMessage())); + $this->setOriginSyncStateToFailed($origin); + throw $ex; } try { if ($this->changeOriginSyncState($origin, self::SYNC_CODE_IN_PROCESS)) { $syncStartTime = $this->getCurrentUtcDateTime(); + if ($settings) { + $processor->setSettings($settings); + } $processor->process($origin, $syncStartTime); $this->changeOriginSyncState($origin, self::SYNC_CODE_SUCCESS, $syncStartTime); } else { @@ -288,15 +299,7 @@ protected function doSyncOrigin(EmailOrigin $origin) throw $ex; } catch (\Exception $ex) { - try { - $this->changeOriginSyncState($origin, self::SYNC_CODE_FAILURE); - } catch (\Exception $innerEx) { - // ignore any exception here - $this->logger->error( - sprintf('Cannot set the fail state. Error: %s', $innerEx->getMessage()), - ['exception' => $innerEx] - ); - } + $this->setOriginSyncStateToFailed($origin); $this->logger->error( sprintf('The synchronization failed. Error: %s', $ex->getMessage()), @@ -406,6 +409,24 @@ protected function changeOriginSyncState(EmailOrigin $origin, $syncCode, $synchr return $affectedRows > 0; } + /** + * Attempts to sets the state of a given email origin to failed. + * + * @param EmailOrigin $origin + */ + protected function setOriginSyncStateToFailed(EmailOrigin $origin) + { + try { + $this->changeOriginSyncState($origin, self::SYNC_CODE_FAILURE); + } catch (\Exception $innerEx) { + // ignore any exception here + $this->logger->error( + sprintf('Cannot set the fail state. Error: %s', $innerEx->getMessage()), + ['exception' => $innerEx] + ); + } + } + /** * Finds an email origin to be synchronised * diff --git a/src/Oro/Bundle/EmailBundle/Sync/Model/SynchronizationProcessorSettings.php b/src/Oro/Bundle/EmailBundle/Sync/Model/SynchronizationProcessorSettings.php new file mode 100644 index 00000000000..45932a896f1 --- /dev/null +++ b/src/Oro/Bundle/EmailBundle/Sync/Model/SynchronizationProcessorSettings.php @@ -0,0 +1,60 @@ +forceMode = $forceMode; + $this->showMessage = $showMessage; + } + + /** + * Set force mode. + * + * @param bool $mode + */ + public function setForceMode($mode) + { + $this->forceMode = $mode; + } + + /** + * Check is force mode enabled. + */ + public function isForceMode() + { + return $this->forceMode === true; + } + + /** + * Set value to show or hide log messages + * + * @param bool $value + */ + public function setShowMessage($value) + { + $this->showMessage = $value; + } + + /** + * Check value is true. + * + * @return bool + */ + public function needShowMessage() + { + return $this->showMessage === true; + } +} diff --git a/src/Oro/Bundle/EmailBundle/Tests/Unit/Datagrid/OriginFolderFilterProviderTest.php b/src/Oro/Bundle/EmailBundle/Tests/Unit/Datagrid/OriginFolderFilterProviderTest.php index 242548fc379..839e9a832ae 100644 --- a/src/Oro/Bundle/EmailBundle/Tests/Unit/Datagrid/OriginFolderFilterProviderTest.php +++ b/src/Oro/Bundle/EmailBundle/Tests/Unit/Datagrid/OriginFolderFilterProviderTest.php @@ -3,6 +3,7 @@ namespace Oro\Bundle\EmailBundle\Tests\Unit\Datagrid; use Doctrine\Bundle\DoctrineBundle\Registry; +use Doctrine\ORM\AbstractQuery; use Oro\Bundle\EmailBundle\Datagrid\OriginFolderFilterProvider; use Oro\Bundle\EmailBundle\Entity\Repository\MailboxRepository; @@ -27,6 +28,9 @@ class OriginFolderFilterProviderTest extends \PHPUnit_Framework_TestCase /** @var \PHPUnit_Framework_MockObject_MockObject */ protected $originRepository; + /** @var \PHPUnit_Framework_MockObject_MockObject|AbstractQuery */ + protected $originQuery; + /** @var OriginFolderFilterProvider */ protected $originFolderFilterProvider; @@ -35,10 +39,38 @@ public function setUp() $this->mailboxRepository = $this->getMockBuilder('Oro\Bundle\EmailBundle\Entity\Repository\MailboxRepository') ->disableOriginalConstructor() ->getMock(); + + $this->originQuery = $this->getMockBuilder('Doctrine\ORM\AbstractQuery') + ->disableOriginalConstructor() + ->setMethods(['getResult']) + ->getMockForAbstractClass(); + + $originQb = $this->getMockBuilder('Doctrine\ORM\QueryBuilder') + ->disableOriginalConstructor() + ->getMock(); + $originQb->expects($this->any()) + ->method('select') + ->will($this->returnSelf()); + $originQb->expects($this->any()) + ->method('leftJoin') + ->will($this->returnSelf()); + $originQb->expects($this->any()) + ->method('andWhere') + ->will($this->returnSelf()); + $originQb->expects($this->any()) + ->method('setParameters') + ->will($this->returnSelf()); + $originQb->expects($this->any()) + ->method('getQuery') + ->will($this->returnValue($this->originQuery)); + $this->originRepository = $this->getMockBuilder('Doctrine\ORM\EntityRepository') ->disableOriginalConstructor() - ->setMethods(['findBy', 'findAvailableMailboxes']) + ->setMethods(['createQueryBuilder', 'findAvailableMailboxes']) ->getMock(); + $this->originRepository->expects($this->any()) + ->method('createQueryBuilder') + ->will($this->returnValue($originQb)); $this->doctrine = $this->getMockBuilder('Doctrine\Bundle\DoctrineBundle\Registry') ->disableOriginalConstructor() @@ -60,9 +92,10 @@ public function testEmptyOrigins() ->method('getRepository') ->will($this->returnValue($this->originRepository)); - $this->originRepository->expects($this->once()) - ->method('findBy') + $this->originQuery->expects($this->once()) + ->method('getResult') ->willReturn([]); + $this->originRepository->expects($this->once()) ->method('findAvailableMailboxes') ->willReturn([]); @@ -100,9 +133,10 @@ public function testPersonalOrigin() ->method('getRepository') ->will($this->returnValue($this->originRepository)); - $this->originRepository->expects($this->once()) - ->method('findBy') + $this->originQuery->expects($this->once()) + ->method('getResult') ->willReturn([$origin1, $origin2]); + $this->originRepository->expects($this->once()) ->method('findAvailableMailboxes') ->willReturn([]); @@ -154,9 +188,10 @@ public function testMailboxOrigins() ->method('getRepository') ->will($this->returnValue($this->originRepository)); - $this->originRepository->expects($this->once()) - ->method('findBy') + $this->originQuery->expects($this->once()) + ->method('getResult') ->willReturn([]); + $this->originRepository->expects($this->once()) ->method('findAvailableMailboxes') ->willReturn([$mailbox1, $mailbox2]); diff --git a/src/Oro/Bundle/EmailBundle/Tests/Unit/Manager/EmailNotificationManagerTest.php b/src/Oro/Bundle/EmailBundle/Tests/Unit/Manager/EmailNotificationManagerTest.php index 8b89ce3d2af..f4b27e28475 100644 --- a/src/Oro/Bundle/EmailBundle/Tests/Unit/Manager/EmailNotificationManagerTest.php +++ b/src/Oro/Bundle/EmailBundle/Tests/Unit/Manager/EmailNotificationManagerTest.php @@ -8,6 +8,7 @@ use Oro\Bundle\EmailBundle\Manager\EmailNotificationManager; use Oro\Bundle\EmailBundle\Tests\Unit\Fixtures\Entity\Email; use Oro\Bundle\EmailBundle\Tests\Unit\Fixtures\Entity\EmailAddress; +use Oro\Bundle\EmailBundle\Tools\EmailBodyHelper; use Oro\Bundle\UIBundle\Tools\HtmlTagHelper; /** @@ -70,48 +71,110 @@ protected function setUp() $this->entityManager, $this->htmlTagHelper, $this->router, - $this->configManager + $this->configManager, + new EmailBodyHelper($this->htmlTagHelper) ); } - public function testGetEmails() + /** + * @dataProvider getEmails + */ + public function testGetEmails($user, $emails, $expectedResult) { - $user = $this->getMockBuilder('Oro\Bundle\UserBundle\Entity\User')->disableOriginalConstructor()->getMock(); $organization = $this->getMockBuilder('Oro\Bundle\OrganizationBundle\Entity\Organization') ->disableOriginalConstructor() ->getMock(); - $testEmails = $this->getEmails($user); - $this->repository->expects($this->once())->method('getNewEmails')->willReturn($testEmails); + $this->repository->expects($this->once())->method('getNewEmails')->willReturn($emails); $maxEmailsDisplay = 1; $emails = $this->emailNotificationManager->getEmails($user, $organization, $maxEmailsDisplay, null); - $this->assertEquals( - [ + $this->assertEquals($expectedResult, $emails); + } + + /** + * @return array + */ + public function getEmails() + { + $user = $this->getMockBuilder('Oro\Bundle\UserBundle\Entity\User')->disableOriginalConstructor()->getMock(); + + $htmlBody = << + + + + + + + +

Lorem ipsum

+dolor sit amet, consectetur adipiscing elit. + + + + + + + + + +
Integersagittis
ornaredo
+ + +EMAILBODY; + + $emails = [ + $this->prepareEmailUser( [ - 'replyRoute' => 'oro_email_email_reply', - 'replyAllRoute' => 'oro_email_email_reply', - 'forwardRoute' => 'oro_email_email_reply', - 'id' => 1, - 'seen' => 0, - 'subject' => 'subject', - 'bodyContent' => 'bodyContent', - 'fromName' => 'fromName', - 'linkFromName' => 'oro_email_email_reply', + 'getId' => 1, + 'getSubject' => 'subject', + 'getFromName' => 'fromName', + 'getBodyContent' => 'bodyContent', ], + $user, + false + ), + $this->prepareEmailUser( [ - 'replyRoute' => 'oro_email_email_reply', - 'replyAllRoute' => 'oro_email_email_reply', - 'forwardRoute' => 'oro_email_email_reply', - 'id' => 2, - 'seen' => 1, - 'subject' => 'subject_1', - 'bodyContent' => 'bodyContent_1', - 'fromName' => 'fromName_1', - 'linkFromName' => 'oro_email_email_reply', - ] + 'getId' => 2, + 'getSubject' => 'subject_1', + 'getBodyContent' => $htmlBody, + 'getFromName' => 'fromName_1', + ], + $user, + true + ) + ]; + + $expectedResult = [ + [ + 'replyRoute' => 'oro_email_email_reply', + 'replyAllRoute' => 'oro_email_email_reply', + 'forwardRoute' => 'oro_email_email_reply', + 'id' => 1, + 'seen' => 0, + 'subject' => 'subject', + 'bodyContent' => 'bodyContent', + 'fromName' => 'fromName', + 'linkFromName' => 'oro_email_email_reply', ], - $emails - ); + [ + 'replyRoute' => 'oro_email_email_reply', + 'replyAllRoute' => 'oro_email_email_reply', + 'forwardRoute' => 'oro_email_email_reply', + 'id' => 2, + 'seen' => 1, + 'subject' => 'subject_1', + 'bodyContent' => 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer sagittis ornare do', + 'fromName' => 'fromName_1', + 'linkFromName' => 'oro_email_email_reply', + ] + ]; + + return [[$user, $emails, $expectedResult]]; } public function testGetCountNewEmails() @@ -125,39 +188,6 @@ public function testGetCountNewEmails() $this->assertEquals(1, $count); } - /** - * @param EmailOwnerInterface $user - * - * @return array - */ - protected function getEmails($user) - { - $firstEmail = $this->prepareEmailUser( - [ - 'getId' => 1, - 'getSubject' => 'subject', - 'getFromName' => 'fromName', - 'getBodyContent' => 'bodyContent', - ], - $user, - false - ); - - $secondEmail = $this->prepareEmailUser( - [ - 'getId' => 2, - 'getSubject' => 'subject_1', - 'getBodyContent' => 'bodyContent_1', - 'getFromName' => 'fromName_1', - - ], - $user, - true - ); - - return [$firstEmail, $secondEmail]; - } - /** * @param array $values * @param EmailOwnerInterface $user diff --git a/src/Oro/Bundle/EmailBundle/Tests/Unit/Provider/EmailActivityListProviderTest.php b/src/Oro/Bundle/EmailBundle/Tests/Unit/Provider/EmailActivityListProviderTest.php index 9f9ef9c3af9..eede652c664 100644 --- a/src/Oro/Bundle/EmailBundle/Tests/Unit/Provider/EmailActivityListProviderTest.php +++ b/src/Oro/Bundle/EmailBundle/Tests/Unit/Provider/EmailActivityListProviderTest.php @@ -3,6 +3,7 @@ namespace Oro\Bundle\EmailBundle\Tests\Unit\Provider; use Oro\Bundle\EmailBundle\Provider\EmailActivityListProvider; +use Oro\Bundle\EmailBundle\Tools\EmailBodyHelper; use Oro\Bundle\OrganizationBundle\Entity\Organization; use Oro\Bundle\UserBundle\Entity\User; use Oro\Bundle\EmailBundle\Entity\EmailUser; @@ -93,7 +94,8 @@ protected function setUp() $this->securityFacadeLink, $this->mailboxProcessStorageLink, $this->activityAssociationHelper, - $this->commentAssociationHelper + $this->commentAssociationHelper, + new EmailBodyHelper($htmlTagHelper) ); $this->emailActivityListProvider->setSecurityContextLink($this->securityContextLink); } diff --git a/src/Oro/Bundle/EmailBundle/Tests/Unit/Sync/Fixtures/TestEmailSynchronizer.php b/src/Oro/Bundle/EmailBundle/Tests/Unit/Sync/Fixtures/TestEmailSynchronizer.php index 4194d5648f7..f49081c62d7 100644 --- a/src/Oro/Bundle/EmailBundle/Tests/Unit/Sync/Fixtures/TestEmailSynchronizer.php +++ b/src/Oro/Bundle/EmailBundle/Tests/Unit/Sync/Fixtures/TestEmailSynchronizer.php @@ -8,6 +8,7 @@ use Oro\Bundle\EmailBundle\Entity\EmailOrigin; use Oro\Bundle\EmailBundle\Sync\KnownEmailAddressCheckerFactory; use Oro\Bundle\EmailBundle\Sync\AbstractEmailSynchronizer; +use Oro\Bundle\EmailBundle\Sync\Model\SynchronizationProcessorSettings; class TestEmailSynchronizer extends AbstractEmailSynchronizer { @@ -56,9 +57,9 @@ public function setCurrentUtcDateTime(\DateTime $now) $this->now = $now; } - public function callDoSyncOrigin(EmailOrigin $origin) + public function callDoSyncOrigin(EmailOrigin $origin, SynchronizationProcessorSettings $settings = null) { - $this->doSyncOrigin($origin); + $this->doSyncOrigin($origin, $settings); } public function callChangeOriginSyncState(EmailOrigin $origin, $syncCode, $synchronizedAt) diff --git a/src/Oro/Bundle/EmailBundle/Tests/Unit/Tools/EmailBodyHelperTest.php b/src/Oro/Bundle/EmailBundle/Tests/Unit/Tools/EmailBodyHelperTest.php new file mode 100644 index 00000000000..0e754fc8c2a --- /dev/null +++ b/src/Oro/Bundle/EmailBundle/Tests/Unit/Tools/EmailBodyHelperTest.php @@ -0,0 +1,59 @@ +getMockBuilder('Oro\Bundle\FormBundle\Provider\HtmlTagProvider') + ->disableOriginalConstructor() + ->getMock(); + $this->htmlTagHelper = new HtmlTagHelper($htmlTagProvider); + $this->bodyHelper = new EmailBodyHelper($this->htmlTagHelper); + } + /** + * @dataProvider bodyData + */ + public function testGetClearBody($bodyText, $expectedResult) + { + $this->assertEquals($expectedResult, $this->bodyHelper->getClearBody($bodyText)); + } + + public function bodyData() + { + $htmlTest = << + some title + + + +

The body text

+ + +HTMLTEXT; + + return [ + 'plain text' => ['test text', 'test text'], + 'text with css' => [ + ' some text', + 'some text' + ], + 'text with javascript' => [ + ' another text', + 'another text' + ], + 'text with body tag' => [$htmlTest, 'The body text'], + 'text with non printed symbols' => ["some\ntext with\tsymbols", 'some text with symbols'] + ]; + } +} diff --git a/src/Oro/Bundle/EmailBundle/Tools/EmailBodyHelper.php b/src/Oro/Bundle/EmailBundle/Tools/EmailBodyHelper.php new file mode 100644 index 00000000000..f14bea08120 --- /dev/null +++ b/src/Oro/Bundle/EmailBundle/Tools/EmailBodyHelper.php @@ -0,0 +1,55 @@ +htmlTagHelper = $htmlTagHelper; + } + + /** + * Returns the plain text representation of email body + * + * @param string $bodyContent + * + * @return string + */ + public function getClearBody($bodyContent) + { + /** + * @todo: Should be refactored or deleted in scope of BAP-11622 + */ + if (extension_loaded('tidy')) { + $config = [ + 'show-body-only' => true, + 'clean' => true, + 'hide-comments' => true + ]; + $tidy = new \tidy(); + $body = $tidy->repairString($bodyContent, $config, 'UTF8'); + } else { + $body = $bodyContent; + // get `body` content in case of html text + if (preg_match('~]*>(.*?)~si', $bodyContent, $bodyText)) { + $body = $bodyText[1]; + } + } + + // clear `script` and `style` tags from content + $body = preg_replace('/<(style|script).*?>.*?<\/\1>/s', '', $body); + + return preg_replace('/(\s\s+|\n+|[^[:print:]])/', ' ', $this->htmlTagHelper->stripTags($body)); + } +} diff --git a/src/Oro/Bundle/EmailBundle/Twig/EmailExtension.php b/src/Oro/Bundle/EmailBundle/Twig/EmailExtension.php index b77c57d55b8..1974095575d 100644 --- a/src/Oro/Bundle/EmailBundle/Twig/EmailExtension.php +++ b/src/Oro/Bundle/EmailBundle/Twig/EmailExtension.php @@ -17,6 +17,7 @@ use Oro\Bundle\EmailBundle\Model\WebSocket\WebSocketSendProcessor; use Oro\Bundle\EmailBundle\Provider\RelatedEmailsProvider; use Oro\Bundle\EmailBundle\Tools\EmailAddressHelper; +use Oro\Bundle\EmailBundle\Tools\EmailBodyHelper; use Oro\Bundle\EmailBundle\Tools\EmailHolderHelper; use Oro\Bundle\SecurityBundle\SecurityFacade; @@ -45,6 +46,9 @@ class EmailExtension extends Twig_Extension /** @var RelatedEmailsProvider */ protected $relatedEmailsProvider; + /** @var EmailBodyHelper */ + protected $emailBodyHelper; + /** * @param EmailHolderHelper $emailHolderHelper * @param EmailAddressHelper $emailAddressHelper @@ -53,6 +57,7 @@ class EmailExtension extends Twig_Extension * @param MailboxProcessStorage $mailboxProcessStorage * @param SecurityFacade $securityFacade * @param RelatedEmailsProvider $relatedEmailsProvider + * @param EmailBodyHelper $emailBodyHelper */ public function __construct( EmailHolderHelper $emailHolderHelper, @@ -61,7 +66,8 @@ public function __construct( EntityManager $em, MailboxProcessStorage $mailboxProcessStorage, SecurityFacade $securityFacade, - RelatedEmailsProvider $relatedEmailsProvider + RelatedEmailsProvider $relatedEmailsProvider, + EmailBodyHelper $emailBodyHelper ) { $this->emailHolderHelper = $emailHolderHelper; $this->emailAddressHelper = $emailAddressHelper; @@ -70,6 +76,7 @@ public function __construct( $this->mailboxProcessStorage = $mailboxProcessStorage; $this->securityFacade = $securityFacade; $this->relatedEmailsProvider = $relatedEmailsProvider; + $this->emailBodyHelper = $emailBodyHelper; } /** @@ -90,6 +97,28 @@ public function getFunctions() ]; } + /** + * {@inheritDoc} + */ + public function getFilters() + { + return [ + new \Twig_SimpleFilter('oro_cleanup_email_body', [$this, 'getCleanEmailBody']) + ]; + } + + /** + * Returns clean text representation without tags + * + * @param string $emailBodyText + * + * @return string + */ + public function getCleanEmailBody($emailBodyText) + { + return $this->emailBodyHelper->getClearBody($emailBodyText); + } + /** * Gets the email address of the given object * diff --git a/src/Oro/Bundle/EmailBundle/composer.json b/src/Oro/Bundle/EmailBundle/composer.json index c70246d9f0e..e84b5f135e7 100644 --- a/src/Oro/Bundle/EmailBundle/composer.json +++ b/src/Oro/Bundle/EmailBundle/composer.json @@ -6,7 +6,7 @@ "license": "MIT", "require": { "php": ">=5.5.9", - "symfony/symfony": "2.8.*", + "symfony/symfony": "2.8.*, !=2.8.10", "doctrine/orm": ">=2.3,<2.4-dev", "a2lix/translation-form-bundle" : "1.x-dev", "oro/attachment-bundle": "dev-master", diff --git a/src/Oro/Bundle/EntityBundle/Tests/Unit/Tools/EntityRoutingHelperTest.php b/src/Oro/Bundle/EntityBundle/Tests/Unit/Tools/EntityRoutingHelperTest.php index 760a1f453f5..5dd7ff2394e 100644 --- a/src/Oro/Bundle/EntityBundle/Tests/Unit/Tools/EntityRoutingHelperTest.php +++ b/src/Oro/Bundle/EntityBundle/Tests/Unit/Tools/EntityRoutingHelperTest.php @@ -273,7 +273,7 @@ public function testGetEntityForNotManageableEntity() /** * @expectedException \Symfony\Component\HttpKernel\Exception\NotFoundHttpException - * @expectedExceptionMessage Record doesn't found. + * @expectedExceptionMessage Record doesn't exist */ public function testGetEntityForNotExistingEntity() { diff --git a/src/Oro/Bundle/EntityBundle/Tools/EntityRoutingHelper.php b/src/Oro/Bundle/EntityBundle/Tools/EntityRoutingHelper.php index 7b2b536aa13..911fb72d0d4 100644 --- a/src/Oro/Bundle/EntityBundle/Tools/EntityRoutingHelper.php +++ b/src/Oro/Bundle/EntityBundle/Tools/EntityRoutingHelper.php @@ -223,7 +223,7 @@ public function getEntity($entityClass, $entityId) } if (!$entity) { throw new RecordNotFoundException( - sprintf("Record doesn't found.") + sprintf("Record doesn't exist") ); } diff --git a/src/Oro/Bundle/EntityBundle/composer.json b/src/Oro/Bundle/EntityBundle/composer.json index 033f0b153a9..afb1903aba4 100644 --- a/src/Oro/Bundle/EntityBundle/composer.json +++ b/src/Oro/Bundle/EntityBundle/composer.json @@ -7,7 +7,7 @@ "license": "MIT", "require": { "php": ">=5.5.9", - "symfony/symfony": "2.8.*", + "symfony/symfony": "2.8.*, !=2.8.10", "oro/entity-config-bundle": "dev-master", "oro/ui-bundle": "dev-master", "oro/grid-bundle": "dev-master", diff --git a/src/Oro/Bundle/EntityConfigBundle/composer.json b/src/Oro/Bundle/EntityConfigBundle/composer.json index 420fd8c5362..73a1e487a76 100644 --- a/src/Oro/Bundle/EntityConfigBundle/composer.json +++ b/src/Oro/Bundle/EntityConfigBundle/composer.json @@ -7,7 +7,7 @@ "license": "MIT", "require": { "php": ">=5.5.9", - "symfony/symfony": "2.8.*", + "symfony/symfony": "2.8.*, !=2.8.10", "oro/config": "dev-master", "oro/log": "dev-master", "oro/cache-bundle": "dev-master", diff --git a/src/Oro/Bundle/EntityExtendBundle/Grid/AbstractFieldsExtension.php b/src/Oro/Bundle/EntityExtendBundle/Grid/AbstractFieldsExtension.php index 830071c3c4d..c4301dbc50d 100644 --- a/src/Oro/Bundle/EntityExtendBundle/Grid/AbstractFieldsExtension.php +++ b/src/Oro/Bundle/EntityExtendBundle/Grid/AbstractFieldsExtension.php @@ -52,7 +52,7 @@ public function __construct( */ public function isApplicable(DatagridConfiguration $config) { - return $config->getDatasourceType() == OrmDatasource::TYPE; + return $config->getDatasourceType() === OrmDatasource::TYPE; } /** diff --git a/src/Oro/Bundle/EntityExtendBundle/Grid/DynamicFieldsExtension.php b/src/Oro/Bundle/EntityExtendBundle/Grid/DynamicFieldsExtension.php index 6a64fe7f40a..70c978dde1f 100644 --- a/src/Oro/Bundle/EntityExtendBundle/Grid/DynamicFieldsExtension.php +++ b/src/Oro/Bundle/EntityExtendBundle/Grid/DynamicFieldsExtension.php @@ -12,7 +12,7 @@ class DynamicFieldsExtension extends AbstractFieldsExtension { - const EXTEND_ENTITY_CONFIG_PATH = '[extended_entity_name]'; + const EXTEND_ENTITY_CONFIG_PATH = 'extended_entity_name'; /** * {@inheritdoc} @@ -21,7 +21,7 @@ public function isApplicable(DatagridConfiguration $config) { return parent::isApplicable($config) - && $config->offsetGetByPath(self::EXTEND_ENTITY_CONFIG_PATH, false) !== false; + && $config->offsetGetOr(self::EXTEND_ENTITY_CONFIG_PATH, false) !== false; } /** @@ -37,7 +37,7 @@ public function getPriority() */ protected function getEntityName(DatagridConfiguration $config) { - return $config->offsetGetByPath(self::EXTEND_ENTITY_CONFIG_PATH); + return $config->offsetGetOr(self::EXTEND_ENTITY_CONFIG_PATH); } /** diff --git a/src/Oro/Bundle/EntityExtendBundle/Migration/EntityMetadataHelper.php b/src/Oro/Bundle/EntityExtendBundle/Migration/EntityMetadataHelper.php index e93ee41f499..39be5cef177 100644 --- a/src/Oro/Bundle/EntityExtendBundle/Migration/EntityMetadataHelper.php +++ b/src/Oro/Bundle/EntityExtendBundle/Migration/EntityMetadataHelper.php @@ -14,9 +14,9 @@ class EntityMetadataHelper protected $doctrine; /** - * @var string[] {table name} => {class name} + * @var array {table name} => [{class name}, ...] */ - protected $tableToClassMap; + protected $tableToClassesMap; /** * @var string[] {class name} => {table name} @@ -32,18 +32,38 @@ public function __construct(ManagerRegistry $doctrine) } /** + * @deprecated Use getEntityClassesByTableNames instead + * * Gets an entity full class name by entity table name * * @param string $tableName * @return string|null */ public function getEntityClassByTableName($tableName) + { + $classes = $this->getEntityClassesByTableName($tableName); + if (count($classes) > 1) { + throw new \RuntimeException(sprintf( + 'Table "%s" has more than 1 class. Use "getEntityClassesByTableNames" method instead.', + $tableName + )); + } + + return reset($classes) ?: null; + } + + /** + * @param string $tableName + * + * @return string[] + */ + public function getEntityClassesByTableName($tableName) { $this->ensureNameMapsLoaded(); - return isset($this->tableToClassMap[$tableName]) - ? $this->tableToClassMap[$tableName] - : null; + return isset($this->tableToClassesMap[$tableName]) + ? $this->tableToClassesMap[$tableName] + : []; } /** @@ -70,8 +90,8 @@ public function getTableNameByEntityClass($className) */ public function getFieldNameByColumnName($tableName, $columnName) { - $className = $this->getEntityClassByTableName($tableName); - if ($className) { + $classNames = $this->getEntityClassesByTableName($tableName); + foreach ($classNames as $className) { $manager = $this->doctrine->getManagerForClass($className); if ($manager instanceof EntityManager) { return $manager->getClassMetadata($className)->getFieldName($columnName); @@ -93,7 +113,7 @@ public function registerEntityClass($tableName, $className) { $this->ensureNameMapsLoaded(); - $this->tableToClassMap[$tableName] = $className; + $this->tableToClassesMap[$tableName][] = $className; $this->classToTableMap[$className] = $tableName; } @@ -102,7 +122,7 @@ public function registerEntityClass($tableName, $className) */ protected function ensureNameMapsLoaded() { - if (null === $this->tableToClassMap) { + if (null === $this->tableToClassesMap) { $this->loadNameMaps(); } } @@ -112,7 +132,7 @@ protected function ensureNameMapsLoaded() */ protected function loadNameMaps() { - $this->tableToClassMap = []; + $this->tableToClassesMap = []; $this->classToTableMap = []; $names = array_keys($this->doctrine->getManagers()); foreach ($names as $name) { @@ -123,7 +143,7 @@ protected function loadNameMaps() $tableName = $metadata->getTableName(); if (!empty($tableName)) { $className = $metadata->getName(); - $this->tableToClassMap[$tableName] = $className; + $this->tableToClassesMap[$tableName][] = $className; $this->classToTableMap[$className] = $tableName; } } diff --git a/src/Oro/Bundle/EntityExtendBundle/Migration/ExtendOptionsBuilder.php b/src/Oro/Bundle/EntityExtendBundle/Migration/ExtendOptionsBuilder.php index 78402cefbbd..9c8288ebd67 100644 --- a/src/Oro/Bundle/EntityExtendBundle/Migration/ExtendOptionsBuilder.php +++ b/src/Oro/Bundle/EntityExtendBundle/Migration/ExtendOptionsBuilder.php @@ -2,6 +2,7 @@ namespace Oro\Bundle\EntityExtendBundle\Migration; +use Oro\Bundle\EntityConfigBundle\Config\ConfigManager; use Oro\Bundle\EntityExtendBundle\Extend\FieldTypeHelper; use Oro\Bundle\EntityExtendBundle\Extend\RelationType; use Oro\Bundle\EntityExtendBundle\Tools\ExtendHelper; @@ -14,8 +15,11 @@ class ExtendOptionsBuilder /** @var FieldTypeHelper */ protected $fieldTypeHelper; + /** @var ConfigManager */ + protected $configManager; + /** @var array */ - protected $tableToEntityMap = []; + protected $tableToEntitiesMap = []; /** @var array */ protected $result = []; @@ -23,13 +27,16 @@ class ExtendOptionsBuilder /** * @param EntityMetadataHelper $entityMetadataHelper * @param FieldTypeHelper $fieldTypeHelper + * @param ConfigManager $configManager */ public function __construct( EntityMetadataHelper $entityMetadataHelper, - FieldTypeHelper $fieldTypeHelper + FieldTypeHelper $fieldTypeHelper, + ConfigManager $configManager ) { $this->entityMetadataHelper = $entityMetadataHelper; $this->fieldTypeHelper = $fieldTypeHelper; + $this->configManager = $configManager; } /** @@ -47,18 +54,22 @@ public function getOptions() public function addTableOptions($tableName, array $options) { $customEntityClassName = $this->getAndRemoveOption($options, ExtendOptionsManager::ENTITY_CLASS_OPTION); - $entityClassName = $this->getEntityClassName($tableName, $customEntityClassName, false); - if (!$entityClassName) { + $entityClassNames = $this->getEntityClassNames($tableName, $customEntityClassName, false); + if (!$entityClassNames) { return; } $tableMode = $this->getAndRemoveOption($options, ExtendOptionsManager::MODE_OPTION); if (!empty($options)) { - $this->result[$entityClassName]['configs'] = $options; + foreach ($entityClassNames as $entityClassName) { + $this->result[$entityClassName]['configs'] = $options; + } } if ($tableMode) { - $this->result[$entityClassName]['mode'] = $tableMode; + foreach ($entityClassNames as $entityClassName) { + $this->result[$entityClassName]['mode'] = $tableMode; + } } } @@ -72,17 +83,19 @@ public function addTableOptions($tableName, array $options) */ public function addColumnOptions($tableName, $columnName, $options) { - $entityClassName = $this->getEntityClassName($tableName, null, false); - if (!$entityClassName) { + $entityClassNames = $this->getEntityClassNames($tableName, null, false); + if (!$entityClassNames) { return; } $newColumnName = $this->getAndRemoveOption($options, ExtendOptionsManager::NEW_NAME_OPTION); if ($newColumnName) { - $this->result[ExtendConfigProcessor::RENAME_CONFIGS][$entityClassName][$columnName] = $newColumnName; + foreach ($entityClassNames as $entityClassName) { + $this->result[ExtendConfigProcessor::RENAME_CONFIGS][$entityClassName][$columnName] = $newColumnName; + } if (empty($options)) { return; - }; + } } $fieldName = $this->getAndRemoveOption($options, ExtendOptionsManager::FIELD_NAME_OPTION); @@ -101,7 +114,15 @@ public function addColumnOptions($tableName, $columnName, $options) foreach ($target as $optionName => $optionValue) { switch ($optionName) { case 'table_name': - $options['extend']['target_entity'] = $this->getEntityClassName($optionValue); + $targetEntityNames = $this->getEntityClassNames($optionValue); + if (count($targetEntityNames) > 1) { + throw new \LogicException(sprintf( + 'Table "%s" is expected to be related with 1 entity, but %d entities found', + $optionValue, + count($entityClassNames) + )); + } + $options['extend']['target_entity'] = reset($targetEntityNames); break; case 'column': $options['extend']['target_field'] = $this->getFieldName($target['table_name'], $optionValue); @@ -122,8 +143,15 @@ public function addColumnOptions($tableName, $columnName, $options) } if (!isset($options['extend']['relation_key'])) { + if (count($entityClassNames) > 1) { + throw new \LogicException(sprintf( + 'Table "%s" is expected to be related with 1 entity, but %d entities found', + $tableName, + count($entityClassNames) + )); + } $options['extend']['relation_key'] = ExtendHelper::buildRelationKey( - $entityClassName, + reset($entityClassNames), $fieldName, $columnUnderlyingType, $options['extend']['target_entity'] @@ -131,15 +159,17 @@ public function addColumnOptions($tableName, $columnName, $options) } } - $this->result[$entityClassName]['fields'][$fieldName] = []; - if (!empty($options)) { - $this->result[$entityClassName]['fields'][$fieldName]['configs'] = $options; - } - if ($columnType) { - $this->result[$entityClassName]['fields'][$fieldName]['type'] = $columnType; - } - if ($columnMode) { - $this->result[$entityClassName]['fields'][$fieldName]['mode'] = $columnMode; + foreach ($entityClassNames as $entityClassName) { + $this->result[$entityClassName]['fields'][$fieldName] = []; + if (!empty($options)) { + $this->result[$entityClassName]['fields'][$fieldName]['configs'] = $options; + } + if ($columnType) { + $this->result[$entityClassName]['fields'][$fieldName]['type'] = $columnType; + } + if ($columnMode) { + $this->result[$entityClassName]['fields'][$fieldName]['mode'] = $columnMode; + } } } @@ -150,12 +180,14 @@ public function addColumnOptions($tableName, $columnName, $options) */ public function addTableAuxiliaryOptions($configType, $tableName, $options) { - $entityClassName = $this->getEntityClassName($tableName, null, false); - if (!$entityClassName) { + $entityClassNames = $this->getEntityClassNames($tableName, null, false); + if (!$entityClassNames) { return; } - $this->result[$configType][$entityClassName]['configs'] = $options; + foreach ($entityClassNames as $entityClassName) { + $this->result[$configType][$entityClassName]['configs'] = $options; + } } /** @@ -166,14 +198,16 @@ public function addTableAuxiliaryOptions($configType, $tableName, $options) */ public function addColumnAuxiliaryOptions($configType, $tableName, $columnName, $options) { - $entityClassName = $this->getEntityClassName($tableName, null, false); - if (!$entityClassName) { + $entityClassNames = $this->getEntityClassNames($tableName, null, false); + if (!$entityClassNames) { return; } $fieldName = $this->getFieldName($tableName, $columnName); - $this->result[$configType][$entityClassName]['fields'][$fieldName] = $options; + foreach ($entityClassNames as $entityClassName) { + $this->result[$configType][$entityClassName]['fields'][$fieldName] = $options; + } } /** @@ -200,23 +234,26 @@ public function getAuxiliaryConfigType($sectionName) * @param string $customEntityClassName The name of a custom entity * @param bool $throwExceptionIfNotFound * - * @return string|null + * @return string[] * * @throws \RuntimeException if an entity class name was not found and $throwExceptionIfNotFound = TRUE */ - protected function getEntityClassName($tableName, $customEntityClassName = null, $throwExceptionIfNotFound = true) + protected function getEntityClassNames($tableName, $customEntityClassName = null, $throwExceptionIfNotFound = true) { - if (!isset($this->tableToEntityMap[$tableName])) { - $entityClassName = !empty($customEntityClassName) - ? $customEntityClassName - : $this->entityMetadataHelper->getEntityClassByTableName($tableName); - if ($throwExceptionIfNotFound && empty($entityClassName)) { - throw new \RuntimeException(sprintf('Cannot find entity for "%s" table.', $tableName)); + if (!isset($this->tableToEntitiesMap[$tableName])) { + $entityClassNames = !empty($customEntityClassName) + ? [$customEntityClassName] + : array_filter( + $this->entityMetadataHelper->getEntityClassesByTableName($tableName), + [$this->configManager, 'hasConfig'] + ); + if ($throwExceptionIfNotFound && empty($entityClassNames)) { + throw new \RuntimeException(sprintf('Cannot find configurable entity for "%s" table.', $tableName)); } - $this->tableToEntityMap[$tableName] = $entityClassName; + $this->tableToEntitiesMap[$tableName] = $entityClassNames; } - $result = $this->tableToEntityMap[$tableName]; + $result = $this->tableToEntitiesMap[$tableName]; if ($throwExceptionIfNotFound && empty($result)) { throw new \RuntimeException(sprintf('Cannot find entity for "%s" table.', $tableName)); } diff --git a/src/Oro/Bundle/EntityExtendBundle/Migration/ExtendOptionsParser.php b/src/Oro/Bundle/EntityExtendBundle/Migration/ExtendOptionsParser.php index 93d12b6aedf..72e415cd1d6 100644 --- a/src/Oro/Bundle/EntityExtendBundle/Migration/ExtendOptionsParser.php +++ b/src/Oro/Bundle/EntityExtendBundle/Migration/ExtendOptionsParser.php @@ -2,6 +2,7 @@ namespace Oro\Bundle\EntityExtendBundle\Migration; +use Oro\Bundle\EntityConfigBundle\Config\ConfigManager; use Oro\Bundle\EntityExtendBundle\Extend\FieldTypeHelper; class ExtendOptionsParser @@ -12,16 +13,22 @@ class ExtendOptionsParser /** @var FieldTypeHelper */ protected $fieldTypeHelper; + /** @var ConfigManager */ + protected $configManager; + /** * @param EntityMetadataHelper $entityMetadataHelper * @param FieldTypeHelper $fieldTypeHelper + * @param ConfigManager $configManager */ public function __construct( EntityMetadataHelper $entityMetadataHelper, - FieldTypeHelper $fieldTypeHelper + FieldTypeHelper $fieldTypeHelper, + ConfigManager $configManager ) { $this->entityMetadataHelper = $entityMetadataHelper; $this->fieldTypeHelper = $fieldTypeHelper; + $this->configManager = $configManager; } /** @@ -32,7 +39,7 @@ public function __construct( */ public function parseOptions(array $options) { - $builder = new ExtendOptionsBuilder($this->entityMetadataHelper, $this->fieldTypeHelper); + $builder = new ExtendOptionsBuilder($this->entityMetadataHelper, $this->fieldTypeHelper, $this->configManager); $objectKeys = array_filter( array_keys($options), diff --git a/src/Oro/Bundle/EntityExtendBundle/Migration/UpdateExtendIndicesMigration.php b/src/Oro/Bundle/EntityExtendBundle/Migration/UpdateExtendIndicesMigration.php index 927d898cfd7..3312e8d162a 100644 --- a/src/Oro/Bundle/EntityExtendBundle/Migration/UpdateExtendIndicesMigration.php +++ b/src/Oro/Bundle/EntityExtendBundle/Migration/UpdateExtendIndicesMigration.php @@ -112,8 +112,8 @@ public function up(Schema $schema, QueryBag $queries) */ protected function processColumn(Schema $schema, QueryBag $queries, $tableName, $columnName, $options) { - $className = $this->entityMetadataHelper->getEntityClassByTableName($tableName); - if (null === $className) { + $classNames = $this->entityMetadataHelper->getEntityClassesByTableName($tableName); + if (!$classNames) { return; } @@ -124,11 +124,15 @@ protected function processColumn(Schema $schema, QueryBag $queries, $tableName, if (!isset($options[ExtendOptionsManager::NEW_NAME_OPTION])) { if (isset($options[ExtendOptionsManager::TYPE_OPTION])) { - $this->buildIndex($columnName, $options, $className, $table); + foreach ($classNames as $className) { + $this->buildIndex($columnName, $options, $className, $table); + } } } else { // in case of renaming column name we should rename existing index - $this->renameIndex($schema, $queries, $tableName, $columnName, $options, $className, $table); + foreach ($classNames as $className) { + $this->renameIndex($schema, $queries, $tableName, $columnName, $options, $className, $table); + } } } diff --git a/src/Oro/Bundle/EntityExtendBundle/Resources/config/services.yml b/src/Oro/Bundle/EntityExtendBundle/Resources/config/services.yml index 79296c8eb84..3405842431d 100644 --- a/src/Oro/Bundle/EntityExtendBundle/Resources/config/services.yml +++ b/src/Oro/Bundle/EntityExtendBundle/Resources/config/services.yml @@ -268,6 +268,7 @@ services: arguments: - "@oro_entity_extend.migration.entity_metadata_helper" - "@oro_entity_extend.extend.field_type_helper" + - "@oro_entity_config.config_manager" oro_entity_extend.migration.extension.extend: class: %oro_entity_extend.migration.extension.extend.class% diff --git a/src/Oro/Bundle/EntityExtendBundle/Tests/Unit/Migration/EntityMetadataHelperTest.php b/src/Oro/Bundle/EntityExtendBundle/Tests/Unit/Migration/EntityMetadataHelperTest.php index c4e34d791bb..3a4a2dd0b05 100644 --- a/src/Oro/Bundle/EntityExtendBundle/Tests/Unit/Migration/EntityMetadataHelperTest.php +++ b/src/Oro/Bundle/EntityExtendBundle/Tests/Unit/Migration/EntityMetadataHelperTest.php @@ -2,6 +2,8 @@ namespace Oro\Bundle\EntityExtendBundle\Tests\Unit\Migration; +use Doctrine\ORM\Mapping\ClassMetadataInfo; + use Oro\Bundle\EntityExtendBundle\Migration\EntityMetadataHelper; class EntityMetadataHelperTest extends \PHPUnit_Framework_TestCase @@ -26,15 +28,8 @@ protected function setUp() public function testGetEntityClassByTableName() { - $metadata = $this->getMockBuilder('Doctrine\ORM\Mapping\ClassMetadata') - ->disableOriginalConstructor() - ->getMock(); - $metadata->expects($this->once()) - ->method('getTableName') - ->will($this->returnValue('acme_test')); - $metadata->expects($this->once()) - ->method('getName') - ->will($this->returnValue('Oro\Bundle\EntityBundle\Tests\Unit\ORM\Fixtures\TestEntity')); + $metadata = new ClassMetadataInfo('Oro\Bundle\EntityBundle\Tests\Unit\ORM\Fixtures\TestEntity'); + $metadata->table['name'] = 'acme_test'; $metadataFactory = $this->getMockBuilder('Doctrine\ORM\Mapping\ClassMetadataFactory') ->disableOriginalConstructor() @@ -64,17 +59,48 @@ public function testGetEntityClassByTableName() ); } - public function testGetTableNameByEntityClass() + public function testGetEntityClassesByTableName() { - $metadata = $this->getMockBuilder('Doctrine\ORM\Mapping\ClassMetadata') + $testEntityMetadata = new ClassMetadataInfo('Oro\Bundle\EntityBundle\Tests\Unit\ORM\Fixtures\TestEntity'); + $testEntityMetadata->table['name'] = 'acme_test'; + $testEntity2Metadata = new ClassMetadataInfo('Oro\Bundle\EntityBundle\Tests\Unit\ORM\Fixtures\TestEntity2'); + $testEntity2Metadata->table['name'] = 'acme_test'; + + $metadataFactory = $this->getMockBuilder('Doctrine\ORM\Mapping\ClassMetadataFactory') + ->disableOriginalConstructor() + ->getMock(); + $metadataFactory->expects($this->once()) + ->method('getAllMetadata') + ->will($this->returnValue([$testEntityMetadata, $testEntity2Metadata])); + + $em = $this->getMockBuilder('Doctrine\ORM\EntityManager') ->disableOriginalConstructor() ->getMock(); - $metadata->expects($this->once()) - ->method('getTableName') - ->will($this->returnValue('acme_test')); - $metadata->expects($this->once()) - ->method('getName') - ->will($this->returnValue('Oro\Bundle\EntityBundle\Tests\Unit\ORM\Fixtures\TestEntity')); + $em->expects($this->once()) + ->method('getMetadataFactory') + ->will($this->returnValue($metadataFactory)); + + $this->doctrine->expects($this->once()) + ->method('getManagers') + ->will($this->returnValue(array('default' => $em))); + $this->doctrine->expects($this->once()) + ->method('getManager') + ->with($this->equalTo('default')) + ->will($this->returnValue($em)); + + $this->assertEquals( + [ + 'Oro\Bundle\EntityBundle\Tests\Unit\ORM\Fixtures\TestEntity', + 'Oro\Bundle\EntityBundle\Tests\Unit\ORM\Fixtures\TestEntity2', + ], + $this->helper->getEntityClassesByTableName('acme_test') + ); + } + + public function testGetTableNameByEntityClass() + { + $metadata = new ClassMetadataInfo('Oro\Bundle\EntityBundle\Tests\Unit\ORM\Fixtures\TestEntity'); + $metadata->table['name'] = 'acme_test'; $metadataFactory = $this->getMockBuilder('Doctrine\ORM\Mapping\ClassMetadataFactory') ->disableOriginalConstructor() @@ -106,19 +132,9 @@ public function testGetTableNameByEntityClass() public function testGetFieldNameByColumnName() { - $metadata = $this->getMockBuilder('Doctrine\ORM\Mapping\ClassMetadata') - ->disableOriginalConstructor() - ->getMock(); - $metadata->expects($this->once()) - ->method('getTableName') - ->will($this->returnValue('acme_test')); - $metadata->expects($this->once()) - ->method('getFieldName') - ->with('name_column') - ->will($this->returnValue('name_field')); - $metadata->expects($this->once()) - ->method('getName') - ->will($this->returnValue('Oro\Bundle\EntityBundle\Tests\Unit\ORM\Fixtures\TestEntity')); + $metadata = new ClassMetadataInfo('Oro\Bundle\EntityBundle\Tests\Unit\ORM\Fixtures\TestEntity'); + $metadata->table['name'] = 'acme_test'; + $metadata->fieldNames['name_column'] = 'name_field'; $metadataFactory = $this->getMockBuilder('Doctrine\ORM\Mapping\ClassMetadataFactory') ->disableOriginalConstructor() diff --git a/src/Oro/Bundle/EntityExtendBundle/Tests/Unit/Migration/ExtendOptionsParserTest.php b/src/Oro/Bundle/EntityExtendBundle/Tests/Unit/Migration/ExtendOptionsParserTest.php index a10dc95eaf1..16b43ccae99 100644 --- a/src/Oro/Bundle/EntityExtendBundle/Tests/Unit/Migration/ExtendOptionsParserTest.php +++ b/src/Oro/Bundle/EntityExtendBundle/Tests/Unit/Migration/ExtendOptionsParserTest.php @@ -24,17 +24,24 @@ protected function setUp() ->getMock(); $this->entityMetadataHelper->expects($this->any()) - ->method('getEntityClassByTableName') + ->method('getEntityClassesByTableName') ->willReturnMap( [ - ['table1', 'Test\Entity1'], - ['table2', 'Test\Entity2'], + ['table1', ['Test\Entity1']], + ['table2', ['Test\Entity2']], ] ); + $configManager = $this->getMockBuilder('Oro\Bundle\EntityConfigBundle\Config\ConfigManager') + ->disableOriginalConstructor() + ->getMock(); + $configManager->expects($this->any()) + ->method('hasConfig') + ->will($this->returnValue(true)); $this->extendOptionsParser = new ExtendOptionsParser( $this->entityMetadataHelper, - new FieldTypeHelper(['enum' => 'manyToOne', 'multiEnum' => 'manyToMany']) + new FieldTypeHelper(['enum' => 'manyToOne', 'multiEnum' => 'manyToMany']), + $configManager ); } diff --git a/src/Oro/Bundle/EntityExtendBundle/Tests/Unit/Migration/Extension/ExtendExtensionTest.php b/src/Oro/Bundle/EntityExtendBundle/Tests/Unit/Migration/Extension/ExtendExtensionTest.php index e1bdf23ef8f..7b74e3df936 100644 --- a/src/Oro/Bundle/EntityExtendBundle/Tests/Unit/Migration/Extension/ExtendExtensionTest.php +++ b/src/Oro/Bundle/EntityExtendBundle/Tests/Unit/Migration/Extension/ExtendExtensionTest.php @@ -49,13 +49,31 @@ protected function setUp() ] ) ); + $this->entityMetadataHelper->expects($this->any()) + ->method('getEntityClassesByTableName') + ->will( + $this->returnValueMap( + [ + ['table1', ['Acme\AcmeBundle\Entity\Entity1']], + ['table2', ['Acme\AcmeBundle\Entity\Entity2']], + ['oro_enum_test_enum', [ExtendHelper::ENTITY_NAMESPACE . 'EV_Test_Enum']], + ] + ) + ); $this->entityMetadataHelper->expects($this->any()) ->method('getFieldNameByColumnName') ->will($this->returnArgument(1)); $this->extendOptionsManager = new ExtendOptionsManager(); + $configManager = $this->getMockBuilder('Oro\Bundle\EntityConfigBundle\Config\ConfigManager') + ->disableOriginalConstructor() + ->getMock(); + $configManager->expects($this->any()) + ->method('hasConfig') + ->will($this->returnValue(true)); $this->extendOptionsParser = new ExtendOptionsParser( $this->entityMetadataHelper, - new FieldTypeHelper(['enum' => 'manyToOne', 'multiEnum' => 'manyToMany']) + new FieldTypeHelper(['enum' => 'manyToOne', 'multiEnum' => 'manyToMany']), + $configManager ); } diff --git a/src/Oro/Bundle/EntityExtendBundle/Tests/Unit/Migration/Schema/ExtendSchemaTest.php b/src/Oro/Bundle/EntityExtendBundle/Tests/Unit/Migration/Schema/ExtendSchemaTest.php index e8634a66957..cfc41e2f6f7 100644 --- a/src/Oro/Bundle/EntityExtendBundle/Tests/Unit/Migration/Schema/ExtendSchemaTest.php +++ b/src/Oro/Bundle/EntityExtendBundle/Tests/Unit/Migration/Schema/ExtendSchemaTest.php @@ -38,18 +38,25 @@ protected function setUp() ->disableOriginalConstructor() ->getMock(); $this->entityMetadataHelper->expects($this->any()) - ->method('getEntityClassByTableName') + ->method('getEntityClassesByTableName') ->will( $this->returnValueMap( [ - ['table1', 'Acme\AcmeBundle\Entity\Entity1'] + ['table1', ['Acme\AcmeBundle\Entity\Entity1']], ] ) ); $this->extendOptionsManager = new ExtendOptionsManager(); + $configManager = $this->getMockBuilder('Oro\Bundle\EntityConfigBundle\Config\ConfigManager') + ->disableOriginalConstructor() + ->getMock(); + $configManager->expects($this->any()) + ->method('hasConfig') + ->will($this->returnValue(true)); $this->extendOptionsParser = new ExtendOptionsParser( $this->entityMetadataHelper, - new FieldTypeHelper(['enum' => 'manyToOne', 'multiEnum' => 'manyToMany']) + new FieldTypeHelper(['enum' => 'manyToOne', 'multiEnum' => 'manyToMany']), + $configManager ); $this->nameGenerator = new ExtendDbIdentifierNameGenerator(); } diff --git a/src/Oro/Bundle/EntityExtendBundle/composer.json b/src/Oro/Bundle/EntityExtendBundle/composer.json index ae88cde4989..fb8ec907898 100644 --- a/src/Oro/Bundle/EntityExtendBundle/composer.json +++ b/src/Oro/Bundle/EntityExtendBundle/composer.json @@ -7,7 +7,7 @@ "license": "MIT", "require": { "php": ">=5.5.9", - "symfony/symfony": "2.8.*", + "symfony/symfony": "2.8.*, !=2.8.10", "oro/log": "dev-master", "oro/ui-bundle": "dev-master", "oro/grid-bundle": "dev-master", diff --git a/src/Oro/Bundle/EntityMergeBundle/DataGrid/Extension/MassAction/MergeMassAction.php b/src/Oro/Bundle/EntityMergeBundle/DataGrid/Extension/MassAction/MergeMassAction.php index 52ff9506265..17e9064a917 100644 --- a/src/Oro/Bundle/EntityMergeBundle/DataGrid/Extension/MassAction/MergeMassAction.php +++ b/src/Oro/Bundle/EntityMergeBundle/DataGrid/Extension/MassAction/MergeMassAction.php @@ -6,43 +6,44 @@ use Oro\Bundle\DataGridBundle\Extension\Action\ActionConfiguration; use Oro\Bundle\DataGridBundle\Extension\MassAction\Actions\AbstractMassAction; -use Oro\Bundle\EntityMergeBundle\Metadata\MetadataRegistry; +use Oro\Bundle\EntityConfigBundle\Provider\ConfigProvider; +use Oro\Bundle\EntityMergeBundle\Metadata\EntityMetadata; class MergeMassAction extends AbstractMassAction { - /** - * @var MetadataRegistry - */ - protected $metadataRegistry; + /** @var ConfigProvider */ + protected $entityConfigProvider; - /** - * @var TranslatorInterface - */ + /** @var TranslatorInterface */ protected $translator; /** - * @param MetadataRegistry $metadataRegistry + * @param ConfigProvider $entityConfigProvider * @param TranslatorInterface $translator */ - public function __construct(MetadataRegistry $metadataRegistry, TranslatorInterface $translator) - { - $this->metadataRegistry = $metadataRegistry; - $this->translator = $translator; + public function __construct( + ConfigProvider $entityConfigProvider, + TranslatorInterface $translator + ) { + parent::__construct(); + + $this->entityConfigProvider = $entityConfigProvider; + $this->translator = $translator; } /** @var array */ protected $requiredOptions = ['route', 'entity_name', 'data_identifier', 'max_element_count']; /** @var array */ - protected $defaultOptions = array( - 'frontend_handle' => 'redirect', - 'handler' => 'oro_entity_merge.mass_action.data_handler', - 'icon' => 'random', - 'frontend_type' => 'merge-mass', - 'route' => 'oro_entity_merge_massaction', - 'data_identifier' => 'id', - 'route_parameters' => array(), - ); + protected $defaultOptions = [ + 'frontend_handle' => 'redirect', + 'handler' => 'oro_entity_merge.mass_action.data_handler', + 'icon' => 'random', + 'frontend_type' => 'merge-mass', + 'route' => 'oro_entity_merge_massaction', + 'data_identifier' => 'id', + 'route_parameters' => [], + ]; /** * {@inheritdoc} @@ -52,17 +53,17 @@ public function setOptions(ActionConfiguration $options) $this->setDefaultOptions($options); if (isset($options['entity_name'])) { - $metadata = $this - ->metadataRegistry - ->getEntityMetadata($options['entity_name']); - - $options['max_element_count'] = $metadata->getMaxEntitiesCount(); + $entityConfig = $this->entityConfigProvider->getConfig($options['entity_name']); + $options['max_element_count'] = $entityConfig->get( + 'max_element_count', + false, + EntityMetadata::MAX_ENTITIES_COUNT + ); - $options['label'] = $this->translator - ->trans( - 'oro.entity_merge.action.merge', - ['{{ label }}' => $this->translator->trans($metadata->get('label'))] - ); + $options['label'] = $this->translator->trans( + 'oro.entity_merge.action.merge', + ['{{ label }}' => $this->translator->trans($entityConfig->get('label'))] + ); } return parent::setOptions($options); diff --git a/src/Oro/Bundle/EntityMergeBundle/EventListener/DataGrid/MergeMassActionListener.php b/src/Oro/Bundle/EntityMergeBundle/EventListener/DataGrid/MergeMassActionListener.php index a4d1ee242a8..0ff845c7c98 100644 --- a/src/Oro/Bundle/EntityMergeBundle/EventListener/DataGrid/MergeMassActionListener.php +++ b/src/Oro/Bundle/EntityMergeBundle/EventListener/DataGrid/MergeMassActionListener.php @@ -3,21 +3,19 @@ namespace Oro\Bundle\EntityMergeBundle\EventListener\DataGrid; use Oro\Bundle\DataGridBundle\Event\BuildBefore; -use Oro\Bundle\EntityMergeBundle\Metadata\MetadataRegistry; +use Oro\Bundle\EntityConfigBundle\Provider\ConfigProvider; class MergeMassActionListener { - /** - * @var MetadataRegistry - */ - protected $metadataRegistry; + /** @var ConfigProvider */ + protected $entityConfigProvider; /** - * @param MetadataRegistry $metadataRegistry + * @param ConfigProvider $entityConfigProvider */ - public function __construct(MetadataRegistry $metadataRegistry) + public function __construct(ConfigProvider $entityConfigProvider) { - $this->metadataRegistry = $metadataRegistry; + $this->entityConfigProvider = $entityConfigProvider; } /** @@ -28,18 +26,15 @@ public function __construct(MetadataRegistry $metadataRegistry) public function onBuildBefore(BuildBefore $event) { $config = $event->getConfig(); - - $massActions = isset($config['mass_actions']) ? $config['mass_actions'] : array(); - - if (empty($massActions['merge']['entity_name'])) { + if (!isset($config['mass_actions']) + || empty($config['mass_actions']['merge']['entity_name']) + ) { return; } - $entityName = $massActions['merge']['entity_name']; - - $entityMergeEnable = $this->metadataRegistry->getEntityMetadata($entityName)->is('enable'); - - if (!$entityMergeEnable) { + $entityClassName = $config['mass_actions']['merge']['entity_name']; + $entityMergeEnabled = $this->entityConfigProvider->getConfig($entityClassName)->is('enable'); + if (!$entityMergeEnabled) { $config->offsetUnsetByPath('[mass_actions][merge]'); } } diff --git a/src/Oro/Bundle/EntityMergeBundle/Resources/config/mass_action.yml b/src/Oro/Bundle/EntityMergeBundle/Resources/config/mass_action.yml index 9f8bcd4ea0f..faefe9f0d11 100644 --- a/src/Oro/Bundle/EntityMergeBundle/Resources/config/mass_action.yml +++ b/src/Oro/Bundle/EntityMergeBundle/Resources/config/mass_action.yml @@ -6,7 +6,7 @@ services: oro_entity_merge.mass_action.merge: class: %oro_entity_merge.mass_action.merge.class% arguments: - - '@oro_entity_merge.metadata.registry' + - '@oro_entity_config.provider.merge' - '@translator' scope: prototype tags: @@ -20,6 +20,6 @@ services: oro_entity_merge.mass_action.merge_mass_action_listener: class: %oro_entity_merge.mass_action.merge_mass_action_listener.class% arguments: - - '@oro_entity_merge.metadata.registry' + - '@oro_entity_config.provider.merge' tags: - { name: kernel.event_listener, event: oro_datagrid.datagrid.build.before, method: onBuildBefore } diff --git a/src/Oro/Bundle/EntityMergeBundle/Tests/Unit/DataGrid/Extension/MassAction/MergeMassActionTest.php b/src/Oro/Bundle/EntityMergeBundle/Tests/Unit/DataGrid/Extension/MassAction/MergeMassActionTest.php index 5d5cec96c2a..0a680ac8397 100644 --- a/src/Oro/Bundle/EntityMergeBundle/Tests/Unit/DataGrid/Extension/MassAction/MergeMassActionTest.php +++ b/src/Oro/Bundle/EntityMergeBundle/Tests/Unit/DataGrid/Extension/MassAction/MergeMassActionTest.php @@ -3,6 +3,7 @@ namespace Oro\Bundle\EntityMergeBundle\Tests\Unit\DataGrid\Extension\MassAction; use Oro\Bundle\DataGridBundle\Extension\Action\ActionConfiguration; +use Oro\Bundle\EntityConfigBundle\Config\Config as EntityConfig; use Oro\Bundle\EntityMergeBundle\DataGrid\Extension\MassAction\MergeMassAction; class MergeMassActionTest extends \PHPUnit_Framework_TestCase @@ -16,29 +17,21 @@ class MergeMassActionTest extends \PHPUnit_Framework_TestCase protected function setUp() { - $metadata = $this - ->getMockBuilder('Oro\Bundle\EntityMergeBundle\Metadata\EntityMetadata') + $entityConfigProvider = $this->getMockBuilder('Oro\Bundle\EntityConfigBundle\Provider\ConfigProvider') ->disableOriginalConstructor() ->getMock(); - - $metadata - ->expects($this->any()) - ->method('getMaxEntitiesCount') - ->will($this->returnValue(self::MAX_ENTITIES_COUNT)); - - $metadataRegistry = $this - ->getMockBuilder('Oro\Bundle\EntityMergeBundle\Metadata\MetadataRegistry') - ->disableOriginalConstructor() - ->getMock(); - - $metadataRegistry - ->expects($this->any()) - ->method('getEntityMetadata') - ->will($this->returnValue($metadata)); + $entityConfig = new EntityConfig( + $this->getMock('Oro\Bundle\EntityConfigBundle\Config\Id\ConfigIdInterface'), + ['max_element_count' => self::MAX_ENTITIES_COUNT] + ); + $entityConfigProvider->expects($this->any()) + ->method('getConfig') + ->with('SomeEntityClass') + ->willReturn($entityConfig); $translator = $this->getMock('Symfony\Component\Translation\TranslatorInterface'); - $this->target = new MergeMassAction($metadataRegistry, $translator); + $this->target = new MergeMassAction($entityConfigProvider, $translator); } /** diff --git a/src/Oro/Bundle/EntityMergeBundle/Tests/Unit/DependencyInjection/OroEntityMergeExtensionTest.php b/src/Oro/Bundle/EntityMergeBundle/Tests/Unit/DependencyInjection/OroEntityMergeExtensionTest.php index 36915f9e49e..8f57a27bfb9 100644 --- a/src/Oro/Bundle/EntityMergeBundle/Tests/Unit/DependencyInjection/OroEntityMergeExtensionTest.php +++ b/src/Oro/Bundle/EntityMergeBundle/Tests/Unit/DependencyInjection/OroEntityMergeExtensionTest.php @@ -81,7 +81,7 @@ public function loadServiceDataProvider() 'service' => 'oro_entity_merge.mass_action.merge', 'class' => '%oro_entity_merge.mass_action.merge.class%', 'arguments' => array( - new Reference('oro_entity_merge.metadata.registry'), + new Reference('oro_entity_config.provider.merge'), new Reference('translator') ), 'tags' => array( diff --git a/src/Oro/Bundle/EntityMergeBundle/Tests/Unit/EventListener/DataGrid/MergeMassActionListenerTest.php b/src/Oro/Bundle/EntityMergeBundle/Tests/Unit/EventListener/DataGrid/MergeMassActionListenerTest.php index c830bfaedc4..42004581750 100644 --- a/src/Oro/Bundle/EntityMergeBundle/Tests/Unit/EventListener/DataGrid/MergeMassActionListenerTest.php +++ b/src/Oro/Bundle/EntityMergeBundle/Tests/Unit/EventListener/DataGrid/MergeMassActionListenerTest.php @@ -2,6 +2,9 @@ namespace Oro\Bundle\EntityMergeBundle\Tests\Unit\EventListener\DataGrid; +use Oro\Bundle\DataGridBundle\Datagrid\Common\DatagridConfiguration; +use Oro\Bundle\DataGridBundle\Event\BuildBefore; +use Oro\Bundle\EntityConfigBundle\Config\Config as EntityConfig; use Oro\Bundle\EntityMergeBundle\EventListener\DataGrid\MergeMassActionListener; class MergeMassActionListenerTest extends \PHPUnit_Framework_TestCase @@ -9,158 +12,115 @@ class MergeMassActionListenerTest extends \PHPUnit_Framework_TestCase /** * @var MergeMassActionListener */ - private $target; + private $mergeMassActionListener; /** * @var \PHPUnit_Framework_MockObject_MockObject */ - private $entityMetadata; - - /** - * @var \PHPUnit_Framework_MockObject_MockObject - */ - private $buildBefore; - - /** - * @var \PHPUnit_Framework_MockObject_MockObject - */ - private $datagridConfig; - - /** - * @var \PHPUnit_Framework_MockObject_MockObject - */ - private $metadataRegistry; - - /** - * @var string - */ - private $entityName; - - /** - * @var array - */ - private $config; + private $entityConfigProvider; protected function setUp() { - $this->metadataRegistry = $this->getMockBuilder('Oro\Bundle\EntityMergeBundle\Metadata\MetadataRegistry') - ->disableOriginalConstructor() - ->getMock(); - - $this->entityMetadata = $this->getMockBuilder('Oro\Bundle\EntityMergeBundle\Metadata\EntityMetadata') - ->disableOriginalConstructor() - ->getMock(); - - $this->buildBefore = $this->getMockBuilder('Oro\Bundle\DataGridBundle\Event\BuildBefore') - ->disableOriginalConstructor() - ->getMock(); - - $this->entityName = 'testEntityName'; - $this->config = array('mass_actions' => array('merge' => array('entity_name' => $this->entityName))); - - $this->datagridConfig = $this->getMockBuilder('Oro\Bundle\DataGridBundle\Datagrid\Common\DatagridConfiguration') + $this->entityConfigProvider = $this->getMockBuilder('Oro\Bundle\EntityConfigBundle\Provider\ConfigProvider') ->disableOriginalConstructor() ->getMock(); - $this->buildBefore->expects($this->any()) - ->method('getConfig') - ->will($this->returnValue($this->datagridConfig)); - - $this->target = new MergeMassActionListener($this->metadataRegistry); + $this->mergeMassActionListener = new MergeMassActionListener($this->entityConfigProvider); } public function testOnBuildUnsetMergeMassAction() { - $this->init(); + $entityName = 'testEntityName'; + $datagridConfig = DatagridConfiguration::create( + ['mass_actions' => ['merge' => ['entity_name' => $entityName]]] + ); + $entityConfig = $this->getEntityConfig(); - $this->entityMetadata->expects($this->once()) - ->method('is') - ->with('enable', true) - ->will($this->returnValue(false)); + $this->entityConfigProvider->expects($this->once()) + ->method('getConfig') + ->with($entityName) + ->willReturn($entityConfig); - $this->datagridConfig->expects($this->once()) - ->method('offsetUnsetByPath') - ->with('[mass_actions][merge]'); + $event = $this->getBuildBeforeEvent($datagridConfig); + $this->mergeMassActionListener->onBuildBefore($event); - $this->target->onBuildBefore($this->buildBefore); + $this->assertEquals( + ['mass_actions' => []], + $datagridConfig->toArray() + ); } public function testOnBuildNotUnsetMergeMass() { - $this->init(); - - $this->entityMetadata->expects($this->once()) - ->method('is') - ->with('enable', true) - ->will($this->returnValue(true)); + $entityName = 'testEntityName'; + $datagridConfig = DatagridConfiguration::create( + ['mass_actions' => ['merge' => ['entity_name' => $entityName]]] + ); + $entityConfig = $this->getEntityConfig(['enable' => true]); - $this->datagridConfig->expects($this->never()) - ->method('offsetUnsetByPath') - ->withAnyParameters(); + $this->entityConfigProvider->expects($this->once()) + ->method('getConfig') + ->with($entityName) + ->willReturn($entityConfig); + $event = $this->getBuildBeforeEvent($datagridConfig); + $this->mergeMassActionListener->onBuildBefore($event); - $this->target->onBuildBefore($this->buildBefore); + $this->assertEquals( + ['mass_actions' => ['merge' => ['entity_name' => $entityName]]], + $datagridConfig->toArray() + ); } public function testOnBuildBeforeSkipsForEmptyMassActions() { - $this->initDatagridConfig(array('mass_actions' => array())); + $datagridConfig = DatagridConfiguration::create( + ['mass_actions' => []] + ); - $this->metadataRegistry->expects($this->never()) - ->method('getEntityMetadata') - ->withAnyParameters(); + $this->entityConfigProvider->expects($this->never()) + ->method('getConfig'); - $this->target->onBuildBefore($this->buildBefore); + $event = $this->getBuildBeforeEvent($datagridConfig); + $this->mergeMassActionListener->onBuildBefore($event); } public function testOnBuildBeforeForEmptyEntityName() { - $this->initDatagridConfig(array('mass_actions' => array('merge' => array('entity_name' => '')))); + $datagridConfig = DatagridConfiguration::create( + ['mass_actions' => ['merge' => ['entity_name' => '']]] + ); - $this->metadataRegistry->expects($this->never()) - ->method('getEntityMetadata') - ->withAnyParameters(); - - $this->target->onBuildBefore($this->buildBefore); - } + $this->entityConfigProvider->expects($this->never()) + ->method('getConfig'); - protected function initMetadataRegistry() - { - $this->metadataRegistry->expects($this->any()) - ->method('getEntityMetadata') - ->will($this->returnValue($this->entityMetadata)); + $event = $this->getBuildBeforeEvent($datagridConfig); + $this->mergeMassActionListener->onBuildBefore($event); } - protected function initDatagridConfig($offsetResult = null) + /** + * @param DatagridConfiguration $datagridConfig + * + * @return BuildBefore + */ + protected function getBuildBeforeEvent(DatagridConfiguration $datagridConfig) { - $rawConfig = $this->config; - $offsetResult = $offsetResult === null ? $this->config['mass_actions'] : $offsetResult; - - $this->datagridConfig->expects($this->any()) - ->method('offsetExists') - ->with('mass_actions') - ->will( - $this->returnCallback( - function ($offset) use ($rawConfig) { - return isset($rawConfig[$offset]); - } - ) - ); - - $this->datagridConfig->expects($this->any()) - ->method('offsetGet') - ->with('mass_actions') - ->will($this->returnValue($offsetResult)); - - $this->datagridConfig->expects($this->any()) - ->method('offsetGet') - ->with('mass_actions') - ->will($this->returnValue($offsetResult)); + return new BuildBefore( + $this->getMock('Oro\Bundle\DataGridBundle\Datagrid\DatagridInterface'), + $datagridConfig + ); } - protected function init() + /** + * @param array $values + * + * @return EntityConfig + */ + protected function getEntityConfig(array $values = []) { - $this->initMetadataRegistry(); - $this->initDatagridConfig(); + return new EntityConfig( + $this->getMock('Oro\Bundle\EntityConfigBundle\Config\Id\ConfigIdInterface'), + $values + ); } } diff --git a/src/Oro/Bundle/EntityMergeBundle/composer.json b/src/Oro/Bundle/EntityMergeBundle/composer.json index 83e3c01ca45..b46d6a72ea6 100644 --- a/src/Oro/Bundle/EntityMergeBundle/composer.json +++ b/src/Oro/Bundle/EntityMergeBundle/composer.json @@ -7,7 +7,7 @@ "license": "MIT", "require": { "php": ">=5.5.9", - "symfony/symfony": "2.8.*", + "symfony/symfony": "2.8.*, !=2.8.10", "oro/entity-config-bundle": "dev-master", "oro/ui-bundle": "dev-master", "oro/grid-bundle": "dev-master", diff --git a/src/Oro/Bundle/EntityPaginationBundle/Datagrid/EntityPaginationExtension.php b/src/Oro/Bundle/EntityPaginationBundle/Datagrid/EntityPaginationExtension.php index 34c66e03246..1bc047eeb16 100644 --- a/src/Oro/Bundle/EntityPaginationBundle/Datagrid/EntityPaginationExtension.php +++ b/src/Oro/Bundle/EntityPaginationBundle/Datagrid/EntityPaginationExtension.php @@ -22,7 +22,7 @@ public function isApplicable(DatagridConfiguration $config) return false; } - return $config->getDatasourceType() == OrmDatasource::TYPE; + return $config->getDatasourceType() === OrmDatasource::TYPE; } /** diff --git a/src/Oro/Bundle/FilterBundle/Filter/AbstractFilter.php b/src/Oro/Bundle/FilterBundle/Filter/AbstractFilter.php index a3de85c794f..eb74de8e53d 100644 --- a/src/Oro/Bundle/FilterBundle/Filter/AbstractFilter.php +++ b/src/Oro/Bundle/FilterBundle/Filter/AbstractFilter.php @@ -13,6 +13,10 @@ use Oro\Component\DoctrineUtils\ORM\QueryUtils; use Oro\Component\PhpUtils\ArrayUtil; +/** + * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) + * @todo refactor in BAP-11688 + */ abstract class AbstractFilter implements FilterInterface { /** @var FormFactoryInterface */ @@ -102,10 +106,25 @@ public function apply(FilterDatasourceAdapterInterface $ds, $data) $subQb ->resetDqlPart('orderBy') ->select($fieldExpr) - ->andWhere($comparisonExpr); - $dql = $this->createDQLWithReplacedAliases($ds, $subQb); - - $subExprs[] = $joinOperator ? $qb->expr()->notIn($fieldExpr, $dql) : $qb->expr()->in($fieldExpr, $dql); + ->andWhere($comparisonExpr) + ->andWhere(sprintf('%1$s = %1$s', $fieldExpr)); + $groupBy = implode(', ', $this->getSelectFieldFromGroupBy($qb)); + if ($groupBy) { + // replace aliases from SELECT by expressions, since SELECT clause is changed + $subQb->groupBy($groupBy); + } + list($dql, $replacements) = $this->createDQLWithReplacedAliases($ds, $subQb); + list($fieldAlias, $field) = explode('.', $fieldExpr); + $replacedFieldExpr = sprintf('%s.%s', $replacements[$fieldAlias], $field); + $oldExpr = sprintf('%1$s = %1$s', $replacedFieldExpr); + $newExpr = sprintf('%s = %s', $replacedFieldExpr, $fieldExpr); + $dql = strtr($dql, [$oldExpr => $newExpr]); + + $subExpr = $qb->expr()->exists($dql); + if ($joinOperator) { + $subExpr = $qb->expr()->not($subExpr); + } + $subExprs[] = $subExpr; } $this->applyFilterToClause($ds, call_user_func_array([$qb->expr(), 'andX'], $subExprs)); } else { @@ -348,7 +367,7 @@ protected function getJoinOperator($operator) * @param FilterDatasourceAdapterInterface $ds * @param QueryBuilder $qb * - * @return string + * @return [$dql, $replacedAliases] */ protected function createDQLWithReplacedAliases(FilterDatasourceAdapterInterface $ds, QueryBuilder $qb) { @@ -359,20 +378,13 @@ function ($alias) use ($ds) { $ds->generateParameterName($this->getName()), ]; }, - $qb->getAllAliases() + QueryUtils::getDqlAliases($qb->getDQL()) ); - return array_reduce( - $replacements, - function ($carry, array $replacement) { - /* - * Replaces old parameter names by newly generated parameter names, so that we don't have - * conflicts in the query. - */ - return preg_replace(sprintf('/(?<=[^\w\.\:])%s(?=\b)/', $replacement[0]), $replacement[1], $carry); - }, - $qb->getDql() - ); + return [ + QueryUtils::replaceDqlAliases($qb->getDQL(), $replacements), + array_combine(array_column($replacements, 0), array_column($replacements, 1)) + ]; } /** @@ -398,7 +410,7 @@ protected function findRelatedJoin(FilterDatasourceAdapterInterface $ds) */ protected function createConditionFieldExprs(QueryBuilder $qb) { - $groupByFields = $this->getSelectFieldFromGroupBy($qb->getDqlPart('groupBy')); + $groupByFields = $this->getSelectFieldFromGroupBy($qb); if ($groupByFields) { return $groupByFields; } @@ -415,37 +427,47 @@ protected function createConditionFieldExprs(QueryBuilder $qb) } /** - * @param Expr\GroupBy[] $groupBy + * @param QueryBuilder $qb * * @return array */ - protected function getSelectFieldFromGroupBy(array $groupBy) + protected function getSelectFieldFromGroupBy(QueryBuilder $qb) { + $groupBy = $qb->getDQLPart('groupBy'); + $expressions = []; foreach ($groupBy as $groupByPart) { foreach ($groupByPart->getParts() as $part) { - $expressions = array_merge($expressions, $this->getSelectFieldFromGroupByPart($part)); + $expressions = array_merge($expressions, $this->getSelectFieldFromGroupByPart($qb, $part)); } } - return $expressions; + $fields = []; + foreach ($expressions as $expression) { + $fields[] = QueryUtils::getSelectExprByAlias($qb, $expression) ?: $expression; + } + + return $fields; } /** - * @param string $groupByPart + * @param QueryBuilder $qb + * @param string $groupByPart * * @return array */ - protected function getSelectFieldFromGroupByPart($groupByPart) + protected function getSelectFieldFromGroupByPart(QueryBuilder $qb, $groupByPart) { $expressions = []; if (strpos($groupByPart, ',') !== false) { $groupByParts = explode(',', $groupByPart); foreach ($groupByParts as $part) { - $expressions = array_merge($expressions, $this->getSelectFieldFromGroupByPart($part)); + $expressions = array_merge($expressions, $this->getSelectFieldFromGroupByPart($qb, $part)); } } else { - $expressions[] = trim($groupByPart); + $trimmedGroupByPart = trim($groupByPart); + $expr = QueryUtils::getSelectExprByAlias($qb, $groupByPart); + $expressions[] = $expr ?: $trimmedGroupByPart; } return $expressions; diff --git a/src/Oro/Bundle/FilterBundle/Grid/Extension/OrmFilterExtension.php b/src/Oro/Bundle/FilterBundle/Grid/Extension/OrmFilterExtension.php index 35ffa932bf0..7add3ad93b2 100644 --- a/src/Oro/Bundle/FilterBundle/Grid/Extension/OrmFilterExtension.php +++ b/src/Oro/Bundle/FilterBundle/Grid/Extension/OrmFilterExtension.php @@ -2,7 +2,6 @@ namespace Oro\Bundle\FilterBundle\Grid\Extension; -use Oro\Bundle\DataGridBundle\Extension\GridViews\GridViewsExtension; use Symfony\Component\Translation\TranslatorInterface; use Oro\Bundle\DataGridBundle\Datagrid\Common\MetadataObject; @@ -13,8 +12,6 @@ use Oro\Bundle\DataGridBundle\Extension\Formatter\Configuration as FormatterConfiguration; use Oro\Bundle\DataGridBundle\Extension\Formatter\Property\PropertyInterface; use Oro\Bundle\DataGridBundle\Datagrid\ParameterBag; -use Oro\Bundle\DataGridBundle\Extension\Pager\PagerInterface; -use Oro\Bundle\DataGridBundle\Extension\Sorter\OrmSorterExtension; use Oro\Bundle\DataGridBundle\Provider\ConfigurationProvider; use Oro\Bundle\FilterBundle\Filter\FilterUtility; use Oro\Bundle\FilterBundle\Filter\FilterInterface; @@ -59,7 +56,7 @@ public function isApplicable(DatagridConfiguration $config) return false; } - return $config->getDatasourceType() == OrmDatasource::TYPE; + return $config->getDatasourceType() === OrmDatasource::TYPE; } /** diff --git a/src/Oro/Bundle/FilterBundle/Resources/public/js/filter/dictionary-filter.js b/src/Oro/Bundle/FilterBundle/Resources/public/js/filter/dictionary-filter.js index 6fea153d13c..093424cc7de 100644 --- a/src/Oro/Bundle/FilterBundle/Resources/public/js/filter/dictionary-filter.js +++ b/src/Oro/Bundle/FilterBundle/Resources/public/js/filter/dictionary-filter.js @@ -339,6 +339,8 @@ define(function(require) { * @return {*} */ setValue: function(value) { + this.preloadSelectedData(value); + var oldValue = this.value; this.value = tools.deepClone(value); this.$(this.elementSelector).inputWidget('data', this.getDataForSelect2()); @@ -353,6 +355,33 @@ define(function(require) { return this; }, + /** + * Preloads selectedData with available data from select2 so that we don't have to + * make additional requests + */ + preloadSelectedData: function(value) { + if (!this.isInitSelect2 || !value.value) { + return; + } + + var data = this.$(this.elementSelector).inputWidget('data'); + _.each(value.value, function(id) { + if (this.selectedData[id]) { + return; + } + + var item = _.find(data, function(item) { + return item.id === id; + }); + + if (!item) { + return; + } + + this.selectedData[item.id] = item; + }, this); + }, + /** * @inheritDoc */ diff --git a/src/Oro/Bundle/FilterBundle/composer.json b/src/Oro/Bundle/FilterBundle/composer.json index 3726056e2d1..f43ed4a903d 100644 --- a/src/Oro/Bundle/FilterBundle/composer.json +++ b/src/Oro/Bundle/FilterBundle/composer.json @@ -7,7 +7,7 @@ "license": "MIT", "require": { "php": ">=5.5.9", - "symfony/symfony": "2.8.*", + "symfony/symfony": "2.8.*, !=2.8.10", "oro/ui-bundle": "dev-master", "oro/requirejs-bundle": "dev-master", "oro/locale-bundle": "dev-master", diff --git a/src/Oro/Bundle/FormBundle/Form/DataTransformer/DurationToStringTransformer.php b/src/Oro/Bundle/FormBundle/Form/DataTransformer/DurationToStringTransformer.php index d48904b17c2..1738a833b6c 100644 --- a/src/Oro/Bundle/FormBundle/Form/DataTransformer/DurationToStringTransformer.php +++ b/src/Oro/Bundle/FormBundle/Form/DataTransformer/DurationToStringTransformer.php @@ -21,9 +21,16 @@ * Parts are rounded up (to int). All PHP string-to-int conversion rules apply. * Invalid numbers result to 0's (1a:b2:3.5m => 1:0:4). Missing leading zeros are also valid (1: => 0:1:0). * In both styles time parts are cumulative, so '1m 119.5s' (or '1:119.5') becomes 3 min. + * + * Supports "," and "." as decimal delimiter */ class DurationToStringTransformer implements DataTransformerInterface { + const DURATION_JIRA_REGEX = '/^ + (?:(?:(\d+(?:[\.,]\d{0,2})?)?)h + (?:[\s]*|$))?(?:(?:(\d+(?:[\.,]\d{0,2})?)?)m + (?:[\s]*|$))?(?:(?:(\d+(?:[\.,]\d{0,2})?)?)s)? + $/ix'; /** * {@inheritdoc} */ @@ -80,17 +87,11 @@ private function getTimeParts($time) $time = trim((string)$time); // matches JIRA style string - $regex = '/^' . - '(?:(?:(\d+(?:\.\d)?)?)h(?:[\s]*|$))?' . - '(?:(?:(\d+(?:\.\d)?)?)m(?:[\s]*|$))?' . - '(?:(?:(\d+(?:\.\d)?)?)s)?' . - '$/i'; - - if (preg_match_all($regex, $time, $matches)) { + if (preg_match_all(self::DURATION_JIRA_REGEX, $time, $matches)) { return [ - 'h' => $matches[1][0], - 'm' => $matches[2][0], - 's' => $matches[3][0], + 'h' => $this->getFloat($matches[1][0]), + 'm' => $this->getFloat($matches[2][0]), + 's' => $this->getFloat($matches[3][0]), ]; } @@ -99,12 +100,24 @@ private function getTimeParts($time) $parts = array_pad(explode(':', $time), -3, 0); return [ - 'h' => (float)$parts[0], - 'm' => (float)$parts[1], - 's' => round($parts[2]), + 'h' => $this->getFloat($parts[0]), + 'm' => $this->getFloat($parts[1]), + 's' => round($this->getFloat($parts[2])), ]; } + /** + * Returns float from a string. Supports either ',' or '.' as decimal delimiter. + * + * @param string $string + * + * @return float + */ + private function getFloat($string) + { + return (float) str_replace(',', '.', $string); + } + /** * Convert \DateInterval to JIRA style encoded time string (1h 2m 3s). Zero values are omitted. * diff --git a/src/Oro/Bundle/FormBundle/Form/DataTransformer/SanitizeHTMLTransformer.php b/src/Oro/Bundle/FormBundle/Form/DataTransformer/SanitizeHTMLTransformer.php index d300b4c6453..d30e2ad4cdf 100644 --- a/src/Oro/Bundle/FormBundle/Form/DataTransformer/SanitizeHTMLTransformer.php +++ b/src/Oro/Bundle/FormBundle/Form/DataTransformer/SanitizeHTMLTransformer.php @@ -12,6 +12,11 @@ class SanitizeHTMLTransformer implements DataTransformerInterface const SUB_DIR = 'ezyang'; const MODE = 0775; + /** + * @var \HtmlPurifier|null + */ + protected $htmlPurifier; + /** * @var string|null */ @@ -55,18 +60,24 @@ public function reverseTransform($value) */ protected function sanitize($value) { - $config = \HTMLPurifier_Config::createDefault(); - $this->fillAllowedElementsConfig($config); - $this->fillCacheConfig($config); - // add inline data support - $config->set( - 'URI.AllowedSchemes', - ['http' => true, 'https' => true, 'mailto' => true, 'ftp' => true, 'data' => true, 'tel' => true] - ); - $config->set('Attr.AllowedFrameTargets', ['_blank']); - $purifier = new \HTMLPurifier($config); - - return $purifier->purify($value); + if (!$value) { + return $value; + } + + if (!$this->htmlPurifier) { + $config = \HTMLPurifier_Config::createDefault(); + $this->fillAllowedElementsConfig($config); + $this->fillCacheConfig($config); + // add inline data support + $config->set( + 'URI.AllowedSchemes', + ['http' => true, 'https' => true, 'mailto' => true, 'ftp' => true, 'data' => true, 'tel' => true] + ); + $config->set('Attr.AllowedFrameTargets', ['_blank']); + $this->htmlPurifier = new \HTMLPurifier($config); + } + + return $this->htmlPurifier->purify($value); } /** diff --git a/src/Oro/Bundle/FormBundle/Form/Type/OroDurationType.php b/src/Oro/Bundle/FormBundle/Form/Type/OroDurationType.php index eef8847087d..0483553fb9e 100644 --- a/src/Oro/Bundle/FormBundle/Form/Type/OroDurationType.php +++ b/src/Oro/Bundle/FormBundle/Form/Type/OroDurationType.php @@ -21,6 +21,15 @@ class OroDurationType extends AbstractType { const NAME = 'oro_duration'; + const VALIDATION_REGEX_JIRA = '/^ + (?:(?:(\d+(?:[\.,]\d{0,2})?)?)h + (?:[\s]*|$))?(?:(?:(\d+(?:[\.,]\d{0,2})?)?)m + (?:[\s]*|$))?(?:(?:(\d+(?:[\.,]\d{0,2})?)?)s?)? + $/ix'; + + const VALIDATION_REGEX_COLUMN = '/^ + ((\d{1,3}:)?\d{1,3}:)?\d{1,3} + $/ix'; /** * {@inheritdoc} @@ -53,18 +62,8 @@ public function preSubmit(FormEvent $event) */ protected function isValidDuration($value) { - $regexJIRAFormat = - '/^' . - '(?:(?:(\d+(?:\.\d)?)?)h(?:[\s]*|$))?' . - '(?:(?:(\d+(?:\.\d)?)?)m(?:[\s]*|$))?' . - '(?:(?:(\d+(?:\.\d)?)?)s?)?' . - '$/i'; - $regexColumnFormat = - '/^' . - '((\d{1,3}:)?\d{1,3}:)?\d{1,3}' . - '$/i'; - - return preg_match($regexJIRAFormat, $value) || preg_match($regexColumnFormat, $value); + return preg_match(self::VALIDATION_REGEX_JIRA, $value) || + preg_match(self::VALIDATION_REGEX_COLUMN, $value); } /** diff --git a/src/Oro/Bundle/FormBundle/Resources/config/requirejs.yml b/src/Oro/Bundle/FormBundle/Resources/config/requirejs.yml index 91fddcf3f03..c4aa3368b79 100644 --- a/src/Oro/Bundle/FormBundle/Resources/config/requirejs.yml +++ b/src/Oro/Bundle/FormBundle/Resources/config/requirejs.yml @@ -49,6 +49,7 @@ config: 'oroform/js/validator/time': 'bundles/oroform/js/validator/time.js' 'oroform/js/validator/type': 'bundles/oroform/js/validator/type.js' 'oroform/js/validator/url': 'bundles/oroform/js/validator/url.js' + 'oroform/js/optional-validation-handler': 'bundles/oroform/js/optional-validation-handler.js' #inline editing 'oroform/js/tools/frontend-type-map': 'bundles/oroform/js/tools/frontend-type-map.js' diff --git a/src/Oro/Bundle/FormBundle/Resources/public/css/less/inline-editing.less b/src/Oro/Bundle/FormBundle/Resources/public/css/less/inline-editing.less index 07164d30ca5..3a6008e45b0 100644 --- a/src/Oro/Bundle/FormBundle/Resources/public/css/less/inline-editing.less +++ b/src/Oro/Bundle/FormBundle/Resources/public/css/less/inline-editing.less @@ -189,6 +189,11 @@ .timepicker-input { width: 121px; } + .fields-row { + display: -webkit-flex; + display: -ms-flexbox; + display: flex + } } &.select-editor { min-width: 180px; diff --git a/src/Oro/Bundle/FormBundle/Resources/public/js/extend/validate.js b/src/Oro/Bundle/FormBundle/Resources/public/js/extend/validate.js index 5ac040851cb..606426ad001 100644 --- a/src/Oro/Bundle/FormBundle/Resources/public/js/extend/validate.js +++ b/src/Oro/Bundle/FormBundle/Resources/public/js/extend/validate.js @@ -4,7 +4,7 @@ define([ 'orotranslation/js/translator', 'oroui/js/tools', 'oroui/js/tools/logger', - './../optional-validation-handler', + 'oroform/js/optional-validation-handler', 'jquery.validate' ], function($, _, __, tools, logger, validationHandler) { 'use strict'; diff --git a/src/Oro/Bundle/FormBundle/Resources/public/js/optional-validation-handler.js b/src/Oro/Bundle/FormBundle/Resources/public/js/optional-validation-handler.js index eed9ecd521d..5a819551dd6 100644 --- a/src/Oro/Bundle/FormBundle/Resources/public/js/optional-validation-handler.js +++ b/src/Oro/Bundle/FormBundle/Resources/public/js/optional-validation-handler.js @@ -82,8 +82,9 @@ define(['jquery'], function($) { * @constructor */ initialize: function(formElement) { - var groups = formElement.find('[data-validation-optional-group]'); var self = this; + + var groups = formElement.find('[data-validation-optional-group]'); var labels = groups.find('label[data-required]'); labels.find('em').hide().html('*'); diff --git a/src/Oro/Bundle/FormBundle/Tests/Unit/Form/DataTransformer/DurationToStringTransformerTest.php b/src/Oro/Bundle/FormBundle/Tests/Unit/Form/DataTransformer/DurationToStringTransformerTest.php index 1c777c2fcaa..1aad7699904 100644 --- a/src/Oro/Bundle/FormBundle/Tests/Unit/Form/DataTransformer/DurationToStringTransformerTest.php +++ b/src/Oro/Bundle/FormBundle/Tests/Unit/Form/DataTransformer/DurationToStringTransformerTest.php @@ -121,7 +121,11 @@ public function reverseTransformDataProvider() 3723, // '01:02:03' ], 'JIRA style all parts with fractions' => [ - '1.5h 2.5m 3.5s', + '1.5h 2.25m 3.5s', + 5539, // '01:32:19' rounded + ], + 'JIRA style all parts with comma fractions' => [ + '1,5h 2.5m 3,5s', 5554, // '01:32:34' rounded ], 'JIRA style no spaces fractions' => [ diff --git a/src/Oro/Bundle/FormBundle/composer.json b/src/Oro/Bundle/FormBundle/composer.json index 2115729ecff..58cda5a4cff 100644 --- a/src/Oro/Bundle/FormBundle/composer.json +++ b/src/Oro/Bundle/FormBundle/composer.json @@ -9,7 +9,7 @@ "license": "MIT", "require": { "php": ">=5.5.9", - "symfony/symfony": "2.8.*", + "symfony/symfony": "2.8.*, !=2.8.10", "genemu/form-bundle": "2.1.*", "oro/security-bundle": "dev-master", "oro/search-bundle": "dev-master", diff --git a/src/Oro/Bundle/GoogleIntegrationBundle/composer.json b/src/Oro/Bundle/GoogleIntegrationBundle/composer.json index b105557b3f8..18cf29ef880 100644 --- a/src/Oro/Bundle/GoogleIntegrationBundle/composer.json +++ b/src/Oro/Bundle/GoogleIntegrationBundle/composer.json @@ -6,7 +6,7 @@ "license": "MIT", "require": { "php": ">=5.5.9", - "symfony/symfony": "2.8.*", + "symfony/symfony": "2.8.*, !=2.8.10", "oro/config-bundle": "dev-master", "oro/migration-bundle": "dev-master", "hwi/oauth-bundle": "~0.3", diff --git a/src/Oro/Bundle/HelpBundle/composer.json b/src/Oro/Bundle/HelpBundle/composer.json index 46f5034dfc3..fd87fb1d98f 100644 --- a/src/Oro/Bundle/HelpBundle/composer.json +++ b/src/Oro/Bundle/HelpBundle/composer.json @@ -7,8 +7,7 @@ "license": "MIT", "require": { "php": ">=5.5.9", - "symfony/symfony": "2.8.*", - "symfony/symfony": "2.8.*", + "symfony/symfony": "2.8.*, !=2.8.10", "oro/config": "dev-master", "oro/platform-bundle": "dev-master" }, diff --git a/src/Oro/Bundle/ImapBundle/Command/Cron/EmailSyncCommand.php b/src/Oro/Bundle/ImapBundle/Command/Cron/EmailSyncCommand.php index 7dfb38603c9..4e64e45d6e4 100644 --- a/src/Oro/Bundle/ImapBundle/Command/Cron/EmailSyncCommand.php +++ b/src/Oro/Bundle/ImapBundle/Command/Cron/EmailSyncCommand.php @@ -11,6 +11,7 @@ use Oro\Bundle\CronBundle\Command\CronCommandInterface; use Oro\Bundle\CronBundle\Command\CronCommandConcurrentJobsInterface; +use Oro\Bundle\EmailBundle\Sync\Model\SynchronizationProcessorSettings; use Oro\Bundle\ImapBundle\Sync\ImapEmailSynchronizer; class EmailSyncCommand extends ContainerAwareCommand implements CronCommandInterface, CronCommandConcurrentJobsInterface @@ -89,6 +90,19 @@ protected function configure() null, InputOption::VALUE_IS_ARRAY | InputOption::VALUE_OPTIONAL, 'The identifier of email origin to be synchronized.' + ) + ->addOption( + 'force', + null, + InputOption::VALUE_NONE, + 'Allows set the force mode. In this mode all emails will be re-synced again for checked folders. + Option "--force" can be used only with option "--id".' + ) + ->addOption( + 'vvv', + null, + InputOption::VALUE_NONE, + 'This option allows show the log messages during resync email' ); } @@ -101,16 +115,24 @@ protected function execute(InputInterface $input, OutputInterface $output) $synchronizer = $this->getContainer()->get('oro_imap.email_synchronizer'); $synchronizer->setLogger(new OutputLogger($output)); + $force = $input->getOption('force'); + $showMessage = $input->getOption('vvv'); $originIds = $input->getOption('id'); - if (!empty($originIds)) { - $synchronizer->syncOrigins($originIds); + + if ($force && empty($originIds)) { + $this->writeAttentionMessageForOptionForce($output); } else { - $synchronizer->sync( - (int)$input->getOption('max-concurrent-tasks'), - (int)$input->getOption('min-exec-interval'), - (int)$input->getOption('max-exec-time'), - (int)$input->getOption('max-tasks') - ); + if (!empty($originIds)) { + $settings = new SynchronizationProcessorSettings($force, $showMessage); + $synchronizer->syncOrigins($originIds, $settings); + } else { + $synchronizer->sync( + (int)$input->getOption('max-concurrent-tasks'), + (int)$input->getOption('min-exec-interval'), + (int)$input->getOption('max-exec-time'), + (int)$input->getOption('max-tasks') + ); + } } } @@ -121,4 +143,17 @@ public function getMaxJobsCount() { return self::MAX_JOBS_COUNT; } + + /** + * @param OutputInterface $output + */ + protected function writeAttentionMessageForOptionForce(OutputInterface $output) + { + $output->writeln( + 'ATTENTION: The option "force" can be used only for concrete email origins.' + ); + $output->writeln( + ' So you should add option "id" with required value of email origin in command line.' + ); + } } diff --git a/src/Oro/Bundle/ImapBundle/Mail/Processor/ContentProcessor.php b/src/Oro/Bundle/ImapBundle/Mail/Processor/ContentProcessor.php index 96a4a4a4b1e..a8801403e09 100644 --- a/src/Oro/Bundle/ImapBundle/Mail/Processor/ContentProcessor.php +++ b/src/Oro/Bundle/ImapBundle/Mail/Processor/ContentProcessor.php @@ -92,8 +92,18 @@ protected function extractContent(PartInterface $part) $encoding = 'ASCII'; } $contentTransferEncoding = 'BINARY'; - if ($part->getHeaders()->has('Content-Transfer-Encoding')) { - $contentTransferEncoding = $part->getHeader('Content-Transfer-Encoding')->getFieldValue(); + $headerKey = 'Content-Transfer-Encoding'; + if ($part->getHeaders()->has($headerKey)) { + $header = $part->getHeader($headerKey); + if ($header instanceof \ArrayIterator) { + foreach ($header as $headerItem) { + if ($headerItem->getFieldName() === $headerKey) { + $contentTransferEncoding = $headerItem->getFieldValue(); + } + } + } else { + $contentTransferEncoding = $header->getFieldValue(); + } } return new Content($part->getContent(), $contentType, $contentTransferEncoding, $encoding); diff --git a/src/Oro/Bundle/ImapBundle/Sync/ImapEmailSynchronizationProcessor.php b/src/Oro/Bundle/ImapBundle/Sync/ImapEmailSynchronizationProcessor.php index 3e8819b7dd4..7631733e2e8 100644 --- a/src/Oro/Bundle/ImapBundle/Sync/ImapEmailSynchronizationProcessor.php +++ b/src/Oro/Bundle/ImapBundle/Sync/ImapEmailSynchronizationProcessor.php @@ -23,6 +23,9 @@ use Oro\Bundle\ImapBundle\Manager\ImapEmailManager; use Oro\Bundle\ImapBundle\Manager\DTO\Email; +/** + * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) + */ class ImapEmailSynchronizationProcessor extends AbstractEmailSynchronizationProcessor { /** Determines how many emails can be loaded from IMAP server at once */ @@ -117,7 +120,8 @@ public function process(EmailOrigin $origin, $syncStartTime) $this->cleanUp(true, $imapFolder->getFolder()); $processSpentTime = time() - $processStartTime; - if ($processSpentTime > self::MAX_ORIGIN_SYNC_TIME) { + + if (false === $this->getSettings()->isForceMode() && $processSpentTime > self::MAX_ORIGIN_SYNC_TIME) { break; } } @@ -232,6 +236,9 @@ function (\Exception $e) use (&$invalid) { * * @param Email[] $emails * @param ImapEmailFolder $imapFolder + * + * @SuppressWarnings(PHPMD.NPathComplexity) + * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ protected function saveEmails(array $emails, ImapEmailFolder $imapFolder) { @@ -248,7 +255,8 @@ protected function saveEmails(array $emails, ImapEmailFolder $imapFolder) if (!$this->checkOnOldEmailForMailbox($folder, $email, $folder->getOrigin()->getMailbox())) { continue; } - if (!$this->checkOnExistsSavedEmail($email, $existingUids)) { + + if ($this->checkToSkipSyncEmail($email, $existingUids)) { continue; } @@ -275,16 +283,21 @@ function (ImapEmail $imapEmail) use ($email) { $emailUser->addFolder($folder); } } - $imapEmail = $this->createImapEmail($email->getId()->getUid(), $emailUser->getEmail(), $imapFolder); - $newImapEmails[] = $imapEmail; - $this->em->persist($imapEmail); - $this->logger->notice( - sprintf( - 'The "%s" (UID: %d) email was persisted.', - $email->getSubject(), - $email->getId()->getUid() - ) - ); + + if (false === $this->getSettings()->isForceMode() + || (true === $this->getSettings()->isForceMode() && count($relatedExistingImapEmails) === 0) + ) { + $imapEmail = $this->createImapEmail($email->getId()->getUid(), $emailUser->getEmail(), $imapFolder); + $newImapEmails[] = $imapEmail; + $this->em->persist($imapEmail); + $this->logger->notice( + sprintf( + 'The "%s" (UID: %d) email was persisted.', + $email->getSubject(), + $email->getId()->getUid() + ) + ); + } } catch (\Exception $e) { $this->logger->warning( sprintf( @@ -348,7 +361,7 @@ protected function checkOnOldEmailForMailbox(EmailFolder $folder, Email $email, } /** - * Check allowing to save email by uid + * Check the email was synced by Uid or wasn't. * * @param Email $email * @param array $existingUids @@ -358,17 +371,51 @@ protected function checkOnOldEmailForMailbox(EmailFolder $folder, Email $email, protected function checkOnExistsSavedEmail(Email $email, array $existingUids) { if (in_array($email->getId()->getUid(), $existingUids)) { - $this->logger->info( - sprintf( - 'Skip "%s" (UID: %d) email, because it is already synchronised.', - $email->getSubject(), - $email->getId()->getUid() - ) - ); - return false; + return true; } - return true; + return false; + } + + /** + * Check allowing to save email by uid + * + * @param Email $email + * @param array $existingUids + * + * @return bool + */ + protected function checkToSkipSyncEmail($email, $existingUids) + { + $existsSavedEmail = $this->checkOnExistsSavedEmail($email, $existingUids); + + $skipSync = false; + + if ($existsSavedEmail) { + $msg = 'Skip "%s" (UID: %d) email, because it is already synchronised.'; + $skipSync = true; + + if ($this->getSettings()->isForceMode()) { + $msg = null; + if ($this->getSettings()->needShowMessage()) { + $msg = 'Sync "%s" (UID: %d) email, because force mode is enabled.'; + } + + $skipSync = false; + } + + if ($msg) { + $this->logger->info( + sprintf( + $msg, + $email->getSubject(), + $email->getId()->getUid() + ) + ); + } + } + + return $skipSync; } /** @@ -469,7 +516,11 @@ protected function getEmailIterator( ImapEmailFolder $imapFolder, EmailFolder $folder ) { - $lastUid = $this->em->getRepository('OroImapBundle:ImapEmail')->findLastUidByFolder($imapFolder); + $lastUid = null; + if (false === $this->getSettings()->isForceMode()) { + $lastUid = $this->em->getRepository('OroImapBundle:ImapEmail')->findLastUidByFolder($imapFolder); + } + if (!$lastUid && $origin->getMailbox() && $folder->getSyncStartDate()) { $emails = $this->initialMailboxSync($folder); } else { diff --git a/src/Oro/Bundle/ImapBundle/composer.json b/src/Oro/Bundle/ImapBundle/composer.json index 128978b8508..c4b6794f370 100644 --- a/src/Oro/Bundle/ImapBundle/composer.json +++ b/src/Oro/Bundle/ImapBundle/composer.json @@ -7,7 +7,7 @@ "license": "MIT", "require": { "php": ">=5.5.9", - "symfony/symfony": "2.8.*", + "symfony/symfony": "2.8.*, !=2.8.10", "oro/log": "dev-master", "zendframework/zend-mail": "2.1.5", "oro/platform-bundle": "dev-master", diff --git a/src/Oro/Bundle/ImportExportBundle/composer.json b/src/Oro/Bundle/ImportExportBundle/composer.json index 3c6ef47f854..0a412c8a50a 100644 --- a/src/Oro/Bundle/ImportExportBundle/composer.json +++ b/src/Oro/Bundle/ImportExportBundle/composer.json @@ -6,7 +6,7 @@ "license": "MIT", "require": { "php": ">=5.5.9", - "symfony/symfony": "2.8.*", + "symfony/symfony": "2.8.*, !=2.8.10", "oro/batch-bundle": "dev-master" }, "autoload": { diff --git a/src/Oro/Bundle/IntegrationBundle/composer.json b/src/Oro/Bundle/IntegrationBundle/composer.json index cb69e4b96a5..f18f7e0fef2 100644 --- a/src/Oro/Bundle/IntegrationBundle/composer.json +++ b/src/Oro/Bundle/IntegrationBundle/composer.json @@ -6,7 +6,7 @@ "license": "MIT", "require": { "php": ">=5.5.9", - "symfony/symfony": "2.8.*", + "symfony/symfony": "2.8.*, !=2.8.10", "doctrine/orm": ">=2.3,<2.4-dev", "oro/log": "dev-master", "oro/workflow-bundle": "dev-master" diff --git a/src/Oro/Bundle/LayoutBundle/composer.json b/src/Oro/Bundle/LayoutBundle/composer.json index fdd5bbd2e2f..5dd29da6d50 100644 --- a/src/Oro/Bundle/LayoutBundle/composer.json +++ b/src/Oro/Bundle/LayoutBundle/composer.json @@ -6,7 +6,7 @@ "license": "MIT", "require": { "php": ">=5.5.9", - "symfony/symfony": "2.8.*", + "symfony/symfony": "2.8.*, !=2.8.10", "oro/layout": "dev-master", "oro/config-expression": "dev-master" }, diff --git a/src/Oro/Bundle/LocaleBundle/Formatter/DateTimeFormatter.php b/src/Oro/Bundle/LocaleBundle/Formatter/DateTimeFormatter.php index 6272bbcce86..008c69c2904 100644 --- a/src/Oro/Bundle/LocaleBundle/Formatter/DateTimeFormatter.php +++ b/src/Oro/Bundle/LocaleBundle/Formatter/DateTimeFormatter.php @@ -17,6 +17,12 @@ class DateTimeFormatter /** @var TranslatorInterface */ private $translator; + /** @var \IntlDateFormatter[] */ + protected $cachedFormatters = []; + + /** @var string[] */ + protected $cachedPatterns = []; + /** * @param LocaleSettings $localeSettings * @param TranslatorInterface $translator @@ -170,8 +176,14 @@ public function getPattern($dateType, $timeType, $locale = null, $value = null) $dateType = $this->parseDateType($dateType); $timeType = $this->parseDateType($timeType); - $localeFormatter = new \IntlDateFormatter($locale, $dateType, $timeType, null, \IntlDateFormatter::GREGORIAN); - return $localeFormatter->getPattern(); + $key = md5(serialize([$dateType, $timeType, $locale])); + if (!isset($this->cachedPatterns[$key])) { + $this->cachedPatterns[$key] = + (new \IntlDateFormatter($locale, $dateType, $timeType, null, \IntlDateFormatter::GREGORIAN)) + ->getPattern(); + } + + return $this->cachedPatterns[$key]; } /** @@ -191,14 +203,20 @@ protected function getFormatter($dateType, $timeType, $locale, $timeZone, $patte if (!$pattern) { $pattern = $this->getPattern($dateType, $timeType, $locale, $value); } - return new \IntlDateFormatter( - $this->localeSettings->getLanguage(), - null, - null, - $timeZone, - \IntlDateFormatter::GREGORIAN, - $pattern - ); + + $key = md5(serialize([$timeZone, $pattern])); + if (!isset($this->cachedFormatters[$key])) { + $this->cachedFormatters[$key] = new \IntlDateFormatter( + $this->localeSettings->getLanguage(), + null, + null, + $timeZone, + \IntlDateFormatter::GREGORIAN, + $pattern + ); + } + + return $this->cachedFormatters[$key]; } /** diff --git a/src/Oro/Bundle/LocaleBundle/Resources/public/js/formatter/datetime.js b/src/Oro/Bundle/LocaleBundle/Resources/public/js/formatter/datetime.js index ad146404197..08524a41a5a 100644 --- a/src/Oro/Bundle/LocaleBundle/Resources/public/js/formatter/datetime.js +++ b/src/Oro/Bundle/LocaleBundle/Resources/public/js/formatter/datetime.js @@ -71,6 +71,35 @@ define(['../locale-settings', 'moment', 'orotranslation/js/translator' return this.frontendFormats.datetime; }, + /** + * @returns {string} + */ + getDateTimeFormatNBSP: function() { + if (!this.frontendFormats.datetimeNBSP) { + this.frontendFormats.datetimeNBSP = this.prepareNbspFormat(this.frontendFormats.datetime); + } + return this.frontendFormats.datetimeNBSP; + }, + + /** + * Replaces spaces to nbsp in format + * + * @param {string} format + * @returns {string} + */ + prepareNbspFormat: function(format) { + format = format.replace(/\s+/g, '\u00a0'); + // format starts from time part + if (/[AaHsSzZ]/.test(format[0])) { + // first nbps before date part replace to usual space + format = format.replace(/([^xXgGYWwEdDQM\u00a0])\s([^HsSAazZ]+)$/, '$1 $2'); + } else { + // first nbps before time part replace to usual space + format = format.replace(/([^HsSAazZ\u00a0])\s([^xXgGYWwEdDQM]+)$/, '$1 $2'); + } + return format; + }, + /** * Return separator between date and time for current format * @@ -275,6 +304,15 @@ define(['../locale-settings', 'moment', 'orotranslation/js/translator' .format(this.getDateTimeFormat()); }, + /** + * @param {string} value + * @returns {string} + */ + formatDateTimeNBSP: function(value) { + return this.getMomentForBackendDateTime(value).tz(this.timezone) + .format(this.getDateTimeFormatNBSP()); + }, + /** * @param {string} value * @returns {string} diff --git a/src/Oro/Bundle/LocaleBundle/composer.json b/src/Oro/Bundle/LocaleBundle/composer.json index 745b3696302..7ecdacef498 100644 --- a/src/Oro/Bundle/LocaleBundle/composer.json +++ b/src/Oro/Bundle/LocaleBundle/composer.json @@ -8,7 +8,7 @@ "require": { "php": ">=5.5.9", "ext-intl": "*", - "symfony/symfony": "2.8.*", + "symfony/symfony": "2.8.*, !=2.8.10", "oro/config": "dev-master", "oro/requirejs-bundle": "dev-master", "oro/config-bundle": "dev-master", diff --git a/src/Oro/Bundle/NavigationBundle/composer.json b/src/Oro/Bundle/NavigationBundle/composer.json index 5d50a6f103d..6f874f0814d 100644 --- a/src/Oro/Bundle/NavigationBundle/composer.json +++ b/src/Oro/Bundle/NavigationBundle/composer.json @@ -6,7 +6,7 @@ "license": "MIT", "require": { "php": ">=5.5.9", - "symfony/symfony": "2.8.*", + "symfony/symfony": "2.8.*, !=2.8.10", "oro/config": "dev-master", "oro/ui-bundle": "dev-master", "oro/user-bundle": "dev-master", diff --git a/src/Oro/Bundle/NoteBundle/composer.json b/src/Oro/Bundle/NoteBundle/composer.json index bcca2662b95..9a25eece9b2 100644 --- a/src/Oro/Bundle/NoteBundle/composer.json +++ b/src/Oro/Bundle/NoteBundle/composer.json @@ -6,7 +6,7 @@ "license": "MIT", "require": { "php": ">=5.5.9", - "symfony/symfony": "2.8.*", + "symfony/symfony": "2.8.*, !=2.8.10", "liip/imagine-bundle": "0.17.*", "oro/ui-bundle": "dev-master", "oro/user-bundle": "dev-master", diff --git a/src/Oro/Bundle/NotificationBundle/composer.json b/src/Oro/Bundle/NotificationBundle/composer.json index a5aac742dde..a1b0cb0cf93 100644 --- a/src/Oro/Bundle/NotificationBundle/composer.json +++ b/src/Oro/Bundle/NotificationBundle/composer.json @@ -6,7 +6,7 @@ "license": "MIT", "require": { "php": ">=5.5.9", - "symfony/symfony": "2.8.*", + "symfony/symfony": "2.8.*, !=2.8.10", "oro/email-bundle": "dev-master" }, "autoload": { diff --git a/src/Oro/Bundle/OrganizationBundle/composer.json b/src/Oro/Bundle/OrganizationBundle/composer.json index d56091ce90d..07a67cc0b01 100644 --- a/src/Oro/Bundle/OrganizationBundle/composer.json +++ b/src/Oro/Bundle/OrganizationBundle/composer.json @@ -6,7 +6,7 @@ "license": "MIT", "require": { "php": ">=5.5.9", - "symfony/symfony": "2.8.*", + "symfony/symfony": "2.8.*, !=2.8.10", "oro/ui-bundle": "dev-master", "oro/user-bundle": "dev-master", "oro/form-bundle": "dev-master" diff --git a/src/Oro/Bundle/PlatformBundle/Tests/Unit/Composer/fixtures/installed.json b/src/Oro/Bundle/PlatformBundle/Tests/Unit/Composer/fixtures/installed.json index 530b0cbab4c..7f0ddcd1496 100644 --- a/src/Oro/Bundle/PlatformBundle/Tests/Unit/Composer/fixtures/installed.json +++ b/src/Oro/Bundle/PlatformBundle/Tests/Unit/Composer/fixtures/installed.json @@ -60,7 +60,7 @@ "symfony/icu": "~1.1", "symfony/monolog-bundle": "2.3.0", "symfony/swiftmailer-bundle": "2.3.*", - "symfony/symfony": "2.8.*", + "symfony/symfony": "2.8.*, !=2.8.10", "twig/extensions": "1.0.1", "zendframework/zend-mail": "2.1.*" }, diff --git a/src/Oro/Bundle/PlatformBundle/composer.json b/src/Oro/Bundle/PlatformBundle/composer.json index a7411a5a4b3..a839f024455 100644 --- a/src/Oro/Bundle/PlatformBundle/composer.json +++ b/src/Oro/Bundle/PlatformBundle/composer.json @@ -7,7 +7,7 @@ "license": "MIT", "require": { "php": ">=5.5.9", - "symfony/symfony": "2.8.*", + "symfony/symfony": "2.8.*, !=2.8.10", "lexik/maintenance-bundle": "dev-master", "oro/config": "dev-master" }, diff --git a/src/Oro/Bundle/QueryDesignerBundle/Grid/Extension/OrmDatasourceExtension.php b/src/Oro/Bundle/QueryDesignerBundle/Grid/Extension/OrmDatasourceExtension.php index cf1d66f6265..dcba3acd436 100644 --- a/src/Oro/Bundle/QueryDesignerBundle/Grid/Extension/OrmDatasourceExtension.php +++ b/src/Oro/Bundle/QueryDesignerBundle/Grid/Extension/OrmDatasourceExtension.php @@ -13,6 +13,7 @@ class OrmDatasourceExtension extends AbstractExtension { + /** @deprecated since 1.10. Use config->getName() instead */ const NAME_PATH = '[name]'; /** @@ -37,7 +38,8 @@ public function __construct(RestrictionBuilderInterface $restrictionBuilder) */ public function isApplicable(DatagridConfiguration $config) { - return $config->getDatasourceType() == OrmDatasource::TYPE + return + $config->getDatasourceType() === OrmDatasource::TYPE && $config->offsetGetByPath('[source][query_config][filters]'); } @@ -46,7 +48,7 @@ public function isApplicable(DatagridConfiguration $config) */ public function visitDatasource(DatagridConfiguration $config, DatasourceInterface $datasource) { - $gridName = $config->offsetGetByPath(self::NAME_PATH); + $gridName = $config->getName(); $parametersKey = md5(json_encode($this->parameters->all())); if (!empty($this->appliedFor[$gridName . $parametersKey])) { diff --git a/src/Oro/Bundle/QueryDesignerBundle/Model/GroupByHelper.php b/src/Oro/Bundle/QueryDesignerBundle/Model/GroupByHelper.php index 0906fd77644..258adc8251d 100644 --- a/src/Oro/Bundle/QueryDesignerBundle/Model/GroupByHelper.php +++ b/src/Oro/Bundle/QueryDesignerBundle/Model/GroupByHelper.php @@ -68,6 +68,11 @@ protected function getPreparedGroupBy($groupBy) */ protected function hasAggregate($select) { + // subselect + if (stripos($select, '(SELECT') === 0) { + return false; + } + preg_match('/(MIN|MAX|AVG|COUNT|SUM|GROUP_CONCAT)\(/i', $select, $matches); return (bool)$matches; diff --git a/src/Oro/Bundle/QueryDesignerBundle/Tests/Unit/Grid/Extension/OrmDatasourceExtensionTest.php b/src/Oro/Bundle/QueryDesignerBundle/Tests/Unit/Grid/Extension/OrmDatasourceExtensionTest.php index fb73b7dabc0..be0d25cedd2 100644 --- a/src/Oro/Bundle/QueryDesignerBundle/Tests/Unit/Grid/Extension/OrmDatasourceExtensionTest.php +++ b/src/Oro/Bundle/QueryDesignerBundle/Tests/Unit/Grid/Extension/OrmDatasourceExtensionTest.php @@ -107,6 +107,7 @@ function ($name, $params) { ->will($this->returnValue($qb)); $config = DatagridConfiguration::create($source); + $config->setName('test_grid'); $extension->visitDatasource($config, $datasource); $result = $qb->getDQL(); @@ -200,22 +201,22 @@ public function visitDatasourceProvider() . 'INNER JOIN user.address address ' . 'WHERE user_name NOT LIKE :string1 AND (' . '(user_status < :datetime2 OR user_status > :datetime3) ' - . 'AND (user.id IN(' + . 'AND ((EXISTS(' . 'SELECT string4.id ' . 'FROM Oro\Bundle\QueryDesignerBundle\Tests\Unit\Fixtures\Models\CMS\CmsUser string4 ' . 'INNER JOIN string4.address string5 ' - . 'WHERE string5.country LIKE :string4) ' - . 'OR user.id IN(' + . 'WHERE string5.country LIKE :string4 AND string4.id = user.id)) ' + . 'OR (EXISTS(' . 'SELECT string7.id ' . 'FROM Oro\Bundle\QueryDesignerBundle\Tests\Unit\Fixtures\Models\CMS\CmsUser string7 ' . 'INNER JOIN string7.address string8 ' - . 'WHERE string8.city LIKE :string5) OR ' - . 'user.id IN(' + . 'WHERE string8.city LIKE :string5 AND string7.id = user.id)) OR ' + . '(EXISTS(' . 'SELECT string10.id ' . 'FROM Oro\Bundle\QueryDesignerBundle\Tests\Unit\Fixtures\Models\CMS\CmsUser string10 ' . 'INNER JOIN string10.address string11 ' - . 'WHERE string11.zip LIKE :string6' - . ')' + . 'WHERE string11.zip LIKE :string6 AND string10.id = user.id' + . '))' . ')' . ')' ], @@ -267,12 +268,12 @@ public function visitDatasourceProvider() . 'INNER JOIN user.address address ' . 'WHERE user_name NOT LIKE :string1 OR (' . 'user_status < :datetime2 OR user_status > :datetime3 ' - . 'OR user.id IN(' + . 'OR (EXISTS(' . 'SELECT string4.id ' . 'FROM Oro\Bundle\QueryDesignerBundle\Tests\Unit\Fixtures\Models\CMS\CmsUser string4 ' . 'INNER JOIN string4.address string5 ' - . 'WHERE string5.country LIKE :string4' - . ')' + . 'WHERE string5.country LIKE :string4 AND string4.id = user.id' + . '))' . ')' ], 'test with OR filters between simple and group conditions' => [ @@ -323,12 +324,12 @@ public function visitDatasourceProvider() . 'INNER JOIN user.address address ' . 'WHERE user_name NOT LIKE :string1 OR (' . '(user_status < :datetime2 OR user_status > :datetime3) ' - . 'AND user.id IN(' + . 'AND (EXISTS(' . 'SELECT string4.id ' . 'FROM Oro\Bundle\QueryDesignerBundle\Tests\Unit\Fixtures\Models\CMS\CmsUser string4 ' . 'INNER JOIN string4.address string5 ' - . 'WHERE string5.country LIKE :string4' - . ')' + . 'WHERE string5.country LIKE :string4 AND string4.id = user.id' + . '))' . ')' ], ]; diff --git a/src/Oro/Bundle/QueryDesignerBundle/Tests/Unit/Model/GroupByHelperTest.php b/src/Oro/Bundle/QueryDesignerBundle/Tests/Unit/Model/GroupByHelperTest.php index 54310388905..b77c7c78dcc 100644 --- a/src/Oro/Bundle/QueryDesignerBundle/Tests/Unit/Model/GroupByHelperTest.php +++ b/src/Oro/Bundle/QueryDesignerBundle/Tests/Unit/Model/GroupByHelperTest.php @@ -67,6 +67,22 @@ public function groupByDataProvider() 'groupBy' => 'alias.field2', 'expected' => ['alias.field2', 'alias.field', 'c1', 'someAlias3'], ], + 'without subselect without group by' => [ + 'selects' => [ + 'alias.field', + 'COUNT(alias.field2)', + ], + 'groupBy' => null, + 'expected' => ['alias.field'], + ], + 'with subselect without group by' => [ + 'selects' => [ + 'alias.field', + '(SELECT COUNT(1) FROM Namespace:Entity e) AS e_count', + ], + 'groupBy' => null, + 'expected' => [], + ], 'without group by' => [ 'selects' => ['t1.f0', 't10.F19 as agF1', 'alias.matchedFields AS c1'], 'groupBy' => null, diff --git a/src/Oro/Bundle/QueryDesignerBundle/composer.json b/src/Oro/Bundle/QueryDesignerBundle/composer.json index 2461d166adb..ae3112bf60d 100644 --- a/src/Oro/Bundle/QueryDesignerBundle/composer.json +++ b/src/Oro/Bundle/QueryDesignerBundle/composer.json @@ -6,7 +6,7 @@ "license": "MIT", "require": { "php": ">=5.5.9", - "symfony/symfony": "2.8.*", + "symfony/symfony": "2.8.*, !=2.8.10", "oro/config": "dev-master", "oro/ui-bundle": "dev-master", "oro/requirejs-bundle": "dev-master", diff --git a/src/Oro/Bundle/ReminderBundle/composer.json b/src/Oro/Bundle/ReminderBundle/composer.json index 664554ad098..ce8d7b2c490 100644 --- a/src/Oro/Bundle/ReminderBundle/composer.json +++ b/src/Oro/Bundle/ReminderBundle/composer.json @@ -7,7 +7,7 @@ "license": "MIT", "require": { "php": ">=5.5.9", - "symfony/symfony": "2.8.*", + "symfony/symfony": "2.8.*, !=2.8.10", "doctrine/orm": ">=2.2.3,<2.4-dev", "doctrine/doctrine-bundle": "1.2.*", "doctrine/data-fixtures": "@dev", diff --git a/src/Oro/Bundle/ReportBundle/Grid/StoreSqlExtension.php b/src/Oro/Bundle/ReportBundle/Grid/StoreSqlExtension.php index 099b86dc8fc..2ac6a3b734b 100644 --- a/src/Oro/Bundle/ReportBundle/Grid/StoreSqlExtension.php +++ b/src/Oro/Bundle/ReportBundle/Grid/StoreSqlExtension.php @@ -34,7 +34,8 @@ public function __construct(SecurityFacade $securityFacade) */ public function isApplicable(DatagridConfiguration $config) { - return $config->getDatasourceType() == OrmDatasource::TYPE && + return + $config->getDatasourceType() === OrmDatasource::TYPE && $this->getParameters()->get(self::DISPLAY_SQL_QUERY, false) && $this->securityFacade->getLoggedUser() && $this->securityFacade->isGranted('oro_report_view_sql'); diff --git a/src/Oro/Bundle/ReportBundle/composer.json b/src/Oro/Bundle/ReportBundle/composer.json index 10525c86f9f..e484528a69a 100644 --- a/src/Oro/Bundle/ReportBundle/composer.json +++ b/src/Oro/Bundle/ReportBundle/composer.json @@ -6,7 +6,7 @@ "license": "MIT", "require": { "php": ">=5.5.9", - "symfony/symfony": "2.8.*", + "symfony/symfony": "2.8.*, !=2.8.10", "oro/ui-bundle": "dev-master", "oro/query-designer-bundle": "dev-master" diff --git a/src/Oro/Bundle/RequireJSBundle/composer.json b/src/Oro/Bundle/RequireJSBundle/composer.json index 1a6d9d936d8..cac3063bf51 100644 --- a/src/Oro/Bundle/RequireJSBundle/composer.json +++ b/src/Oro/Bundle/RequireJSBundle/composer.json @@ -7,7 +7,7 @@ "license": "MIT", "require": { "php": ">=5.5.9", - "symfony/symfony": "~2.7", + "symfony/symfony": "2.8.*, !=2.8.10", "doctrine/doctrine-cache-bundle": "~1.3" }, "autoload": { diff --git a/src/Oro/Bundle/SSOBundle/composer.json b/src/Oro/Bundle/SSOBundle/composer.json index 394f045b210..fd59cc0ea4c 100644 --- a/src/Oro/Bundle/SSOBundle/composer.json +++ b/src/Oro/Bundle/SSOBundle/composer.json @@ -6,7 +6,7 @@ "license": "MIT", "require": { "php": ">=5.5.9", - "symfony/symfony": "2.8.*", + "symfony/symfony": "2.8.*, !=2.8.10", "oro/config-bundle": "dev-master", "oro/migration-bundle": "dev-master", "hwi/oauth-bundle": "~0.3", diff --git a/src/Oro/Bundle/SearchBundle/Extension/Pager/SearchPagerExtension.php b/src/Oro/Bundle/SearchBundle/Extension/Pager/SearchPagerExtension.php index b649aa7b44e..173f5b1fe96 100644 --- a/src/Oro/Bundle/SearchBundle/Extension/Pager/SearchPagerExtension.php +++ b/src/Oro/Bundle/SearchBundle/Extension/Pager/SearchPagerExtension.php @@ -27,8 +27,7 @@ public function __construct(IndexerPager $pager) */ public function isApplicable(DatagridConfiguration $config) { - // enabled by default for search datasource - return $config->getDatasourceType() == SearchDatasource::TYPE; + return $config->getDatasourceType() === SearchDatasource::TYPE; } /** diff --git a/src/Oro/Bundle/SearchBundle/Extension/SearchResultsExtension.php b/src/Oro/Bundle/SearchBundle/Extension/SearchResultsExtension.php index fc55ab184d6..1cc73de54d1 100644 --- a/src/Oro/Bundle/SearchBundle/Extension/SearchResultsExtension.php +++ b/src/Oro/Bundle/SearchBundle/Extension/SearchResultsExtension.php @@ -57,7 +57,7 @@ public function __construct( */ public function isApplicable(DatagridConfiguration $config) { - return $config->offsetGetByPath(self::TYPE_PATH) == self::TYPE_VALUE ? true : false; + return $config->offsetGetByPath(self::TYPE_PATH) === self::TYPE_VALUE; } /** diff --git a/src/Oro/Bundle/SearchBundle/composer.json b/src/Oro/Bundle/SearchBundle/composer.json index a8dec0a7318..9e2aedfdb4c 100644 --- a/src/Oro/Bundle/SearchBundle/composer.json +++ b/src/Oro/Bundle/SearchBundle/composer.json @@ -7,7 +7,7 @@ "license": "MIT", "require": { "php": ">=5.5.9", - "symfony/symfony": "2.8.*", + "symfony/symfony": "2.8.*, !=2.8.10", "friendsofsymfony/rest-bundle": "1.5.0-RC2", "nelmio/api-doc-bundle": "dev-master", "jms/job-queue-bundle": "dev-master", diff --git a/src/Oro/Bundle/SecurityBundle/Form/Extension/AclProtectedFieldTypeExtension.php b/src/Oro/Bundle/SecurityBundle/Form/Extension/AclProtectedFieldTypeExtension.php index f010206abab..703e1ec86d8 100644 --- a/src/Oro/Bundle/SecurityBundle/Form/Extension/AclProtectedFieldTypeExtension.php +++ b/src/Oro/Bundle/SecurityBundle/Form/Extension/AclProtectedFieldTypeExtension.php @@ -14,11 +14,13 @@ use Symfony\Component\Form\FormInterface; use Symfony\Component\Form\FormView; use Symfony\Component\Security\Acl\Voter\FieldVote; +use Symfony\Component\Validator\ConstraintViolation; use Oro\Bundle\EntityBundle\ORM\EntityClassResolver; use Oro\Bundle\EntityBundle\ORM\DoctrineHelper; -use Oro\Bundle\SecurityBundle\SecurityFacade; use Oro\Bundle\EntityConfigBundle\Provider\ConfigProvider; +use Oro\Bundle\SecurityBundle\SecurityFacade; +use Oro\Bundle\SecurityBundle\Validator\Constraints\FieldAccessGranted; class AclProtectedFieldTypeExtension extends AbstractTypeExtension { @@ -71,7 +73,9 @@ public function __construct( */ public function getExtendedType() { - return 'form'; + return method_exists('Symfony\Component\Form\AbstractType', 'getBlockPrefix') + ? 'Symfony\Component\Form\Extension\Core\Type\FormType' + : 'form'; } /** @@ -144,6 +148,7 @@ public function postSubmit(FormEvent $event) }; foreach ($this->disabledFields as $field) { + /** @var Form $fieldInstance */ $fieldInstance = $form->get($field); // Clear all other validation errors if ($fieldInstance->getErrors()->count()) { @@ -152,13 +157,7 @@ public function postSubmit(FormEvent $event) } // add Field ACL validation error - $fieldInstance->addError( - new FormError( - sprintf('You are not allowed to modify \'%s\' field.', $field) - // do not use message template and 'message parameters' params here - // they are not processed in SOAP responses, only message will be used - ) - ); + $fieldInstance->addError($this->getFieldForbiddenFormError()); } } @@ -279,7 +278,7 @@ protected function getPropertyByForm(FormInterface $form) * @param FormView $view * @param FormInterface $form */ - protected function processHiddenFieldsWithErrors($hiddenFieldsWithErrors, FormView $view, FormInterface $form) + protected function processHiddenFieldsWithErrors(array $hiddenFieldsWithErrors, FormView $view, FormInterface $form) { if (count($hiddenFieldsWithErrors)) { $viewErrors = array_key_exists('errors', $view->vars) ? $view->vars['errors'] : []; @@ -307,4 +306,28 @@ protected function processHiddenFieldsWithErrors($hiddenFieldsWithErrors, FormVi } } } + + /** + * Get Form Error with FieldAccessGranted constraint + * + * @return FormError + */ + protected function getFieldForbiddenFormError() + { + $constraint = new FieldAccessGranted(); + $message = $constraint->message; + $violation = new ConstraintViolation( + $message, + $message, + [], + '', + '', + '', + null, + null, + $constraint + ); + + return new FormError($message, $message, [], null, $violation); + } } diff --git a/src/Oro/Bundle/SecurityBundle/Migrations/Schema/LoadBasePermissionsQuery.php b/src/Oro/Bundle/SecurityBundle/Migrations/Schema/LoadBasePermissionsQuery.php index 9bd6b82fce9..090b8ba5a7b 100644 --- a/src/Oro/Bundle/SecurityBundle/Migrations/Schema/LoadBasePermissionsQuery.php +++ b/src/Oro/Bundle/SecurityBundle/Migrations/Schema/LoadBasePermissionsQuery.php @@ -35,7 +35,9 @@ protected function processQueries(LoggerInterface $logger, $dryRun = false) 'description' => Type::STRING ]; - foreach ($this->permissions as $permission) { + $permissions = array_diff($this->permissions, $this->getExistingPermissions($logger)); + + foreach ($permissions as $permission) { $this->addSql( $query, [ @@ -51,4 +53,14 @@ protected function processQueries(LoggerInterface $logger, $dryRun = false) parent::processQueries($logger, $dryRun); } + /** + * @param LoggerInterface $logger + * @return array + */ + protected function getExistingPermissions(LoggerInterface $logger) + { + $sql = 'SELECT name FROM oro_security_permission'; + $this->logQuery($logger, $sql); + return array_column((array)$this->connection->fetchAll($sql), 'name'); + } } diff --git a/src/Oro/Bundle/SecurityBundle/ORM/Walker/AclHelper.php b/src/Oro/Bundle/SecurityBundle/ORM/Walker/AclHelper.php index cf3dc8eba10..eeee7620aff 100644 --- a/src/Oro/Bundle/SecurityBundle/ORM/Walker/AclHelper.php +++ b/src/Oro/Bundle/SecurityBundle/ORM/Walker/AclHelper.php @@ -34,14 +34,10 @@ class AclHelper const ORO_ACL_WALKER = 'Oro\Bundle\SecurityBundle\ORM\Walker\AclWalker'; const ORO_USER_CLASS = 'Oro\Bundle\UserBundle\Entity\User'; - /** - * @var OwnershipConditionDataBuilder - */ + /** @var OwnershipConditionDataBuilder */ protected $builder; - /** - * @var EntityManager - */ + /** @var EntityManager */ protected $em; /** @var array */ @@ -81,9 +77,7 @@ public function applyAclToCriteria($className, Criteria $criteria, $permission, { $conditionData = $this->builder->getAclConditionData($className, $permission); if (!empty($conditionData)) { - $entityField = $value = $pathExpressionType = $organizationField = $organizationValue = $ignoreOwner = null; - list($entityField, $value, $pathExpressionType, $organizationField, $organizationValue, $ignoreOwner) - = $conditionData; + list($entityField, $value, , $organizationField, $organizationValue, $ignoreOwner) = $conditionData; if (isset($mapField[$organizationField])) { $organizationField = $mapField[$organizationField]; @@ -231,22 +225,15 @@ protected function processSubselect(Subselect $subSelect, $permission) */ protected function processSelect($select, $permission) { - if ($select instanceof SelectStatement) { - $isSubRequest = false; - } else { - $isSubRequest = true; - } - $whereConditions = []; $joinConditions = []; - $fromClause = $isSubRequest ? $select->subselectFromClause : $select->fromClause; + $fromClause = $select instanceof SelectStatement ? $select->fromClause : $select->subselectFromClause; foreach ($fromClause->identificationVariableDeclarations as $fromKey => $identificationVariableDeclaration) { $condition = $this->processRangeVariableDeclaration( $identificationVariableDeclaration->rangeVariableDeclaration, $permission, - false, - $isSubRequest + false ); if ($condition) { $whereConditions[] = $condition; @@ -261,8 +248,7 @@ protected function processSelect($select, $permission) $condition = $this->processRangeVariableDeclaration( $join->joinAssociationDeclaration, $permission, - true, - $isSubRequest + true ); } else { $condition = $this->processJoinAssociationPathExpression( @@ -354,28 +340,25 @@ protected function processJoinAssociationPathExpression( * @param RangeVariableDeclaration $rangeVariableDeclaration * @param string $permission * @param bool $isJoin - * @param bool $isSubRequest * * @return null|AclCondition|JoinAclCondition */ protected function processRangeVariableDeclaration( RangeVariableDeclaration $rangeVariableDeclaration, $permission, - $isJoin = false, - $isSubRequest = false + $isJoin = false ) { - $this->addEntityAlias($rangeVariableDeclaration); - $entityName = $rangeVariableDeclaration->abstractSchemaName; - $entityAlias = $rangeVariableDeclaration->aliasIdentificationVariable; + $resultData = false; + $this->addEntityAlias($rangeVariableDeclaration); - $isUserTable = in_array($rangeVariableDeclaration->abstractSchemaName, [self::ORO_USER_CLASS]); - $resultData = false; - if (!$isUserTable || $rangeVariableDeclaration->isRoot) { + $entityName = $rangeVariableDeclaration->abstractSchemaName; + if ($entityName !== self::ORO_USER_CLASS || $rangeVariableDeclaration->isRoot) { $resultData = $this->builder->getAclConditionData($entityName, $permission); } if ($resultData !== false && ($resultData === null || !empty($resultData))) { + $entityAlias = $rangeVariableDeclaration->aliasIdentificationVariable; $entityField = $value = $pathExpressionType = $organizationField = $organizationValue = $ignoreOwner = null; if (!empty($resultData)) { list($entityField, $value, $pathExpressionType, $organizationField, $organizationValue, $ignoreOwner) diff --git a/src/Oro/Bundle/SecurityBundle/Resources/config/services.yml b/src/Oro/Bundle/SecurityBundle/Resources/config/services.yml index c1c9570a21c..c44e08df389 100644 --- a/src/Oro/Bundle/SecurityBundle/Resources/config/services.yml +++ b/src/Oro/Bundle/SecurityBundle/Resources/config/services.yml @@ -84,7 +84,6 @@ parameters: oro_security.configuration.provider.permission_configuration.class: Oro\Bundle\SecurityBundle\Configuration\PermissionConfigurationProvider oro_security.configuration.builder.permission_configuration.class: Oro\Bundle\SecurityBundle\Configuration\PermissionConfigurationBuilder - oro_security.form.extension.aclprotected_fields_type.class: Oro\Bundle\SecurityBundle\Form\Extension\AclProtectedFieldTypeExtension oro_security.serializer.filter_chain.class: Oro\Component\EntitySerializer\Filter\EntityAwareFilterChain oro_security.serializer.acl_filter: Oro\Bundle\SecurityBundle\Filter\SerializerFieldFilter @@ -682,7 +681,7 @@ services: - '@validator' oro_security.form.extension.aclprotected_fields_type: - class: %oro_security.form.extension.aclprotected_fields_type.class% + class: Oro\Bundle\SecurityBundle\Form\Extension\AclProtectedFieldTypeExtension arguments: - '@oro_security.security_facade' - '@oro_entity.orm.entity_class_resolver' @@ -690,7 +689,7 @@ services: - '@oro_entity_config.provider.security' - '@logger' tags: - - { name: form.type_extension, alias: form } + - { name: form.type_extension, alias: form, extended_type: 'Symfony\Component\Form\Extension\Core\Type\FormType' } oro_security.acl.extension.field: public: false @@ -722,3 +721,10 @@ services: - '@oro_security.entity_security_metadata_provider' tags: - { name: kernel.event_listener, event: oro.entity_config.pre_flush, method: preFlush } + + oro_security.helper.property_path_helper: + class: Oro\Bundle\SecurityBundle\Util\PropertyPathSecurityHelper + arguments: + - '@security.authorization_checker' + - '@doctrine' + - '@oro_entity_config.provider.entity' diff --git a/src/Oro/Bundle/SecurityBundle/Tests/Unit/Form/Extension/AclProtectedFieldTypeExtensionTest.php b/src/Oro/Bundle/SecurityBundle/Tests/Unit/Form/Extension/AclProtectedFieldTypeExtensionTest.php index 30ab7a4e0a7..6fce2c044a5 100644 --- a/src/Oro/Bundle/SecurityBundle/Tests/Unit/Form/Extension/AclProtectedFieldTypeExtensionTest.php +++ b/src/Oro/Bundle/SecurityBundle/Tests/Unit/Form/Extension/AclProtectedFieldTypeExtensionTest.php @@ -5,11 +5,13 @@ use Symfony\Component\EventDispatcher\EventDispatcher; use Symfony\Component\Form\Form; use Symfony\Component\Form\FormBuilder; +use Symfony\Component\Form\FormConfigBuilder; use Symfony\Component\Form\FormError; use Symfony\Component\Form\FormErrorIterator; use Symfony\Component\Form\FormEvent; use Symfony\Component\Form\FormView; use Symfony\Component\Form\Test\FormIntegrationTestCase; +use Symfony\Component\PropertyAccess\PropertyPath; use Symfony\Component\Security\Acl\Voter\FieldVote; use Oro\Bundle\EntityConfigBundle\Config\Config; @@ -65,7 +67,14 @@ protected function setUp() public function testGetExtendedType() { - $this->assertEquals('form', $this->extension->getExtendedType()); + $expectedResult = method_exists('Symfony\Component\Form\AbstractType', 'getBlockPrefix') + ? 'Symfony\Component\Form\Extension\Core\Type\FormType' + : 'form'; + + $this->assertEquals( + $expectedResult, + $this->extension->getExtendedType() + ); } public function testBuildFormWithCorrectData() @@ -210,26 +219,37 @@ public function testPreAndPostSubmit() $entity->country = 'USA'; $entity->city = 'Los Angeles'; $entity->street = 'Main street'; + $entity->zip = 78945; /** @var Form $form */ $form = $this->factory->create('form', $entity, $options); $form->add('city'); $form->add('street'); $form->add('country'); + $dispatcher = $this->getMock('Symfony\Component\EventDispatcher\EventDispatcherInterface'); + $builder = new FormBuilder('postoffice', null, $dispatcher, $this->factory); + $builder->setPropertyPath(new PropertyPath('zip')); + $builder->setAttribute('error_mapping', array()); + $builder->setErrorBubbling(false); + $builder->setMapped(true); + $form->add($builder->getForm()); + // add error that should be cleaned $form->get('country')->addError(new FormError('test error')); $data = [ 'country' => 'some country', 'city' => 'some city', - 'street' => 'some street' + 'street' => 'some street', + 'postoffice' => 61000 ]; $this->securityFacade->expects($this->any()) ->method('isGranted') ->willReturnCallback( function ($permission, FieldVote $object) { - return $object->getField() !== 'country'; + $this->assertEquals('CREATE', $permission); + return !in_array($object->getField(), ['country', 'zip']); } ); @@ -240,7 +260,8 @@ function ($permission, FieldVote $object) { [ 'country' => 'USA', 'city' => 'some city', - 'street' => 'some street' + 'street' => 'some street', + 'postoffice' => '78945' ], $event->getData() ); @@ -251,9 +272,15 @@ function ($permission, FieldVote $object) { $countryErrors = $form->get('country')->getErrors(); $this->assertCount(1, $countryErrors); $this->assertEquals( - 'You are not allowed to modify \'country\' field.', + 'You have no access to modify this field.', $countryErrors[0]->getMessage() ); + $postofficeErrors = $form->get('postoffice')->getErrors(); + $this->assertCount(1, $postofficeErrors); + $this->assertEquals( + 'You have no access to modify this field.', + $postofficeErrors[0]->getMessage() + ); $this->assertCount(0, $form->get('city')->getErrors()); $this->assertCount(0, $form->get('street')->getErrors()); } diff --git a/src/Oro/Bundle/SecurityBundle/Tests/Unit/Migrations/Schema/LoadBasePermissionsQueryTest.php b/src/Oro/Bundle/SecurityBundle/Tests/Unit/Migrations/Schema/LoadBasePermissionsQueryTest.php index 66101634f53..10199e3c63a 100644 --- a/src/Oro/Bundle/SecurityBundle/Tests/Unit/Migrations/Schema/LoadBasePermissionsQueryTest.php +++ b/src/Oro/Bundle/SecurityBundle/Tests/Unit/Migrations/Schema/LoadBasePermissionsQueryTest.php @@ -26,7 +26,7 @@ protected function tearDown() public function testExecute() { - $this->assertConnectionCalled(['VIEW', 'CREATE', 'EDIT', 'DELETE', 'ASSIGN']); + $this->assertConnectionCalled(['VIEW', 'CREATE', 'EDIT', 'DELETE', 'ASSIGN'], 4); $query = new LoadBasePermissionsQuery(); $query->setConnection($this->connection); @@ -35,8 +35,9 @@ public function testExecute() /** * @param array $permissions + * @param int $countCalls */ - protected function assertConnectionCalled(array $permissions) + protected function assertConnectionCalled(array $permissions, $countCalls) { $permissions = array_map( function ($permission) { @@ -52,7 +53,12 @@ function (array $values) { $permissions ); - $this->connection->expects($this->exactly(count($permissions))) + $this->connection->expects($this->once()) + ->method('fetchAll') + ->with('SELECT name FROM oro_security_permission') + ->willReturn([['name' => 'ASSIGN']]); + + $this->connection->expects($this->exactly($countCalls)) ->method('executeUpdate') ->willReturnCallback( function ($query, array $params = [], array $types = []) use (&$data) { diff --git a/src/Oro/Bundle/SecurityBundle/Tests/Unit/Util/PropertyPathSecurityHelperTest.php b/src/Oro/Bundle/SecurityBundle/Tests/Unit/Util/PropertyPathSecurityHelperTest.php new file mode 100644 index 00000000000..59dade8de39 --- /dev/null +++ b/src/Oro/Bundle/SecurityBundle/Tests/Unit/Util/PropertyPathSecurityHelperTest.php @@ -0,0 +1,105 @@ +authorizationChecker = $this + ->getMock('Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface'); + $this->managerRegistry = $this->getMockBuilder('Doctrine\Common\Persistence\ManagerRegistry') + ->disableOriginalConstructor() + ->getMock(); + $this->entityConfigProvider = $this->getMockBuilder('Oro\Bundle\EntityConfigBundle\Provider\ConfigProvider') + ->disableOriginalConstructor() + ->getMock(); + $this->helper = new PropertyPathSecurityHelper( + $this->authorizationChecker, + $this->managerRegistry, + $this->entityConfigProvider + ); + } + + public function testIsGrantedByPropertyPath() + { + $address = new CmsAddress(); + $address->city = 'test'; + $user = new CmsUser(); + $user->id = 1; + $user->setAddress($address); + $article = new CmsArticle(); + $article->setAuthor($user); + + $addressMetadata = new ClassMetadata(get_class($address)); + $userMetadata = new ClassMetadata(get_class($user)); + $user->associationMappings = [ + 'address' => ['targetEntity' => get_class($address)] + ]; + $articleMetadata = new ClassMetadata(get_class($article)); + $articleMetadata->associationMappings = [ + 'user' => ['targetEntity' => get_class($user)] + ]; + + $propertyPath = 'user.address.city'; + + $em = $this->getMockBuilder('Doctrine\Common\Persistence\ObjectManager') + ->disableOriginalConstructor() + ->getMock(); + $this->managerRegistry->expects($this->any()) + ->method('getManagerForClass') + ->willReturn($em); + $em->expects($this->any()) + ->method('getClassMetadata') + ->willReturnMap( + [ + [get_class($address), $addressMetadata], + [get_class($user), $userMetadata], + [get_class($article), $articleMetadata] + ] + ); + + $this->authorizationChecker->expects($this->any()) + ->method('isGranted') + ->willReturnCallback(function ($permission, $object) { + if ($object instanceof FieldVote) { + if ($object->getField() === 'city') { + return false; + } + } + + return true; + }); + + $isGranted = $this->helper->isGrantedByPropertyPath($article, $propertyPath, 'EDIT'); + $this->assertFalse($isGranted); + } + + /** + * @expectedException \InvalidArgumentException + * @expectedExceptionMessage Can't get entity manager for class stdClass + */ + public function testIisGrantedByPropertyPathOnWrongClass() + { + $this->helper->isGrantedByPropertyPath(new \stdClass(), 'somePath', 'EDIT'); + } +} diff --git a/src/Oro/Bundle/SecurityBundle/Util/PropertyPathSecurityHelper.php b/src/Oro/Bundle/SecurityBundle/Util/PropertyPathSecurityHelper.php new file mode 100644 index 00000000000..5f106a460cc --- /dev/null +++ b/src/Oro/Bundle/SecurityBundle/Util/PropertyPathSecurityHelper.php @@ -0,0 +1,138 @@ +authorizationChecker = $authorizationChecker; + $this->managerRegistry = $managerRegistry; + $this->entityConfigProvider = $entityConfigProvider; + } + + /** + * Check access by given property path. Would be checked all the property path parts. + * For example, if $propertyPath = 'firstLevelRelation.secondLevelRelation.someField', + * the next checks will be processes: + * - check field access to 'firstLevelRelation' field for given $object + * - check object access to the 'firstLevelRelation' object value + * - check field access to 'secondLevelRelation' field for the 'firstLevelRelation' object value + * - check object access to the 'secondLevelRelation' object value + * - check field access to 'someField' field for the 'secondLevelRelation' object value + * + * In case if on the some step we will have no object, access will be checked on class level. + * + * @param object $object + * @param string $propertyPath + * @param string $permission + * + * @return bool + */ + public function isGrantedByPropertyPath($object, $propertyPath, $permission = 'VIEW') + { + $propertyAccessor = PropertyAccess::createPropertyAccessor(); + $propertyPath = new PropertyPath($propertyPath); + $pathElements = array_values($propertyPath->getElements()); + $stepsCount = $propertyPath->getLength(); + + // prepare data for the first check iteration + $className = ClassUtils::getClass($object); + $metadata = $this->getMetadataForClass($className); + + foreach ($pathElements as $id => $field) { + // check access on field level + if (!$this->checkIsGranted($permission, $object, $className, $field)) { + return false; + } + + $hasAssociation = $metadata->hasAssociation($field) + || $this->entityConfigProvider->hasConfig($className, $field); + + // check access on object level in case if current step if not final and prepare data + // for the next step + if ($hasAssociation && ($stepsCount - 1) !== $id) { + // get object from the relation to make checks on it + $object = $propertyAccessor->getValue($object, $field); + if (!$this->checkIsGranted($permission, $object, $className) + ) { + return false; + } + + // prepare data for the next step + $className = $metadata->hasAssociation($field) + ? $metadata->getAssociationTargetClass($field) + : $this->entityConfigProvider->getConfig($className, $field)->getId()->getClassName(); + $metadata = $this->getMetadataForClass($className); + } + } + + return true; + } + + /** + * Check access for given attributes + * + * @param string $permission + * @param object $object + * @param string $className + * @param string $field + * + * @return bool + */ + protected function checkIsGranted($permission, $object = null, $className = null, $field = null) + { + // in case if we have no object, check access on class level + $object = $object ?: new ObjectIdentity('entity', $className); + + // in case if we have field, check Field access + $object = $field ? new FieldVote($object, $field) : $object; + + return $this->authorizationChecker->isGranted($permission, $object); + } + + /** + * @param string $class + * + * @return ClassMetadata + * @throws \Exception + */ + protected function getMetadataForClass($class) + { + $entityManager = $this->managerRegistry->getManagerForClass($class); + if (!$entityManager) { + throw new \InvalidArgumentException(sprintf('Can\'t get entity manager for class %s', $class)); + } + + return $entityManager->getClassMetadata($class); + } +} diff --git a/src/Oro/Bundle/SecurityBundle/Validator/Constraints/FieldAccessGranted.php b/src/Oro/Bundle/SecurityBundle/Validator/Constraints/FieldAccessGranted.php new file mode 100644 index 00000000000..19d614e2b89 --- /dev/null +++ b/src/Oro/Bundle/SecurityBundle/Validator/Constraints/FieldAccessGranted.php @@ -0,0 +1,10 @@ +=5.5.9", - "symfony/symfony": "2.8.*", + "symfony/symfony": "2.8.*, !=2.8.10", "ass/xmlsecurity": "dev-master", "oro/config": "dev-master", "oro/config-expression": "dev-master", diff --git a/src/Oro/Bundle/SegmentBundle/composer.json b/src/Oro/Bundle/SegmentBundle/composer.json index 492d4fc1b13..ccf08da414a 100644 --- a/src/Oro/Bundle/SegmentBundle/composer.json +++ b/src/Oro/Bundle/SegmentBundle/composer.json @@ -7,7 +7,7 @@ "license": "MIT", "require": { "php": ">=5.5.9", - "symfony/symfony": "2.8.*", + "symfony/symfony": "2.8.*, !=2.8.10", "oro/query-designer-bundle": "dev-master" }, "autoload": { diff --git a/src/Oro/Bundle/SidebarBundle/composer.json b/src/Oro/Bundle/SidebarBundle/composer.json index 258038eca8b..29f691ac503 100644 --- a/src/Oro/Bundle/SidebarBundle/composer.json +++ b/src/Oro/Bundle/SidebarBundle/composer.json @@ -7,7 +7,7 @@ "license": "MIT", "require": { "php": ">=5.5.9", - "symfony/symfony": "2.8.*", + "symfony/symfony": "2.8.*, !=2.8.10", "oro/platform-bundle": "dev-master", "oro/requirejs-bundle": "dev-master" }, diff --git a/src/Oro/Bundle/SoapBundle/composer.json b/src/Oro/Bundle/SoapBundle/composer.json index a9258589cbf..e2ceb17eb6c 100644 --- a/src/Oro/Bundle/SoapBundle/composer.json +++ b/src/Oro/Bundle/SoapBundle/composer.json @@ -7,7 +7,7 @@ "license": "MIT", "require": { "php": ">=5.5.9", - "symfony/symfony": "2.8.*", + "symfony/symfony": "2.8.*, !=2.8.10", "besimple/soap-common": "dev-master", "besimple/soap-server": "dev-master", "besimple/soap-bundle": "dev-master", diff --git a/src/Oro/Bundle/SyncBundle/composer.json b/src/Oro/Bundle/SyncBundle/composer.json index 086bac1d0f0..45247c55b41 100644 --- a/src/Oro/Bundle/SyncBundle/composer.json +++ b/src/Oro/Bundle/SyncBundle/composer.json @@ -7,7 +7,7 @@ "license": "MIT", "require": { "php": ">=5.5.9", - "symfony/symfony": "2.8.*", + "symfony/symfony": "2.8.*, !=2.8.10", "jdare/clank-bundle": "dev-master", "oro/platform-bundle": "dev-master", "oro/requirejs-bundle": "dev-master" diff --git a/src/Oro/Bundle/TagBundle/Grid/AbstractTagsExtension.php b/src/Oro/Bundle/TagBundle/Grid/AbstractTagsExtension.php index 65ff7ba82f8..219afc6011d 100644 --- a/src/Oro/Bundle/TagBundle/Grid/AbstractTagsExtension.php +++ b/src/Oro/Bundle/TagBundle/Grid/AbstractTagsExtension.php @@ -14,6 +14,7 @@ abstract class AbstractTagsExtension extends AbstractExtension const GRID_COLUMN_ALIAS_PATH = '[source][query_config][column_aliases]'; const GRID_FILTERS_PATH = '[filters][columns]'; const GRID_SORTERS_PATH = '[sorters][columns]'; + /** @deprecated since 1.10. Use config->getName() instead */ const GRID_NAME_PATH = 'name'; const FILTER_COLUMN_NAME = 'tagname'; const PROPERTY_ID_PATH = '[properties][id]'; @@ -48,7 +49,7 @@ public function __construct( */ protected function isReportOrSegmentGrid(DatagridConfiguration $config) { - $gridName = $config->offsetGetByPath(self::GRID_NAME_PATH); + $gridName = $config->getName(); return strpos($gridName, 'oro_report') === 0 || diff --git a/src/Oro/Bundle/TagBundle/composer.json b/src/Oro/Bundle/TagBundle/composer.json index 36b516fb928..37090a0024e 100644 --- a/src/Oro/Bundle/TagBundle/composer.json +++ b/src/Oro/Bundle/TagBundle/composer.json @@ -6,7 +6,7 @@ "license": "MIT", "require": { "php": ">=5.5.9", - "symfony/symfony": "2.8.*", + "symfony/symfony": "2.8.*, !=2.8.10", "oro/form-bundle": "dev-master", "oro/requirejs-bundle": "dev-master" }, diff --git a/src/Oro/Bundle/TestFrameworkBundle/composer.json b/src/Oro/Bundle/TestFrameworkBundle/composer.json index a4233d6f7e5..6b717dd929a 100644 --- a/src/Oro/Bundle/TestFrameworkBundle/composer.json +++ b/src/Oro/Bundle/TestFrameworkBundle/composer.json @@ -7,7 +7,7 @@ "license": "MIT", "require": { "php": ">=5.5.9", - "symfony/symfony": "2.8.*" + "symfony/symfony": "2.8.*, !=2.8.10" }, "autoload": { "psr-0": { "Oro\\Bundle\\TestFrameworkBundle": "" } diff --git a/src/Oro/Bundle/ThemeBundle/composer.json b/src/Oro/Bundle/ThemeBundle/composer.json index 4eb208d3652..f6fc4339e50 100644 --- a/src/Oro/Bundle/ThemeBundle/composer.json +++ b/src/Oro/Bundle/ThemeBundle/composer.json @@ -7,7 +7,7 @@ "license": "MIT", "require": { "php": ">=5.5.9", - "symfony/symfony": "2.8.*", + "symfony/symfony": "2.8.*, !=2.8.10", "oro/assetic-bundle": "@dev" }, "autoload": { diff --git a/src/Oro/Bundle/TrackingBundle/composer.json b/src/Oro/Bundle/TrackingBundle/composer.json index b5d8ea1d6bb..e42b18411c0 100644 --- a/src/Oro/Bundle/TrackingBundle/composer.json +++ b/src/Oro/Bundle/TrackingBundle/composer.json @@ -6,7 +6,7 @@ "license": "MIT", "require": { "php": ">=5.5.9", - "symfony/symfony": "2.8.*", + "symfony/symfony": "2.8.*, !=2.8.10", "doctrine/orm": ">=2.2.3,<2.4-dev", "doctrine/doctrine-bundle": "1.2.*", "doctrine/data-fixtures": "@dev", diff --git a/src/Oro/Bundle/TranslationBundle/composer.json b/src/Oro/Bundle/TranslationBundle/composer.json index f3fbb356e18..2d738f4dcf9 100644 --- a/src/Oro/Bundle/TranslationBundle/composer.json +++ b/src/Oro/Bundle/TranslationBundle/composer.json @@ -7,7 +7,7 @@ "license": "MIT", "require": { "php": ">=5.5.9", - "symfony/symfony": "2.8.*", + "symfony/symfony": "2.8.*, !=2.8.10", "doctrine/data-fixtures": "@dev", "doctrine/doctrine-fixtures-bundle": "@dev", "gedmo/doctrine-extensions": "2.3.*", diff --git a/src/Oro/Bundle/UIBundle/Resources/config/requirejs.yml b/src/Oro/Bundle/UIBundle/Resources/config/requirejs.yml index f178cd6bf2e..313ed2a7197 100644 --- a/src/Oro/Bundle/UIBundle/Resources/config/requirejs.yml +++ b/src/Oro/Bundle/UIBundle/Resources/config/requirejs.yml @@ -90,6 +90,8 @@ config: 'jquery': 'jquery' 'oroui/js/extend/select2': 'jquery.select2': 'jquery.select2' + 'oroui/js/select2-l10n': + 'jquery.select2': 'jquery.select2' 'oroui/js/extend/jquery.multiselect': 'jquery.multiselect': 'jquery.multiselect' 'oroui/js/extend/jquery.multiselect-filter': diff --git a/src/Oro/Bundle/UIBundle/Resources/public/css/less/oro.less b/src/Oro/Bundle/UIBundle/Resources/public/css/less/oro.less index 6eb18013ab9..5e8a8bc0de9 100644 --- a/src/Oro/Bundle/UIBundle/Resources/public/css/less/oro.less +++ b/src/Oro/Bundle/UIBundle/Resources/public/css/less/oro.less @@ -3043,6 +3043,9 @@ span.validation-failed { .popover-content { padding: 13px 30px 13px 15px; height: 100%; + &.popover-no-close-button { + padding-right: 15px; + } } .oro-popover-content { overflow-y: auto; diff --git a/src/Oro/Bundle/UIBundle/Resources/public/js/extend/select2.js b/src/Oro/Bundle/UIBundle/Resources/public/js/extend/select2.js index 9cd7df76a6d..50500dffc2d 100644 --- a/src/Oro/Bundle/UIBundle/Resources/public/js/extend/select2.js +++ b/src/Oro/Bundle/UIBundle/Resources/public/js/extend/select2.js @@ -1,6 +1,11 @@ -define(['jquery', 'underscore', 'orotranslation/js/translator', 'jquery.select2'], function($, _, __, Select2) { +define(function(require) { 'use strict'; + var $ = require('jquery'); + var _ = require('underscore'); + var Select2 = require('jquery.select2'); + require('oroui/js/select2-l10n'); + /** * An overload of populateResults method, * renders search results with collapsible groups @@ -411,9 +416,4 @@ define(['jquery', 'underscore', 'orotranslation/js/translator', 'jquery.select2' prototype.moveHighlight = _.wrap(prototype.moveHighlight, overrideMethods.moveHighlight); }(Select2['class'].multi.prototype)); - - $.fn.select2.defaults = $.extend($.fn.select2.defaults, { - formatSearching: function() { return __('Searching...'); }, - formatNoMatches: function() { return __('No matches found'); } - }); }); diff --git a/src/Oro/Bundle/UIBundle/Resources/public/js/select2-l10n.js b/src/Oro/Bundle/UIBundle/Resources/public/js/select2-l10n.js new file mode 100644 index 00000000000..924588c78da --- /dev/null +++ b/src/Oro/Bundle/UIBundle/Resources/public/js/select2-l10n.js @@ -0,0 +1,30 @@ +define(function(require) { + 'use strict'; + + var __ = require('orotranslation/js/translator'); + var $ = require('jquery'); + require('jquery.select2'); + + $.fn.select2.defaults = $.extend($.fn.select2.defaults, { + formatNoMatches: function() { + return __('No matches found'); + }, + formatInputTooShort: function(input, min) { + var number = min - input.length; + return __('oro.ui.select2.input_too_short', {number: number}, number); + }, + formatInputTooLong: function(input, max) { + var number = input.length - max; + return __('oro.ui.select2.input_too_long', {number: number}, number); + }, + formatSelectionTooBig: function(limit) { + return __('oro.ui.select2.selection_too_big', {limit: limit}, limit); + }, + formatLoadMore: function() { + return __('oro.ui.select2.load_more'); + }, + formatSearching: function() { + return __('Searching...'); + } + }); +}); diff --git a/src/Oro/Bundle/UIBundle/Resources/public/js/tools/text-util.js b/src/Oro/Bundle/UIBundle/Resources/public/js/tools/text-util.js new file mode 100644 index 00000000000..3acd077165d --- /dev/null +++ b/src/Oro/Bundle/UIBundle/Resources/public/js/tools/text-util.js @@ -0,0 +1,88 @@ +define(['orotranslation/js/translator'], function(__) { + 'use strict'; + + // matches "A. L. Price", "In progress", "one of all" + var shortWordsAtStartRegExp = /^((\w{1,3}|\w\.)\s+(\w{1,3}|\w\.)\s+|(\w{1,3}|\w\.?)\s+)(\w+|$)/; + + // matches "A. L. Price", "In progress", "one of all" + var shortWordsAtEndRegExp = /(\w+|^)(\s+(\w{1,3}|\w\.)\s+(\w{1,3}|\w\.)|\s+(\w{1,3}|\w\.?))$/; + + var postpositionsRegExp = new RegExp('\\s+(' + __('postpositions') + ')(\\W|$)', 'gi'); + var prepositionsRegExp = new RegExp('(\\W|^)(' + __('prepositions') + ')((\\s+)(' + + __('articles') + ')|)\\s+', 'gi'); + + var abbreviateIgnoreList = __('abbreviate_ignore_list').split('|'); + + /** + * @export oroui/js/tools/text-util + */ + return { + /** + * Prepares text for output + * + * @param {string} text + * @returns {string} + */ + prepareText: function(text) { + // disallow line breaks at start of string if there are short words at the start + text = text.replace(shortWordsAtStartRegExp, function() { + // can be one of two following cases + // ["A. L. Price", "A. L. ", "A.", "L.", undefined, "Price", 0, "A. L. Price"] + // ["Roy Greenwell", "Roy ", undefined, undefined, "Roy", "Greenwell", 0, "Roy Greenwell"] + if (arguments[4]) { + return arguments[4] + /* nbsp */'\u00A0' + arguments[5]; + } else { + return arguments[2] + /* nbsp */'\u00A0' + arguments[3] + /* nbsp */'\u00A0' + arguments[5]; + } + }); + // disallow line breaks at end of string if there are short words at the end + text = text.replace(shortWordsAtEndRegExp, function() { + // can be one of two following cases + // ["Body big", "Body", " big", undefined, undefined, "big", 0, "Body big"] + // ["Boston Sea tea", "Boston", " Sea tea", "Sea", "tea", undefined, 0, "Boston Sea tea"] + if (arguments[5]) { + return arguments[1] + /* nbsp */'\u00A0' + arguments[5]; + } else { + return arguments[1] + /* nbsp */'\u00A0' + arguments[3] + /* nbsp */'\u00A0' + arguments[4]; + } + }); + // process postpositions + text = text.replace(postpositionsRegExp, '\u00A0$1$2'); + // console.log(text.replace(/\u00A0/g, '^')); + + // process prepositions + text = text.replace(prepositionsRegExp, function() { + if (arguments[5]) { + // with article + return arguments[1] + arguments[2] + '\u00A0' + arguments[5] + '\u00A0'; + } else { + // without article + return arguments[1] + arguments[2] + '\u00A0'; + } + }); + // console.log(text.replace(/\u00A0/g, '^')); + + return text; + }, + + /** + * Abbreviates text if it has more than `minWordsCount` + * + * @param {string} text + * @param {number} minWordsCount + * @return {string} + */ + abbreviate: function(text, minWordsCount) { + var words = text.split(/\W+/); + if (words.length < minWordsCount) { + return text; + } + return words.map(function(word) { + if (abbreviateIgnoreList.indexOf(word.toLowerCase()) !== -1) { + return ''; + } + return word[0].toUpperCase(); + }).join(''); + } + }; +}); diff --git a/src/Oro/Bundle/UIBundle/Resources/translations/jsmessages.en.yml b/src/Oro/Bundle/UIBundle/Resources/translations/jsmessages.en.yml index 4a8567cc010..3528b25309c 100644 --- a/src/Oro/Bundle/UIBundle/Resources/translations/jsmessages.en.yml +++ b/src/Oro/Bundle/UIBundle/Resources/translations/jsmessages.en.yml @@ -40,6 +40,7 @@ Microsecond: Microsecond "Choose File": "Choose File" "Searching...": "Searching..." "No matches found": "No matches found" +"Loading more results...": "Loading more results..." OK: OK "N/A": "N/A" maximize: maximize @@ -84,3 +85,12 @@ oro: other: Other jstree: move_node_error: "You can not move node {{ nodeText }} here." + select2: + input_too_short: "{1}Please enter {{ number }} more character|[2,Inf[Please enter {{ number }} more characters" + input_too_long: "{1}Please delete {{ number }} character|[2,Inf[Please delete {{ number }} characters" + selection_too_big: "{1}You can only select {{ limit }} item|[2,Inf[You can only select {{ limit }} items" + load_more: "Loading more results..." +articles: "a|an|the|some|every|any|no" +abbreviate_ignore_list: "a|an|the|of|at|per|in|on" +postpositions: "ago|apart|aside|away|hence|through" +prepositions: "a|an|per|aboard|about|bout|above|abreast|abroad|across|after|against|again|along|amid|among|around|as|astride|at|atop|ontop|bar|before|afore|tofore|B4|behind|ahind|below|ablow|beneath|beside|besides|between|beyond|but|by|come|despite|spite|down|during|except|for|4|from|in|inside|into|less|like|minus|near|of|off|on|onto|opposite|out|outside|over|past|per|post|pre|pro|re|short|since|than|through|thru|throughout|thruout|to|2|@|the|toward|towards|under|underneath|unlike|until|til|till|up|upon|upside|versus|via|vice|with|within|without|worth" diff --git a/src/Oro/Bundle/UIBundle/Tests/Unit/Twig/HtmlTagExtensionTest.php b/src/Oro/Bundle/UIBundle/Tests/Unit/Twig/HtmlTagExtensionTest.php index a33ddc11cfb..d02c95fbe7a 100644 --- a/src/Oro/Bundle/UIBundle/Tests/Unit/Twig/HtmlTagExtensionTest.php +++ b/src/Oro/Bundle/UIBundle/Tests/Unit/Twig/HtmlTagExtensionTest.php @@ -32,18 +32,13 @@ public function testGetName() public function testGetFilters() { - $filters = $this->extension->getFilters(); - - $this->assertTrue(is_array($filters)); - $this->assertEquals(3, sizeof($filters)); - - $filter = $filters[0]; - $this->assertInstanceOf('\Twig_SimpleFilter', $filter); - $this->assertEquals($filter->getName(), 'oro_tag_filter'); - $callable = $filter->getCallable(); - $this->assertTrue(is_array($callable)); - $this->assertEquals(2, sizeof($callable)); - $this->assertEquals($callable[0], $this->extension); - $this->assertEquals($callable[1], 'tagFilter'); + $this->assertEquals( + [ + new \Twig_SimpleFilter('oro_tag_filter', [$this->extension, 'tagFilter'], ['is_safe' => ['all']]), + new \Twig_SimpleFilter('oro_html_purify', [$this->extension, 'htmlPurify']), + new \Twig_SimpleFilter('oro_html_sanitize', [$this->extension, 'htmlSanitize'], ['is_safe' => ['html']]) + ], + $this->extension->getFilters() + ); } } diff --git a/src/Oro/Bundle/UIBundle/Tools/HtmlTagHelper.php b/src/Oro/Bundle/UIBundle/Tools/HtmlTagHelper.php index 852b1d9e03c..00eb9371393 100644 --- a/src/Oro/Bundle/UIBundle/Tools/HtmlTagHelper.php +++ b/src/Oro/Bundle/UIBundle/Tools/HtmlTagHelper.php @@ -15,6 +15,9 @@ class HtmlTagHelper /** @var string */ protected $cacheDir; + /** @var SanitizeHTMLTransformer|null */ + protected $purifyTransformer; + /** * @param HtmlTagProvider $htmlTagProvider * @param string|null $cacheDir @@ -48,9 +51,11 @@ public function sanitize($string) */ public function purify($string) { - $transformer = new SanitizeHTMLTransformer(null, $this->cacheDir); + if (!$this->purifyTransformer) { + $this->purifyTransformer = new SanitizeHTMLTransformer(null, $this->cacheDir); + } - return trim($transformer->transform($string)); + return trim($this->purifyTransformer->transform($string)); } /** diff --git a/src/Oro/Bundle/UserBundle/Autocomplete/UserAclHandler.php b/src/Oro/Bundle/UserBundle/Autocomplete/UserAclHandler.php index fa1be57afe0..41cd6060b90 100644 --- a/src/Oro/Bundle/UserBundle/Autocomplete/UserAclHandler.php +++ b/src/Oro/Bundle/UserBundle/Autocomplete/UserAclHandler.php @@ -270,29 +270,33 @@ protected function addSearchCriteria(QueryBuilder $queryBuilder, $search) 'where', $queryBuilder->expr()->orX( $queryBuilder->expr()->like( - $queryBuilder->expr()->concat( - 'user.firstName', + $queryBuilder->expr()->lower( $queryBuilder->expr()->concat( - $queryBuilder->expr()->literal(' '), - 'user.lastName' + 'user.firstName', + $queryBuilder->expr()->concat( + $queryBuilder->expr()->literal(' '), + 'user.lastName' + ) ) ), '?1' ), $queryBuilder->expr()->like( - $queryBuilder->expr()->concat( - 'user.lastName', + $queryBuilder->expr()->lower( $queryBuilder->expr()->concat( - $queryBuilder->expr()->literal(' '), - 'user.firstName' + 'user.lastName', + $queryBuilder->expr()->concat( + $queryBuilder->expr()->literal(' '), + 'user.firstName' + ) ) ), '?1' ), - $queryBuilder->expr()->like('user.username', '?1') + $queryBuilder->expr()->like($queryBuilder->expr()->lower('user.username'), '?1') ) ) - ->setParameter(1, '%' . str_replace(' ', '%', $search) . '%'); + ->setParameter(1, '%' . str_replace(' ', '%', strtolower($search)) . '%'); } /** diff --git a/src/Oro/Bundle/UserBundle/composer.json b/src/Oro/Bundle/UserBundle/composer.json index c8aed5b0078..0b122dc08a5 100644 --- a/src/Oro/Bundle/UserBundle/composer.json +++ b/src/Oro/Bundle/UserBundle/composer.json @@ -7,7 +7,7 @@ "license": "MIT", "require": { "php": ">=5.5.9", - "symfony/symfony": "2.8.*", + "symfony/symfony": "2.8.*, !=2.8.10", "friendsofsymfony/rest-bundle": "1.5.0-RC2", "nelmio/api-doc-bundle": "dev-master", "escapestudios/wsse-authentication-bundle": "2.3.x-dev", diff --git a/src/Oro/Bundle/WindowsBundle/composer.json b/src/Oro/Bundle/WindowsBundle/composer.json index 315c1f9b185..0d259dab879 100644 --- a/src/Oro/Bundle/WindowsBundle/composer.json +++ b/src/Oro/Bundle/WindowsBundle/composer.json @@ -7,7 +7,7 @@ "license": "MIT", "require": { "php": ">=5.5.9", - "symfony/symfony": "2.8.*", + "symfony/symfony": "2.8.*, !=2.8.10", "oro/ui-bundle": "dev-master", "friendsofsymfony/rest-bundle": "1.5.0-RC2", "nelmio/api-doc-bundle": "dev-master", diff --git a/src/Oro/Bundle/WorkflowBundle/Form/Type/WorkflowAttributesType.php b/src/Oro/Bundle/WorkflowBundle/Form/Type/WorkflowAttributesType.php index a5e87a6a673..e28101e1f9e 100644 --- a/src/Oro/Bundle/WorkflowBundle/Form/Type/WorkflowAttributesType.php +++ b/src/Oro/Bundle/WorkflowBundle/Form/Type/WorkflowAttributesType.php @@ -8,11 +8,12 @@ use Symfony\Component\OptionsResolver\OptionsResolverInterface; use Symfony\Component\OptionsResolver\Options; use Symfony\Component\EventDispatcher\EventDispatcherInterface; -use Symfony\Component\Security\Acl\Voter\FieldVote; +use Symfony\Component\PropertyAccess\PropertyPath; use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface; use Oro\Bundle\ActionBundle\Model\Attribute; use Oro\Bundle\ActionBundle\Model\AttributeGuesser; +use Oro\Bundle\SecurityBundle\Util\PropertyPathSecurityHelper; use Oro\Bundle\WorkflowBundle\Form\EventListener\DefaultValuesListener; use Oro\Bundle\WorkflowBundle\Form\EventListener\InitActionsListener; use Oro\Bundle\WorkflowBundle\Form\EventListener\RequiredAttributesListener; @@ -63,9 +64,9 @@ class WorkflowAttributesType extends AbstractType protected $dispatcher; /** - * @var AuthorizationCheckerInterface + * @var PropertyPathSecurityHelper */ - protected $authorizationChecker; + protected $propertyPathSecurityHelper; /** * @param WorkflowRegistry $workflowRegistry @@ -85,7 +86,7 @@ public function __construct( RequiredAttributesListener $requiredAttributesListener, ContextAccessor $contextAccessor, EventDispatcherInterface $dispatcher, - AuthorizationCheckerInterface $authorizationChecker + PropertyPathSecurityHelper $propertyPathSecurityHelper ) { $this->workflowRegistry = $workflowRegistry; $this->attributeGuesser = $attributeGuesser; @@ -94,7 +95,7 @@ public function __construct( $this->requiredAttributesListener = $requiredAttributesListener; $this->contextAccessor = $contextAccessor; $this->dispatcher = $dispatcher; - $this->authorizationChecker = $authorizationChecker; + $this->propertyPathSecurityHelper = $propertyPathSecurityHelper; } /** @@ -147,6 +148,11 @@ protected function addAttributes(FormBuilderInterface $builder, array $options) { /** @var Workflow $workflow */ $workflow = $options['workflow']; + $attributes = []; + $config = $workflow->getDefinition()->getConfiguration(); + if (isset($config['attributes'])) { + $attributes = $workflow->getDefinition()->getConfiguration()['attributes']; + } $entity = null; if (isset($options['data'])) { /** @var WorkflowData $data */ @@ -168,7 +174,16 @@ protected function addAttributes(FormBuilderInterface $builder, array $options) if (null === $attributeOptions) { $attributeOptions = array(); } - if (!$entity || $this->isEditableField($entity, $attribute->getName())) { + $fieldName = $attribute->getName(); + if (isset($attributes[$fieldName])) { + $attributeConfiguration = $attributes[$fieldName]; + if (array_key_exists('property_path', $attributeConfiguration) + && $attributeConfiguration['property_path'] + ) { + $fieldName = $attributeConfiguration['property_path']; + } + } + if (!$entity || $this->isEditableField($entity, $fieldName)) { $this->addAttributeField($builder, $attribute, $attributeOptions, $options); } } @@ -182,9 +197,17 @@ protected function addAttributes(FormBuilderInterface $builder, array $options) */ protected function isEditableField($entity, $fieldName) { - return $this->authorizationChecker->isGranted( - 'EDIT', - new FieldVote($entity, $fieldName) + $propertyPath = new PropertyPath($fieldName); + $pathElements = array_values($propertyPath->getElements()); + if ($propertyPath->getLength() >= 2) { + array_shift($pathElements); + $fieldName = implode('.', $pathElements); + } + + return $this->propertyPathSecurityHelper->isGrantedByPropertyPath( + $entity, + $fieldName, + 'EDIT' ); } diff --git a/src/Oro/Bundle/WorkflowBundle/Resources/config/form_types.yml b/src/Oro/Bundle/WorkflowBundle/Resources/config/form_types.yml index 124113dc2a3..ae1511a98bc 100644 --- a/src/Oro/Bundle/WorkflowBundle/Resources/config/form_types.yml +++ b/src/Oro/Bundle/WorkflowBundle/Resources/config/form_types.yml @@ -32,7 +32,7 @@ services: - '@oro_workflow.form.event_listener.required_attributes' - '@oro_action.context_accessor' - '@event_dispatcher' - - '@security.authorization_checker' + - '@oro_security.helper.property_path_helper' tags: - { name: form.type, alias: oro_workflow_attributes } diff --git a/src/Oro/Bundle/WorkflowBundle/Tests/Unit/Form/Type/AbstractWorkflowAttributesTypeTestCase.php b/src/Oro/Bundle/WorkflowBundle/Tests/Unit/Form/Type/AbstractWorkflowAttributesTypeTestCase.php index 9c5fd302b87..69d343d3d51 100644 --- a/src/Oro/Bundle/WorkflowBundle/Tests/Unit/Form/Type/AbstractWorkflowAttributesTypeTestCase.php +++ b/src/Oro/Bundle/WorkflowBundle/Tests/Unit/Form/Type/AbstractWorkflowAttributesTypeTestCase.php @@ -2,6 +2,7 @@ namespace Oro\Bundle\WorkflowBundle\Tests\Unit\Form\Type; +use Oro\Bundle\SecurityBundle\Util\PropertyPathSecurityHelper; use Symfony\Component\Form\Test\FormIntegrationTestCase; use Symfony\Component\EventDispatcher\EventDispatcherInterface; @@ -129,7 +130,7 @@ protected function createWorkflowAttributesType( InitActionsListener $initActionListener = null, RequiredAttributesListener $requiredAttributesListener = null, EventDispatcherInterface $dispatcher = null, - AuthorizationCheckerInterface $authorizationChecker = null + PropertyPathSecurityHelper $propertyPathSecurityHelper = null ) { if (!$workflowRegistry) { $workflowRegistry = $this->createWorkflowRegistryMock(); @@ -149,8 +150,8 @@ protected function createWorkflowAttributesType( if (!$dispatcher) { $dispatcher = $this->createDispatcherMock(); } - if (!$authorizationChecker) { - $authorizationChecker = $this->createAuthorizationCheckerMock(); + if (!$propertyPathSecurityHelper) { + $propertyPathSecurityHelper = $this->createPropertyPathSecurityHelper(); } return new WorkflowAttributesType( @@ -161,7 +162,7 @@ protected function createWorkflowAttributesType( $requiredAttributesListener, new ContextAccessor(), $dispatcher, - $authorizationChecker + $propertyPathSecurityHelper ); } @@ -219,8 +220,10 @@ protected function createDispatcherMock() ->getMock(); } - protected function createAuthorizationCheckerMock() + protected function createPropertyPathSecurityHelper() { - return $this->getMock('Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface'); + return $this->getMockBuilder('Oro\Bundle\SecurityBundle\Util\PropertyPathSecurityHelper') + ->disableOriginalConstructor() + ->getMock(); } } diff --git a/src/Oro/Bundle/WorkflowBundle/Tests/Unit/Form/Type/WorkflowAttributesTypeTest.php b/src/Oro/Bundle/WorkflowBundle/Tests/Unit/Form/Type/WorkflowAttributesTypeTest.php index 5871be798a8..efb4e41093c 100644 --- a/src/Oro/Bundle/WorkflowBundle/Tests/Unit/Form/Type/WorkflowAttributesTypeTest.php +++ b/src/Oro/Bundle/WorkflowBundle/Tests/Unit/Form/Type/WorkflowAttributesTypeTest.php @@ -47,7 +47,7 @@ class WorkflowAttributesTypeTest extends AbstractWorkflowAttributesTypeTestCase /** * @var \PHPUnit_Framework_MockObject_MockObject */ - protected $authorizationChecker; + protected $propertyPathSecurityHelper; protected function setUp() { @@ -59,7 +59,7 @@ protected function setUp() $this->initActionListener = $this->createInitActionsListenerMock(); $this->requiredAttributesListener = $this->createRequiredAttributesListenerMock(); $this->dispatcher = $this->createDispatcherMock(); - $this->authorizationChecker = $this->createAuthorizationCheckerMock(); + $this->propertyPathSecurityHelper = $this->createPropertyPathSecurityHelper(); $this->type = $this->createWorkflowAttributesType( $this->workflowRegistry, @@ -68,7 +68,7 @@ protected function setUp() $this->initActionListener, $this->requiredAttributesListener, $this->dispatcher, - $this->authorizationChecker + $this->propertyPathSecurityHelper ); } @@ -394,13 +394,13 @@ public function testNotEditableAttributes() ) ); - $this->authorizationChecker->expects($this->at(0)) - ->method('isGranted') - ->with('EDIT', new FieldVote($entity, 'first')) + $this->propertyPathSecurityHelper->expects($this->at(0)) + ->method('isGrantedByPropertyPath') + ->with($entity, 'first', 'EDIT') ->willReturn(true); - $this->authorizationChecker->expects($this->at(1)) - ->method('isGranted') - ->with('EDIT', new FieldVote($entity, 'second')) + $this->propertyPathSecurityHelper->expects($this->at(1)) + ->method('isGrantedByPropertyPath') + ->with($entity, 'second', 'EDIT') ->willReturn(false); $form = $this->factory->create($this->type, $formData, $formOptions); diff --git a/src/Oro/Bundle/WorkflowBundle/composer.json b/src/Oro/Bundle/WorkflowBundle/composer.json index fd0666d8f8c..f35edc432e2 100644 --- a/src/Oro/Bundle/WorkflowBundle/composer.json +++ b/src/Oro/Bundle/WorkflowBundle/composer.json @@ -7,7 +7,7 @@ "license": "MIT", "require": { "php": ">=5.5.9", - "symfony/symfony": "2.8.*", + "symfony/symfony": "2.8.*, !=2.8.10", "doctrine/orm": ">=2.2.3,<2.4-dev", "doctrine/doctrine-bundle": "1.2.*", "doctrine/data-fixtures": "@dev", diff --git a/src/Oro/Component/Config/Common/ConfigObject.php b/src/Oro/Component/Config/Common/ConfigObject.php index 7df0373599a..4490695995a 100644 --- a/src/Oro/Component/Config/Common/ConfigObject.php +++ b/src/Oro/Component/Config/Common/ConfigObject.php @@ -59,17 +59,31 @@ public static function createNamed($name, array $params) * throws exception if current object is unnamed * * @return string - * @throws LogicException + * @throws \LogicException */ public function getName() { if (!isset($this[self::NAME_KEY])) { - throw new \LogicException("Trying to get name of unnamed object"); + throw new \LogicException('Trying to get name of unnamed object'); } return $this[self::NAME_KEY]; } + /** + * Set Object name + * + * $param string $name + * + * @return $this + */ + public function setName($name) + { + $this[self::NAME_KEY] = $name; + + return $this; + } + /** * Returns param array * If keys specified returns only intersection @@ -147,11 +161,7 @@ public function offsetGetByPath($path, $default = null) return $default; } - if ($default === null && $value !== null) { - return $value; - } - - return $value ? : $default; + return null !== $value ? $value : $default; } /** diff --git a/src/Oro/Component/Config/Tests/Unit/Common/ObjectTest.php b/src/Oro/Component/Config/Tests/Unit/Common/ObjectTest.php index 111b55a32cc..fb8ffb3e3ab 100644 --- a/src/Oro/Component/Config/Tests/Unit/Common/ObjectTest.php +++ b/src/Oro/Component/Config/Tests/Unit/Common/ObjectTest.php @@ -7,69 +7,404 @@ class ObjectTest extends \PHPUnit_Framework_TestCase { /** - * @param array $params + * @return ConfigObject + */ + protected function getConfigObject() + { + return ConfigObject::create( + [ + 'true' => true, + 'false' => false, + 'null' => null, + 'array' => [ + 'true' => true, + 'false' => false, + 'null' => null, + ], + ] + ); + } + + /** + * @param string $property + * @param bool $expected + * + * @dataProvider getOffsetExistsDataProvider + */ + public function testOffsetExists($property, $expected) + { + $object = $this->getConfigObject(); + $this->assertEquals($expected, $object->offsetExists($property)); + } + + public function getOffsetExistsDataProvider() + { + return [ + [ + 'property' => 'true', + 'expected' => true, + ], + [ + 'property' => 'false', + 'expected' => true, + ], + [ + 'property' => 'null', + 'expected' => false, + ], + [ + 'property' => 'unknown', + 'expected' => false, + ], + ]; + } + + /** * @param string $path - * @param bool $expected - * @dataProvider getOffsetGetByPathDataProvider + * @param bool $expected + * + * @dataProvider getOffsetExistByPathDataProvider */ - public function testOffsetGetByPath(array $params, $path, $expected) + public function testOffsetExistByPath($path, $expected) { - $object = ConfigObject::create($params); + $object = $this->getConfigObject(); $this->assertEquals($expected, $object->offsetExistByPath($path)); } + public function getOffsetExistByPathDataProvider() + { + return [ + [ + 'path' => '[true]', + 'expected' => true, + ], + [ + 'path' => '[false]', + 'expected' => true, + ], + [ + 'path' => '[null]', + 'expected' => false, + ], + [ + 'path' => '[unknown]', + 'expected' => false, + ], + [ + 'path' => 'true', + 'expected' => true, + ], + [ + 'path' => 'false', + 'expected' => true, + ], + [ + 'path' => 'null', + 'expected' => false, + ], + [ + 'path' => 'unknown', + 'expected' => false, + ], + [ + 'path' => '[array][false]', + 'expected' => true, + ], + [ + 'path' => '[array][true]', + 'expected' => true, + ], + [ + 'path' => '[array][null]', + 'expected' => false, + ], + [ + 'path' => '[array][unknown]', + 'expected' => false, + ], + ]; + } + + /** + * @param string $property + * @param bool $expected + * + * @dataProvider getOffsetGetDataProvider + */ + public function testOffsetGet($property, $expected) + { + $object = $this->getConfigObject(); + $this->assertEquals($expected, $object->offsetGet($property)); + } + + public function getOffsetGetDataProvider() + { + return [ + [ + 'property' => 'true', + 'expected' => true, + ], + [ + 'property' => 'false', + 'expected' => false, + ], + [ + 'property' => 'null', + 'expected' => null, + ], + ]; + } + + /** + * @param string $property + * @param bool $expected + * + * @dataProvider getOffsetGetOrDataProvider + */ + public function testOffsetGetOr($property, $expected) + { + $object = $this->getConfigObject(); + $this->assertEquals($expected, $object->offsetGetOr($property)); + } + + public function getOffsetGetOrDataProvider() + { + return [ + [ + 'property' => 'true', + 'expected' => true, + ], + [ + 'property' => 'false', + 'expected' => false, + ], + [ + 'property' => 'null', + 'expected' => null, + ], + [ + 'property' => 'unknown', + 'expected' => false, + ], + ]; + } + + /** + * @param string $path + * @param bool $expected + * + * @dataProvider getOffsetGetByPathDataProvider + */ + public function testOffsetGetByPath($path, $expected) + { + $object = $this->getConfigObject(); + $this->assertEquals($expected, $object->offsetGetByPath($path)); + } + public function getOffsetGetByPathDataProvider() { - $params = [ - 'true' => true, - 'false' => false, - 'null' => null, - 'array' => [ - 'true' => true, - 'false' => false, - 'null' => null, + return [ + [ + 'path' => '[true]', + 'expected' => true, + ], + [ + 'path' => '[false]', + 'expected' => false, + ], + [ + 'path' => '[null]', + 'expected' => null, + ], + [ + 'path' => '[unknown]', + 'expected' => false, + ], + [ + 'path' => 'true', + 'expected' => true, + ], + [ + 'path' => 'false', + 'expected' => false, + ], + [ + 'path' => 'null', + 'expected' => null, + ], + [ + 'path' => 'unknown', + 'expected' => false, + ], + [ + 'path' => '[array][false]', + 'expected' => false, + ], + [ + 'path' => '[array][true]', + 'expected' => true, + ], + [ + 'path' => '[array][null]', + 'expected' => null, + ], + [ + 'path' => '[array][unknown]', + 'expected' => false, ], ]; + } + + /** + * @param string $property + * @param bool $expected + * + * @dataProvider getOffsetGetOrWithDefaultValueDataProvider + */ + public function testOffsetGetOrWithDefaultValue($property, $expected) + { + $object = $this->getConfigObject(); + $this->assertEquals($expected, $object->offsetGetOr($property, 'default')); + } + + public function getOffsetGetOrWithDefaultValueDataProvider() + { return [ [ - 'params' => $params, - 'path' => '[true]', + 'property' => 'true', 'expected' => true, ], [ - 'params' => $params, - 'path' => '[false]', + 'property' => 'false', + 'expected' => false, + ], + [ + 'property' => 'null', + 'expected' => 'default', + ], + [ + 'property' => 'unknown', + 'expected' => 'default', + ], + ]; + } + + /** + * @param string $path + * @param bool $expected + * + * @dataProvider getOffsetGetByPathWithDefaultValueDataProvider + */ + public function testOffsetGetByPathWithDefaultValue($path, $expected) + { + $object = $this->getConfigObject(); + $this->assertEquals($expected, $object->offsetGetByPath($path, 'default')); + } + + public function getOffsetGetByPathWithDefaultValueDataProvider() + { + return [ + [ + 'path' => '[true]', 'expected' => true, ], [ - 'params' => $params, - 'path' => '[null]', + 'path' => '[false]', 'expected' => false, ], [ - 'params' => $params, - 'path' => '[unknown]', + 'path' => '[null]', + 'expected' => 'default', + ], + [ + 'path' => '[unknown]', + 'expected' => 'default', + ], + [ + 'path' => 'true', + 'expected' => true, + ], + [ + 'path' => 'false', 'expected' => false, ], [ - 'params' => $params, - 'path' => '[array][false]', + 'path' => 'null', + 'expected' => 'default', + ], + [ + 'path' => 'unknown', + 'expected' => 'default', + ], + [ + 'path' => '[array][false]', + 'expected' => false, + ], + [ + 'path' => '[array][true]', 'expected' => true, ], [ - 'params' => $params, + 'path' => '[array][null]', + 'expected' => 'default', + ], + [ + 'path' => '[array][unknown]', + 'expected' => 'default', + ], + ]; + } + + /** + * @param string $path + * + * @dataProvider getOffsetSetByPathWithDefaultValueDataProvider + */ + public function testOffsetSetByPath($path) + { + $object = $this->getConfigObject(); + $value = 'test'; + $this->assertEquals($value, $object->offsetSetByPath($path, $value)->offsetGetByPath($path)); + } + + public function getOffsetSetByPathWithDefaultValueDataProvider() + { + return [ + [ + 'path' => '[true]', + ], + [ + 'path' => '[false]', + ], + [ + 'path' => '[null]', + ], + [ + 'path' => '[unknown]', + ], + [ + 'path' => 'true', + ], + [ + 'path' => 'false', + ], + [ + 'path' => 'null', + ], + [ + 'path' => 'unknown', + ], + [ + 'path' => '[array][false]', + ], + [ 'path' => '[array][true]', - 'expected' => true, ], [ - 'params' => $params, 'path' => '[array][null]', - 'expected' => false, ], [ - 'params' => $params, 'path' => '[array][unknown]', - 'expected' => false, ], ]; } diff --git a/src/Oro/Component/DoctrineUtils/ORM/QueryUtils.php b/src/Oro/Component/DoctrineUtils/ORM/QueryUtils.php index cd1b46f4bff..0a2b1f313b0 100644 --- a/src/Oro/Component/DoctrineUtils/ORM/QueryUtils.php +++ b/src/Oro/Component/DoctrineUtils/ORM/QueryUtils.php @@ -430,6 +430,36 @@ public static function dqlContainsParameter($dql, $parameterName) return (bool) preg_match($pattern, $dql . ' '); } + /** + * @param string $dql + * + * @return array + */ + public static function getDqlAliases($dql) + { + $matches = []; + preg_match_all('/(FROM|JOIN)\s+\S+\s+(AS\s+)?(\S+)/i', $dql, $matches); + + return $matches[3]; + } + + /** + * @param string $dql + * @param array $replacements + * + * @return string + */ + public static function replaceDqlAliases($dql, array $replacements) + { + return array_reduce( + $replacements, + function ($carry, array $replacement) { + return preg_replace(sprintf('/(?<=[^\w\.\:])%s(?=\b)/', $replacement[0]), $replacement[1], $carry); + }, + $dql + ); + } + /** * @param QueryBuilder $qb * @param Expr\Join $join diff --git a/src/Oro/Component/DoctrineUtils/Tests/Unit/ORM/QueryUtilsTest.php b/src/Oro/Component/DoctrineUtils/Tests/Unit/ORM/QueryUtilsTest.php index 7ddc25524dd..b4cc86953db 100644 --- a/src/Oro/Component/DoctrineUtils/Tests/Unit/ORM/QueryUtilsTest.php +++ b/src/Oro/Component/DoctrineUtils/Tests/Unit/ORM/QueryUtilsTest.php @@ -391,6 +391,143 @@ protected function getParameterMock($name) return $parameter; } + /** + * @dataProvider getDqlAliasesDataProvider + */ + public function testGetDqlAliases(callable $dqlFactory, array $expectedAliases) + { + $this->assertEquals($expectedAliases, QueryUtils::getDqlAliases($dqlFactory($this->em))); + } + + public function getDqlAliasesDataProvider() + { + return [ + 'query with fully qualified entity name' => [ + function (EntityManager $em) { + return $em->createQueryBuilder() + ->select('p') + ->from('Oro\Component\DoctrineUtils\Tests\Unit\Fixtures\Entity\Person', 'p') + ->join('p.bestItem', 'i') + ->getDQL(); + }, + ['p', 'i'], + ], + 'query aliased entity name' => [ + function (EntityManager $em) { + return $em->createQueryBuilder() + ->select('p') + ->from('Test:Person', 'p') + ->join('p.bestItem', 'i') + ->getDQL(); + }, + ['p', 'i'], + ], + 'query with subquery' => [ + function (EntityManager $em) { + $qb = $em->createQueryBuilder(); + + return $qb + ->select('p') + ->from('Test:Person', 'p') + ->join('p.bestItem', 'i') + ->where( + $qb->expr()->exists( + $em->createQueryBuilder() + ->select('p2') + ->from('Test:Person', 'p2') + ->join('p2.groups', '_g2') + ->where('p2.id = p.id') + ) + ) + ->getDQL(); + }, + ['p', 'i', 'p2', '_g2'], + ], + 'query with newlines after aliases, AS keyword and case insensitive' => [ + function () { + return <<assertEquals($expectedDql, QueryUtils::replaceDqlAliases($dql, $replacements)); + } + + public function replaceDqlAliasesProvider() + { + return [ + [ + <<propertyAccessor = new PropertyAccessor(); @@ -118,14 +115,7 @@ public function removeOption($id, $optionName) */ protected function getPropertyPath($optionName) { - if (isset($this->cache[$optionName])) { - $propertyPath = $this->cache[$optionName]; - } else { - $propertyPath = new PropertyPath($optionName); - $this->cache[$optionName] = $propertyPath; - } - - return $propertyPath; + return new PropertyPath($optionName); } /** diff --git a/src/Oro/Component/PhpUtils/ArrayUtil.php b/src/Oro/Component/PhpUtils/ArrayUtil.php index 5a82d7a3eac..24bc08eec23 100644 --- a/src/Oro/Component/PhpUtils/ArrayUtil.php +++ b/src/Oro/Component/PhpUtils/ArrayUtil.php @@ -353,7 +353,7 @@ public static function arrayColumn(array $array, $columnKey, $indexKey = null) return []; } - if (empty($columnKey)) { + if (!isset($columnKey)) { throw new \InvalidArgumentException('Column key is empty'); } diff --git a/src/Oro/Component/PhpUtils/Tests/Unit/ArrayUtilTest.php b/src/Oro/Component/PhpUtils/Tests/Unit/ArrayUtilTest.php index 869522da0e6..8b36e367652 100644 --- a/src/Oro/Component/PhpUtils/Tests/Unit/ArrayUtilTest.php +++ b/src/Oro/Component/PhpUtils/Tests/Unit/ArrayUtilTest.php @@ -719,7 +719,15 @@ public function arrayColumnProvider() null, [] ], - + '0 column (key for which empty() is true)' => [ + [ + ['a', 'b'], + ['first', 'second'], + ], + 0, + null, + ['a', 'first'], + ] ]; } diff --git a/src/Oro/Component/PropertyAccess/PropertyAccessor.php b/src/Oro/Component/PropertyAccess/PropertyAccessor.php index af568d947fa..dacda1ede8b 100644 --- a/src/Oro/Component/PropertyAccess/PropertyAccessor.php +++ b/src/Oro/Component/PropertyAccess/PropertyAccessor.php @@ -38,6 +38,9 @@ class PropertyAccessor implements PropertyAccessorInterface /** @var bool */ protected $ignoreInvalidIndices; + /** @var PropertyPathInterface[] */ + protected $propertyPathCache = []; + /** * @param bool $magicCall Determines whether the use of "__call" is enabled * @param bool $ignoreInvalidIndices Determines whether a reading a value by non-existing index @@ -85,18 +88,7 @@ public function __construct($magicCall = false, $ignoreInvalidIndices = false) */ public function setValue(&$object, $propertyPath, $value) { - if (is_string($propertyPath)) { - $propertyPath = new PropertyPath($propertyPath); - } elseif (!$propertyPath instanceof PropertyPathInterface) { - throw new Exception\InvalidPropertyPathException( - sprintf( - 'The property path must be a string or an instance of ' . - '"Symfony\Component\PropertyAccess\PropertyPathInterface". ' . - 'Got: "%s".', - is_object($propertyPath) ? get_class($propertyPath) : gettype($propertyPath) - ) - ); - } + $propertyPath = $this->getPropertyPath($propertyPath); $path = $propertyPath->getElements(); $values = &$this->readPropertiesUntil($object, $propertyPath, true); @@ -162,18 +154,7 @@ public function setValue(&$object, $propertyPath, $value) */ public function remove(&$object, $propertyPath) { - if (is_string($propertyPath)) { - $propertyPath = new PropertyPath($propertyPath); - } elseif (!$propertyPath instanceof PropertyPathInterface) { - throw new Exception\InvalidPropertyPathException( - sprintf( - 'The property path must be a string or an instance of ' . - '"Symfony\Component\PropertyAccess\PropertyPathInterface". ' . - 'Got: "%s".', - is_object($propertyPath) ? get_class($propertyPath) : gettype($propertyPath) - ) - ); - } + $propertyPath = $this->getPropertyPath($propertyPath); $path = $propertyPath->getElements(); $values = &$this->readPropertiesUntil($object, $propertyPath); @@ -254,18 +235,7 @@ public function remove(&$object, $propertyPath) */ public function getValue($object, $propertyPath) { - if (is_string($propertyPath)) { - $propertyPath = new PropertyPath($propertyPath); - } elseif (!$propertyPath instanceof PropertyPathInterface) { - throw new Exception\InvalidPropertyPathException( - sprintf( - 'The property path must be a string or an instance of ' . - '"Symfony\Component\PropertyAccess\PropertyPathInterface". ' . - 'Got: "%s".', - is_object($propertyPath) ? get_class($propertyPath) : gettype($propertyPath) - ) - ); - } + $propertyPath = $this->getPropertyPath($propertyPath); $path = $propertyPath->getElements(); $length = count($path); @@ -282,18 +252,7 @@ public function getValue($object, $propertyPath) */ public function isReadable($objectOrArray, $propertyPath) { - if (is_string($propertyPath)) { - $propertyPath = new PropertyPath($propertyPath); - } elseif (!$propertyPath instanceof PropertyPathInterface) { - throw new Exception\InvalidPropertyPathException( - sprintf( - 'The property path must be a string or an instance of ' . - '"Symfony\Component\PropertyAccess\PropertyPathInterface". ' . - 'Got: "%s".', - is_object($propertyPath) ? get_class($propertyPath) : gettype($propertyPath) - ) - ); - } + $propertyPath = $this->getPropertyPath($propertyPath); try { $this->readPropertiesUntil( @@ -317,18 +276,7 @@ public function isReadable($objectOrArray, $propertyPath) */ public function isWritable($objectOrArray, $propertyPath) { - if (is_string($propertyPath)) { - $propertyPath = new PropertyPath($propertyPath); - } elseif (!$propertyPath instanceof PropertyPathInterface) { - throw new Exception\InvalidPropertyPathException( - sprintf( - 'The property path must be a string or an instance of ' . - '"Symfony\Component\PropertyAccess\PropertyPathInterface". ' . - 'Got: "%s".', - is_object($propertyPath) ? get_class($propertyPath) : gettype($propertyPath) - ) - ); - } + $propertyPath = $this->getPropertyPath($propertyPath); try { $propertyValues = $this->readPropertiesUntil( @@ -858,4 +806,25 @@ protected function isMethodAccessible(\ReflectionClass $class, $methodName, $par return false; } + + /** + * Gets a PropertyPath instance and caches it. + * + * @param string|PropertyPath $propertyPath + * + * @return PropertyPath + */ + protected function getPropertyPath($propertyPath) + { + if ($propertyPath instanceof PropertyPathInterface) { + return $propertyPath; + } + if (isset($this->propertyPathCache[$propertyPath])) { + return $this->propertyPathCache[$propertyPath]; + } + + $propertyPathInstance = new PropertyPath($propertyPath); + + return $this->propertyPathCache[$propertyPath] = $propertyPathInstance; + } } diff --git a/src/Oro/Component/PropertyAccess/Tests/Unit/PropertyAccessorTest.php b/src/Oro/Component/PropertyAccess/Tests/Unit/PropertyAccessorTest.php index ce078aec297..c1a90cf5859 100644 --- a/src/Oro/Component/PropertyAccess/Tests/Unit/PropertyAccessorTest.php +++ b/src/Oro/Component/PropertyAccess/Tests/Unit/PropertyAccessorTest.php @@ -14,6 +14,7 @@ * @SuppressWarnings(PHPMD.ExcessivePublicCount) * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) * @SuppressWarnings(PHPMD.TooManyMethods) + * @SuppressWarnings(PHPMD.TooManyPublicMethods) */ class PropertyAccessorTest extends \PHPUnit_Framework_TestCase { @@ -89,7 +90,7 @@ public function testGetValueWhenIndexExceptionsDisabled($objectOrArray, $path, $ } /** - * @expectedException \Symfony\Component\PropertyAccess\Exception\InvalidPropertyPathException + * @expectedException \Symfony\Component\PropertyAccess\Exception\InvalidArgumentException */ public function testGetValueThrowsExceptionForInvalidPropertyPathType() { @@ -196,7 +197,7 @@ public function testSetValue($objectOrArray, $path) } /** - * @expectedException \Symfony\Component\PropertyAccess\Exception\InvalidPropertyPathException + * @expectedException \Symfony\Component\PropertyAccess\Exception\InvalidArgumentException */ public function testSetValueThrowsExceptionForInvalidPropertyPathType() { @@ -327,7 +328,7 @@ public function testRemove($objectOrArray, $path) } /** - * @expectedException \Symfony\Component\PropertyAccess\Exception\InvalidPropertyPathException + * @expectedException \Symfony\Component\PropertyAccess\Exception\InvalidArgumentException */ public function testRemoveThrowsExceptionForInvalidPropertyPathType() {