diff --git a/.travis.yml b/.travis.yml index e045108e..b21bab46 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,11 +3,11 @@ language: php php: - 5.3 - 5.4 + - 5.5 env: -# - SYMFONY_VERSION=2.1.* - SYMFONY_VERSION=2.2.* -# - SYMFONY_VERSION=2.3.* + - SYMFONY_VERSION=2.3.* # - SYMFONY_VERSION=dev-master before_script: diff --git a/Admin/Extension/PublishWorkflowExtension.php b/Admin/Extension/PublishTimePeriodExtension.php similarity index 78% rename from Admin/Extension/PublishWorkflowExtension.php rename to Admin/Extension/PublishTimePeriodExtension.php index 54fe59ed..50e0020b 100644 --- a/Admin/Extension/PublishWorkflowExtension.php +++ b/Admin/Extension/PublishTimePeriodExtension.php @@ -6,11 +6,11 @@ use Sonata\AdminBundle\Form\FormMapper; /** - * Admin extension to add publish workflow fields. + * Admin extension to add publish workflow time period fields. * * @author Daniel Leech */ -class PublishWorkflowExtension extends AdminExtension +class PublishTimePeriodExtension extends AdminExtension { public function configureFormFields(FormMapper $formMapper) { @@ -21,10 +21,6 @@ public function configureFormFields(FormMapper $formMapper) $formMapper->with('form.group_publish_workflow', array( 'translation_domain' => 'CmfCoreBundle' - )) - ->add('publishable', 'checkbox', array( - 'required' => false, - ), array( )) ->add('publish_start_date', 'date', $dateOptions, array( 'help' => 'form.help_publish_start_date', diff --git a/Admin/Extension/PublishableExtension.php b/Admin/Extension/PublishableExtension.php new file mode 100644 index 00000000..744ffd07 --- /dev/null +++ b/Admin/Extension/PublishableExtension.php @@ -0,0 +1,26 @@ + + */ +class PublishableExtension extends AdminExtension +{ + public function configureFormFields(FormMapper $formMapper) + { + $formMapper->with('form.group_publish_workflow', array( + 'translation_domain' => 'CmfCoreBundle' + )) + ->add('publishable', 'checkbox', array( + 'required' => false, + ), array( + )) + ->end(); + } +} diff --git a/CHANGELOG.md b/CHANGELOG.md index 167483ab..c97559be 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,23 @@ Changelog ========= +* **2013-06-20**: [PublishWorkflow] The PublishWorkflowChecker now implements + SecurityContextInterface and the individual checks are moved to voters. + Use the service cmf_core.publish_workflow.checker and call + `isGranted('VIEW', $content)` - or `'VIEW_ANONYMOUS'` if you don't want to + see unpublished content even if the current user is allowed to see it. + Configuration was adjusted: The parameter for the role that may see unpublished + content moved from `role` to `publish_workflow.view_non_published_role`. + The security context is also triggered by a core security voter, so that + using the isGranted method of the standard security will check for + publication. + The PublishWorkflowInterface is split into the reading interfaces + PublishableInterface and PublishTimePeriodInterface as well as + PublishableWriteInterface and PublishableTimePeriodWriteInterface. The sonata + admin extension has been split accordingly and there are now + cmf_core.admin_extension.publish_workflow.time_period and + cmf_core.admin_extension.publish_workflow.publishable. + * **2013-05-16**: [PublishWorkFlowChecker] Removed Request argument from check method. Class now accepts a DateTime object to optionally "set" the current time. diff --git a/CmfCoreBundle.php b/CmfCoreBundle.php index cffac093..92c2832a 100644 --- a/CmfCoreBundle.php +++ b/CmfCoreBundle.php @@ -5,6 +5,7 @@ use Symfony\Component\HttpKernel\Bundle\Bundle; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Cmf\Bundle\CoreBundle\DependencyInjection\Compiler\RequestAwarePass; +use Symfony\Cmf\Bundle\CoreBundle\DependencyInjection\Compiler\AddPublishedVotersPass; class CmfCoreBundle extends Bundle { @@ -12,5 +13,6 @@ public function build(ContainerBuilder $container) { parent::build($container); $container->addCompilerPass(new RequestAwarePass()); + $container->addCompilerPass(new AddPublishedVotersPass()); } } diff --git a/DependencyInjection/CmfCoreExtension.php b/DependencyInjection/CmfCoreExtension.php index 4755655b..2f84d544 100644 --- a/DependencyInjection/CmfCoreExtension.php +++ b/DependencyInjection/CmfCoreExtension.php @@ -17,13 +17,27 @@ public function load(array $configs, ContainerBuilder $container) $loader = new XmlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config')); $loader->load('services.xml'); - $container->setParameter($this->getAlias().'.role', $config['role']); $container->setParameter($this->getAlias() . '.document_manager_name', $config['document_manager_name']); - if (!$config['publish_workflow_listener']) { - $container->removeDefinition($this->getAlias() . '.publish_workflow_listener'); + if ($config['publish_workflow']['enabled']) { + $this->loadPublishWorkflow($config['publish_workflow'], $loader, $container); + } + } + + public function loadPublishWorkflow($config, XmlFileLoader $loader, ContainerBuilder $container) + { + $container->setParameter($this->getAlias().'.publish_workflow.view_non_published_role', $config['view_non_published_role']); + $loader->load('publish_workflow.xml'); + + if (!$config['request_listener']) { + $container->removeDefinition($this->getAlias() . '.publish_workflow.request_listener'); } elseif (!class_exists('Symfony\Cmf\Bundle\RoutingBundle\Routing\DynamicRouter')) { - throw new InvalidConfigurationException("The 'publish_workflow_listener' may not be enabled unless 'Symfony\Cmf\Bundle\RoutingBundle\Routing\DynamicRouter' is available."); + throw new InvalidConfigurationException('The "publish_workflow.request_listener" may not be enabled unless "Symfony\Cmf\Bundle\RoutingBundle\Routing\DynamicRouter" is available.'); + } + + if (!class_exists('Sonata\AdminBundle\Admin\AdminExtension')) { + $container->removeDefinition($this->getAlias() . '.admin_extension.publish_workflow.publishable'); + $container->removeDefinition($this->getAlias() . '.admin_extension.publish_workflow.time_period'); } } diff --git a/DependencyInjection/Compiler/AddPublishedVotersPass.php b/DependencyInjection/Compiler/AddPublishedVotersPass.php new file mode 100644 index 00000000..2fb90be5 --- /dev/null +++ b/DependencyInjection/Compiler/AddPublishedVotersPass.php @@ -0,0 +1,40 @@ + + * @author Johannes M. Schmitt + */ +class AddPublishedVotersPass implements CompilerPassInterface +{ + /** + * {@inheritDoc} + */ + public function process(ContainerBuilder $container) + { + if (!$container->hasDefinition('cmf_core.publish_workflow.access_decision_manager')) { + return; + } + + $voters = new \SplPriorityQueue(); + foreach ($container->findTaggedServiceIds('cmf_published_voter') as $id => $attributes) { + $priority = isset($attributes[0]['priority']) ? $attributes[0]['priority'] : 0; + $voters->insert(new Reference($id), $priority); + } + + $voters = iterator_to_array($voters); + ksort($voters); + + $container->getDefinition('cmf_core.publish_workflow.access_decision_manager')->replaceArgument(0, array_values($voters)); + } +} diff --git a/DependencyInjection/Configuration.php b/DependencyInjection/Configuration.php index 27c014f0..30e4d26d 100644 --- a/DependencyInjection/Configuration.php +++ b/DependencyInjection/Configuration.php @@ -15,8 +15,14 @@ public function getConfigTreeBuilder() $rootNode ->children() ->scalarNode('document_manager_name')->defaultValue('default')->end() - ->scalarNode('role')->defaultValue('IS_AUTHENTICATED_ANONYMOUSLY')->end() - ->booleanNode('publish_workflow_listener')->defaultFalse()->end() + ->arrayNode('publish_workflow') + ->addDefaultsIfNotSet() + ->children() + ->booleanNode('enabled')->defaultTrue()->end() + ->scalarNode('view_non_published_role')->defaultValue('ROLE_CAN_VIEW_NON_PUBLISHED')->end() + ->booleanNode('request_listener')->defaultTrue()->end() + ->end() + ->end() ->end() ; diff --git a/EventListener/PublishWorkflowListener.php b/EventListener/PublishWorkflowListener.php index 8b91c60b..878f3c12 100644 --- a/EventListener/PublishWorkflowListener.php +++ b/EventListener/PublishWorkflowListener.php @@ -7,25 +7,47 @@ use Symfony\Component\HttpKernel\Event\GetResponseEvent; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; +use Symfony\Cmf\Bundle\CoreBundle\PublishWorkflow\PublishWorkflowChecker; + use Symfony\Cmf\Bundle\RoutingBundle\Routing\DynamicRouter; -use Symfony\Cmf\Bundle\CoreBundle\PublishWorkflow\PublishWorkflowCheckerInterface; /** - * Makes sure only published routes and content can be accessed + * A request listener that makes sure only published routes and content can be + * accessed. + * + * @author David Buchmann */ class PublishWorkflowListener implements EventSubscriberInterface { /** - * @var PublishWorkflowCheckerInterface + * @var PublishWorkflowChecker */ protected $publishWorkflowChecker; /** - * @param PublishWorkflowCheckerInterface $publishWorkflowChecker + * The attribute to check with the workflow checker, typically VIEW or VIEW_ANONYMOUS + * + * @var string + */ + private $attribute; + + /** + * @param PublishWorkflowChecker $publishWorkflowChecker + * @param string $attribute the attribute name to check */ - public function __construct(PublishWorkflowCheckerInterface $publishWorkflowChecker) + public function __construct(PublishWorkflowChecker $publishWorkflowChecker, $attribute = 'VIEW') { $this->publishWorkflowChecker = $publishWorkflowChecker; + $this->attribute = $attribute; + } + + public function getAttribute() + { + return $this->attribute; + } + public function setAttribute($attribute) + { + $this->attribute = $attribute; } /** @@ -38,12 +60,12 @@ public function onKernelRequest(GetResponseEvent $event) $request = $event->getRequest(); $route = $request->attributes->get(DynamicRouter::ROUTE_KEY); - if ($route && !$this->publishWorkflowChecker->checkIsPublished($route, false, $request)) { + if ($route && !$this->publishWorkflowChecker->isGranted($this->getAttribute(), $route)) { throw new NotFoundHttpException('Route not found at: ' . $request->getPathInfo()); } $content = $request->attributes->get(DynamicRouter::CONTENT_KEY); - if ($content && !$this->publishWorkflowChecker->checkIsPublished($content, false, $request)) { + if ($content && !$this->publishWorkflowChecker->isGranted($this->getAttribute(), $content)) { throw new NotFoundHttpException('Content not found for: ' . $request->getPathInfo()); } } @@ -59,5 +81,4 @@ static public function getSubscribedEvents() KernelEvents::REQUEST => array(array('onKernelRequest', 1)), ); } - } diff --git a/PublishWorkflow/PublishTimePeriodInterface.php b/PublishWorkflow/PublishTimePeriodInterface.php new file mode 100644 index 00000000..76610e0c --- /dev/null +++ b/PublishWorkflow/PublishTimePeriodInterface.php @@ -0,0 +1,32 @@ + +*/ +class PublishWorkflowChecker implements SecurityContextInterface { /** - * @var string the role name for the security check + * This attribute means the user is allowed to see this content, either + * because it is published or because he is granted the bypassingRole. */ - protected $requiredRole; + const VIEW_ATTRIBUTE = 'VIEW'; /** - * @var SecurityContextInterface + * This attribute means the content is available for viewing by anonymous + * users. This can be used where the role based exception from the + * publication check is not wanted. + * + * The bypass role is handled by the workflow checker, the individual + * voters should treat VIEW and VIEW_ANONYMOUS the same. + */ + const VIEW_ANONYMOUS_ATTRIBUTE = 'VIEW_ANONYMOUS'; + + /** + * @var ContainerInterface */ - protected $securityContext; + private $container; /** - * @var \DateTime + * @var bool|string Role allowed to bypass the published check if the + * VIEW attribute is used, or false to never bypass */ - protected $currentTime; + private $bypassingRole; /** - * @param string $requiredRole the role to check with the securityContext - * (if you pass one), defaults to everybody: IS_AUTHENTICATED_ANONYMOUSLY - * @param \Symfony\Component\Security\Core\SecurityContextInterface|null $securityContext - * the security context to use to check for the role. No security - * check if this is null + * @var AccessDecisionManagerInterface */ - public function __construct($requiredRole = "IS_AUTHENTICATED_ANONYMOUSLY", SecurityContextInterface $securityContext = null) + private $accessDecisionManager; + + /** + * @var TokenInterface + */ + private $token; + + /** + * @param ContainerInterface $container to get the security context from. + * We cannot inject the security context directly as this would lead + * to a circular reference. + * @param AccessDecisionManagerInterface $accessDecisionManager + * @param string $bypassingRole A role that is allowed to bypass the + * published check if we ask for the VIEW attribute . + */ + public function __construct(ContainerInterface $container, AccessDecisionManagerInterface $accessDecisionManager, $bypassingRole = false) { - $this->requiredRole = $requiredRole; - $this->securityContext = $securityContext; - $this->currentTime = new \DateTime(); + $this->container = $container; + $this->accessDecisionManager = $accessDecisionManager; + $this->bypassingRole = $bypassingRole; } /** - * Overwrite the current time + * {@inheritDoc} * - * @param \DateTime $currentTime + * Defaults to the token from the default security context, but can be + * overwritten locally. */ - public function setCurrentTime(\DateTime $currentTime) + public function getToken() { - $this->currentTime = $currentTime; + if (null === $this->token) { + $securityContext = $this->container->get('security.context'); + + return $securityContext->getToken(); + } + + return $this->token; } /** - * {inheritDoc} + * {@inheritDoc} */ - public function checkIsPublished($document, $ignoreRole = false) + public function setToken(TokenInterface $token = null) { - if (!$document instanceOf PublishWorkflowInterface) { - return true; - } + $this->token = $token; + } + + /** + * Checks if the access decision manager supports the given class. + * + * @param string $class A class name + * + * @return boolean true if this decision manager can process the class + */ + public function supportsClass($class) + { + return $this->accessDecisionManager->supportsClass($class); + } - if ($this->securityContext && $this->securityContext->isGranted($this->requiredRole)) { - if (!$ignoreRole) { - return true; - } + /** + * {@inheritDoc} + */ + public function isGranted($attributes, $object = null) + { + if (!is_array($attributes)) { + $attributes = array($attributes); } - $startDate = $document->getPublishStartDate(); - $endDate = $document->getPublishEndDate(); - $isPublishable = $document->isPublishable(); + $securityContext = $this->container->get('security.context'); - if (null === $startDate && null === $endDate) { - return $isPublishable !== false; + if (null !== $securityContext->getToken() + && (count($attributes) === 1) + && self::VIEW_ATTRIBUTE === reset($attributes) + && $securityContext->isGranted($this->bypassingRole) + ) { + return true; } - if ((null === $startDate || $this->currentTime >= $startDate) && - (null === $endDate || $this->currentTime < $endDate) - ) { - return $isPublishable !== false; + $token = $this->getToken(); + if (null === $token) { + // not logged in, surely we can not skip the check. + // create a dummy token to check for publication even if no + // firewall is present. + $token = new AnonymousToken('', ''); } - return false; + return $this->accessDecisionManager->decide($token, $attributes, $object); } -} +} \ No newline at end of file diff --git a/PublishWorkflow/PublishWorkflowCheckerInterface.php b/PublishWorkflow/PublishWorkflowCheckerInterface.php deleted file mode 100644 index 23cb4c90..00000000 --- a/PublishWorkflow/PublishWorkflowCheckerInterface.php +++ /dev/null @@ -1,18 +0,0 @@ - + */ +class PublishTimePeriodVoter implements VoterInterface +{ + /** + * @var \DateTime + */ + protected $currentTime; + + public function __construct() + { + // we create the timestamp on instantiation to avoid glitches due to + // the time passing during the request + $this->currentTime = new \DateTime(); + } + + /** + * Overwrite the current time. + * + * @param \DateTime $currentTime + */ + public function setCurrentTime(\DateTime $currentTime) + { + $this->currentTime = $currentTime; + } + + /** + * {@inheritdoc} + */ + public function supportsAttribute($attribute) + { + return PublishWorkflowChecker::VIEW_ATTRIBUTE === $attribute + || PublishWorkflowChecker::VIEW_ANONYMOUS_ATTRIBUTE === $attribute + ; + } + + /** + * {@inheritdoc} + */ + public function supportsClass($class) + { + return is_subclass_of($class, 'Symfony\Cmf\Bundle\CoreBundle\PublishWorkflow\PublishTimePeriodInterface'); + } + + /** + * {@inheritdoc} + * + * @param PublishTimePeriodInterface $object + */ + public function vote(TokenInterface $token, $object, array $attributes) + { + if (!$this->supportsClass(get_class($object))) { + return self::ACCESS_ABSTAIN; + } + + $startDate = $object->getPublishStartDate(); + $endDate = $object->getPublishEndDate(); + + $decision = self::ACCESS_GRANTED; + foreach($attributes as $attribute) { + if (! $this->supportsAttribute($attribute)) { + // there was an unsupported attribute in the request. + // now we only abstain or deny if we find a supported attribute + // and the content is not publishable + $decision = self::ACCESS_ABSTAIN; + continue; + } + + if ((null !== $startDate && $this->currentTime < $startDate) || + (null !== $endDate && $this->currentTime > $endDate) + ) { + return self::ACCESS_DENIED; + } + } + + return $decision; + } +} diff --git a/PublishWorkflow/Voter/PublishableVoter.php b/PublishWorkflow/Voter/PublishableVoter.php new file mode 100644 index 00000000..44226d68 --- /dev/null +++ b/PublishWorkflow/Voter/PublishableVoter.php @@ -0,0 +1,64 @@ + + */ +class PublishableVoter implements VoterInterface +{ + /** + * {@inheritdoc} + */ + public function supportsAttribute($attribute) + { + return PublishWorkflowChecker::VIEW_ATTRIBUTE === $attribute + || PublishWorkflowChecker::VIEW_ANONYMOUS_ATTRIBUTE === $attribute + ; + } + + /** + * {@inheritdoc} + */ + public function supportsClass($class) + { + return is_subclass_of($class, 'Symfony\Cmf\Bundle\CoreBundle\PublishWorkflow\PublishableInterface'); + } + + /** + * {@inheritdoc} + * + * @param PublishableInterface $object + */ + public function vote(TokenInterface $token, $object, array $attributes) + { + if (!$this->supportsClass(get_class($object))) { + return self::ACCESS_ABSTAIN; + } + + $decision = self::ACCESS_GRANTED; + foreach($attributes as $attribute) { + if (! $this->supportsAttribute($attribute)) { + // there was an unsupported attribute in the request. + // now we only abstain or deny if we find a supported attribute + // and the content is not publishable + $decision = self::ACCESS_ABSTAIN; + continue; + } + if (! $object->isPublishable()) { + return self::ACCESS_DENIED; + } + } + + return $decision; + } +} \ No newline at end of file diff --git a/Resources/config/publish_workflow.xml b/Resources/config/publish_workflow.xml new file mode 100644 index 00000000..66d945ff --- /dev/null +++ b/Resources/config/publish_workflow.xml @@ -0,0 +1,63 @@ + + + + + + Symfony\Cmf\Bundle\CoreBundle\PublishWorkflow\PublishWorkflowChecker + Symfony\Component\Security\Core\Authorization\AccessDecisionManager + Symfony\Cmf\Bundle\CoreBundle\PublishWorkflow\Voter\PublishableVoter + Symfony\Cmf\Bundle\CoreBundle\PublishWorkflow\Voter\PublishTimePeriodVoter + Symfony\Cmf\Bundle\CoreBundle\EventListener\PublishWorkflowListener + Symfony\Cmf\Bundle\CoreBundle\Security\Authorization\Voter\PublishedVoter + Symfony\Cmf\Bundle\CoreBundle\Admin\Extension\PublishableExtension + Symfony\Cmf\Bundle\CoreBundle\Admin\Extension\PublishTimePeriodExtension + + + + + + + unanimous + true + + + + + + %cmf_core.publish_workflow.view_non_published_role% + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Resources/config/schema/core-1.0.xsd b/Resources/config/schema/core-1.0.xsd index 3d6c6d26..4ad47259 100644 --- a/Resources/config/schema/core-1.0.xsd +++ b/Resources/config/schema/core-1.0.xsd @@ -8,11 +8,18 @@ + + + - - - + + + + + + + diff --git a/Resources/config/services.xml b/Resources/config/services.xml index 6d000d3f..78be50f3 100644 --- a/Resources/config/services.xml +++ b/Resources/config/services.xml @@ -6,38 +6,26 @@ Symfony\Cmf\Bundle\CoreBundle\Twig\Extension\CmfExtension - Symfony\Cmf\Bundle\CoreBundle\PublishWorkflow\PublishWorkflowChecker - Symfony\Cmf\Bundle\CoreBundle\EventListener\PublishWorkflowListener + Symfony\Cmf\Bundle\CoreBundle\Templating\Helper\CmfHelper Symfony\Cmf\Bundle\CoreBundle\EventListener\RequestAwareListener - Symfony\Cmf\Bundle\CoreBundle\Admin\Extension\PublishWorkflowExtension - - - %cmf_core.document_manager_name% + - - %cmf_core.role% - - - - - - + + + + %cmf_core.document_manager_name% - - - - diff --git a/Security/Authorization/Voter/PublishedVoter.php b/Security/Authorization/Voter/PublishedVoter.php new file mode 100644 index 00000000..facaf064 --- /dev/null +++ b/Security/Authorization/Voter/PublishedVoter.php @@ -0,0 +1,72 @@ + + */ +class PublishedVoter implements VoterInterface +{ + /** + * @var PublishWorkflowChecker + */ + private $publishWorkflowChecker; + + /** + * @param PublishWorkflowChecker $publishWorkflowChecker + */ + public function __construct(PublishWorkflowChecker $publishWorkflowChecker) + { + $this->publishWorkflowChecker = $publishWorkflowChecker; + } + + /** + * {@inheritdoc} + */ + public function supportsAttribute($attribute) + { + return PublishWorkflowChecker::VIEW_ATTRIBUTE === $attribute + || PublishWorkflowChecker::VIEW_ANONYMOUS_ATTRIBUTE === $attribute + ; + } + + /** + * {@inheritdoc} + */ + public function supportsClass($class) + { + return $this->publishWorkflowChecker->supportsClass($class); + } + + /** + * {@inheritdoc} + * + * @param object $object + */ + public function vote(TokenInterface $token, $object, array $attributes) + { + if (!$this->supportsClass(get_class($object))) { + return self::ACCESS_ABSTAIN; + } + foreach($attributes as $attribute) { + if (! $this->supportsAttribute($attribute)) { + return self::ACCESS_ABSTAIN; + } + } + + if ($this->publishWorkflowChecker->isGranted($attributes, $object)) { + return self::ACCESS_GRANTED; + } + + return self::ACCESS_DENIED; + } +} \ No newline at end of file diff --git a/Templating/Helper/CmfHelper.php b/Templating/Helper/CmfHelper.php index d7d9810f..d6ab599c 100644 --- a/Templating/Helper/CmfHelper.php +++ b/Templating/Helper/CmfHelper.php @@ -2,14 +2,16 @@ namespace Symfony\Cmf\Bundle\CoreBundle\Templating\Helper; -use Symfony\Cmf\Bundle\CoreBundle\PublishWorkflow\PublishWorkflowCheckerInterface; +use PHPCR\Util\PathHelper; use Symfony\Component\Templating\Helper\Helper; use Doctrine\Common\Persistence\ManagerRegistry; use Doctrine\ODM\PHPCR\Exception\MissingTranslationException; use Doctrine\ODM\PHPCR\DocumentManager; -use PHPCR\Util\PathHelper; +use Symfony\Cmf\Bundle\CoreBundle\PublishWorkflow\PublishWorkflowChecker; +use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException; +use Symfony\Component\Security\Core\SecurityContextInterface; /** * Provides CMF helper functions. @@ -24,18 +26,18 @@ class CmfHelper extends Helper protected $dm; /** - * @var PublishWorkflowCheckerInterface + * @var SecurityContextInterface */ protected $publishWorkflowChecker; /** * Instantiates the content controller. * - * @param PublishWorkflowCheckerInterface $publishWorkflowChecker + * @param SecurityContextInterface $publishWorkflowChecker * @param ManagerRegistry $registry * @param string $objectManagerName */ - public function __construct(PublishWorkflowCheckerInterface $publishWorkflowChecker, $registry = null, $objectManagerName = null) + public function __construct(SecurityContextInterface $publishWorkflowChecker = null, $registry = null, $objectManagerName = null) { $this->publishWorkflowChecker = $publishWorkflowChecker; @@ -44,6 +46,15 @@ public function __construct(PublishWorkflowCheckerInterface $publishWorkflowChec } } + protected function getDm() + { + if (!$this->dm) { + throw new \RuntimeException('Document Manager has not been initialized yet.'); + } + + return $this->dm; + } + /** * Gets the helper name. * @@ -79,7 +90,7 @@ public function getParentPath($document) public function getPath($document) { try { - return $this->dm->getUnitOfWork()->getDocumentId($document); + return $this->getDm()->getUnitOfWork()->getDocumentId($document); } catch (\Exception $e) { return false; } @@ -93,26 +104,33 @@ public function getPath($document) */ public function find($path) { - return $this->dm->find(null, $path); + return $this->getDm()->find(null, $path); } /** * Gets a document instance and validate if its eligible. * - * @param string|object $document the id of a document or the document object itself - * @param Boolean|null $ignoreRole if the role should be ignored or null if publish workflow should be ignored - * @param null|string $class class name to filter on + * @param string|object $document the id of a document or the document + * object itself + * @param boolean|null $ignoreRole whether the bypass role should be + * ignored (leading to only show published content regardless of the + * current user) or null to skip the published check completely. + * @param null|string $class class name to filter on * * @return null|object */ private function getDocument($document, $ignoreRole = false, $class = null) { if (is_string($document)) { - $document = $this->dm->find(null, $document); + $document = $this->getDm()->find(null, $document); + } + if (null !== $ignoreRole && null === $this->publishWorkflowChecker) { + throw new InvalidConfigurationException('You can not fetch only published documents when the publishWorkflowChecker is not set. Either enable the publish workflow or pass "ignoreRole = null" to skip publication checks.'); } if (empty($document) - || (null !== $ignoreRole && !$this->publishWorkflowChecker->checkIsPublished($document, $ignoreRole)) + || (false === $ignoreRole && !$this->publishWorkflowChecker->isGranted(PublishWorkflowChecker::VIEW_ATTRIBUTE, $document)) + || (true === $ignoreRole && !$this->publishWorkflowChecker->isGranted(PublishWorkflowChecker::VIEW_ANONYMOUS_ATTRIBUTE, $document)) || (null != $class && !($document instanceof $class)) ) { return null; @@ -156,19 +174,26 @@ public function findMany($paths = array(), $limit = false, $offset = false, $ign } /** - * Checks if a document is published. + * Check if a document is published, regardless of the current users role. * - * @param string $document + * If you need the bypass role, you will have a firewall configured and can + * simply use {{ is_granted('VIEW', document) }} * - * @return Boolean + * @param object $document + * + * @return boolean */ public function isPublished($document) { + if (null === $this->publishWorkflowChecker) { + throw new InvalidConfigurationException('You can not check for publication as the publish workflow is not enabled.'); + } + if (empty($document)) { return false; } - return $this->publishWorkflowChecker->checkIsPublished($document, true); + return $this->publishWorkflowChecker->isGranted(PublishWorkflowChecker::VIEW_ANONYMOUS_ATTRIBUTE, $document); } /** @@ -181,7 +206,7 @@ public function isPublished($document) public function getLocalesFor($document, $includeFallbacks = false) { if (is_string($document)) { - $document = $this->dm->find(null, $document); + $document = $this->getDm()->find(null, $document); } if (empty($document)) { @@ -189,7 +214,7 @@ public function getLocalesFor($document, $includeFallbacks = false) } try { - $locales = $this->dm->getLocalesFor($document, $includeFallbacks); + $locales = $this->getDm()->getLocalesFor($document, $includeFallbacks); } catch (MissingTranslationException $e) { $locales = array(); } @@ -207,13 +232,13 @@ public function getChild($parent, $name) { if (is_object($parent)) { try { - $parent = $this->dm->getUnitOfWork()->getDocumentId($parent); + $parent = $this->getDm()->getUnitOfWork()->getDocumentId($parent); } catch (\Exception $e) { return false; } } - return $this->dm->find(null, "$parent/$name"); + return $this->getDm()->find(null, "$parent/$name"); } /** @@ -236,9 +261,9 @@ public function getChildren($parent, $limit = false, $offset = false, $filter = if ($limit || $offset) { if (is_object($parent)) { - $parent = $this->dm->getUnitOfWork()->getDocumentId($parent); + $parent = $this->getDm()->getUnitOfWork()->getDocumentId($parent); } - $node = $this->dm->getPhpcrSession()->getNode($parent); + $node = $this->getDm()->getPhpcrSession()->getNode($parent); $children = (array) $node->getNodeNames(); foreach ($children as $key => $child) { // filter before fetching data already to save some traffic @@ -255,7 +280,7 @@ public function getChildren($parent, $limit = false, $offset = false, $filter = $children = array_slice($children, $key); } } else { - $children = $this->dm->getChildren($parent, $filter); + $children = $this->getDm()->getChildren($parent, $filter); } $result = array(); @@ -314,7 +339,7 @@ private function getChildrenPaths($path, array &$children, $depth) --$depth; - $node = $this->dm->getPhpcrSession()->getNode($path); + $node = $this->getDm()->getPhpcrSession()->getNode($path); $names = (array) $node->getNodeNames(); foreach ($names as $name) { if (strpos($name, 'phpcr_locale:') === 0) { @@ -340,7 +365,7 @@ public function getDescendants($parent, $depth = null) $children = array(); if (is_object($parent)) { - $parent = $this->dm->getUnitOfWork()->getDocumentId($parent); + $parent = $this->getDm()->getUnitOfWork()->getDocumentId($parent); } $this->getChildrenPaths($parent, $children, $depth); @@ -350,38 +375,28 @@ public function getDescendants($parent, $depth = null) /** * Check children for a possible following document * - * @param \Traversable $childNames - * @param Boolean $reverse - * @param string $parentPath + * @param array $childNames + * @param string $path * @param Boolean $ignoreRole * @param null|string $class - * @param null|string $nodeName * * @return null|object */ - private function checkChildren($childNames, $reverse, $parentPath, $ignoreRole = false, $class = null, $nodeName = null) + private function checkChildren(array $childNames, $path, $ignoreRole = false, $class = null) { - if ($reverse) { - $childNames = array_reverse($childNames->getArrayCopy()); - } - - $check = empty($nodeName); foreach ($childNames as $name) { if (strpos($name, 'phpcr_locale:') === 0) { continue; } - if ($check) { - try { - $child = $this->getDocument("$parentPath/$name", $ignoreRole, $class); - if ($child) { - return $child; - } - } catch (MissingTranslationException $e) { - continue; - } - } elseif ($nodeName == $name) { - $check = true; + try { + $child = $this->getDocument(ltrim($path, '/')."/$name", $ignoreRole, $class); + } catch (MissingTranslationException $e) { + continue; + } + + if ($child) { + return $child; } } @@ -389,130 +404,288 @@ private function checkChildren($childNames, $reverse, $parentPath, $ignoreRole = } /** - * Search for a following document + * Traverse the depth to find previous documents * - * @param string|object $path document instance or path - * @param string|object $anchor document instance or path - * @param null|integer $depth - * @param Boolean $reverse - * @param Boolean $ignoreRole - * @param null|string $class + * @param null|integer $depth + * @param integer $anchorDepth + * @param array $childNames + * @param string $path + * @param Boolean $ignoreRole + * @param null|string $class * * @return null|object */ - private function search($path, $anchor = null, $depth = null, $reverse = false, $ignoreRole = false, $class = null) + private function traversePrevDepth($depth, $anchorDepth, array $childNames, $path, $ignoreRole, $class) { - if (empty($path)) { - return null; + foreach ($childNames as $childName) { + $childPath = "$path/$childName"; + $node = $this->getDm()->getPhpcrSession()->getNode($childPath); + if (null === $depth || PathHelper::getPathDepth($childPath) - $anchorDepth < $depth) { + $childNames = $node->getNodeNames()->getArrayCopy(); + if (!empty($childNames)) { + $childNames = array_reverse($childNames); + $result = $this->traversePrevDepth($depth, $anchorDepth, $childNames, $childPath, $ignoreRole, $class); + if ($result) { + return $result; + } + } + } + + $result = $this->checkChildren($childNames, $node->getPath(), $ignoreRole, $class); + if ($result) { + return $result; + } } + } + /** + * Search for a previous document + * + * @param string|object $path document instance or path from which to search + * @param string|object $anchor document instance or path which serves as an anchor from which to flatten the hierarchy + * @param null|integer $depth depth up to which to traverse down the tree when an anchor is provided + * @param Boolean $ignoreRole if to ignore the role + * @param null|string $class the class to filter by + * + * @return null|object + */ + private function searchDepthPrev($path, $anchor, $depth = null, $ignoreRole = false, $class = null) + { if (is_object($path)) { - $path = $this->dm->getUnitOfWork()->getDocumentId($path); + $path = $this->getDm()->getUnitOfWork()->getDocumentId($path); } - $node = $this->dm->getPhpcrSession()->getNode($path); + if (null === $path || '/' === $path) { + return null; + } - if ($anchor) { - if (is_object($anchor)) { - $anchor = $this->dm->getUnitOfWork()->getDocumentId($anchor); - } + $node = $this->getDm()->getPhpcrSession()->getNode($path); + + if (is_object($anchor)) { + $anchor = $this->getDm()->getUnitOfWork()->getDocumentId($anchor); + } + + if (0 !== strpos($path, $anchor)) { + throw new \RuntimeException("The anchor path '$anchor' is not a parent of the current path '$path'."); + } + + if ($path === $anchor) { + return null; + } + + $parent = $node->getParent(); + $parentPath = $parent->getPath(); + + $childNames = $parent->getNodeNames()->getArrayCopy(); + if (!empty($childNames)) { + $childNames = array_reverse($childNames); + $key = array_search($node->getName(), $childNames); + $childNames = array_slice($childNames, $key + 1); + } + + // traverse the previous siblings down the tree + $result = $this->traversePrevDepth($depth, PathHelper::getPathDepth($anchor), $childNames, $parentPath, $ignoreRole, $class); + if ($result) { + return $result; + } + + // check siblings + $result = $this->checkChildren($childNames, $parentPath, $ignoreRole, $class); + if ($result) { + return $result; + } - if (strpos($path, $anchor) !== 0) { - throw new \RuntimeException("The anchor path '$anchor' is not a parent of the current path '$path'."); + // check parents + // TODO do we need to traverse towards the anchor? + if (0 === strpos($parentPath, $anchor)) { + $parent = $parent->getParent(); + $childNames = $parent->getNodeNames()->getArrayCopy(); + $key = array_search(PathHelper::getNodeName($parentPath), $childNames); + $childNames = array_slice($childNames, 0, $key + 1); + $childNames = array_reverse($childNames); + $result = $this->checkChildren($childNames, $parent->getPath(), $ignoreRole, $class); + if ($result) { + return $result; } + } - if (!$reverse - && (null === $depth || PathHelper::getPathDepth($path() - PathHelper::getPathDepth($anchor)) < $depth) - ) { - $childNames = $node->getNodeNames(); - if ($childNames->count()) { - $result = $this->checkChildren($childNames, $reverse, $path, $ignoreRole, $class); - if ($result) { - return $result; - } - } + return null; + } + + /** + * Search for a next document + * + * @param string|object $path document instance or path from which to search + * @param string|object $anchor document instance or path which serves as an anchor from which to flatten the hierarchy + * @param null|integer $depth depth up to which to traverse down the tree when an anchor is provided + * @param Boolean $ignoreRole if to ignore the role + * @param null|string $class the class to filter by + * + * @return null|object + */ + private function searchDepthNext($path, $anchor, $depth = null, $ignoreRole = false, $class = null) + { + if (is_object($path)) { + $path = $this->getDm()->getUnitOfWork()->getDocumentId($path); + } + + if (null === $path || '/' === $path) { + return null; + } + + $node = $this->getDm()->getPhpcrSession()->getNode($path); + + if (is_object($anchor)) { + $anchor = $this->getDm()->getUnitOfWork()->getDocumentId($anchor); + } + + if (0 !== strpos($path, $anchor)) { + throw new \RuntimeException("The anchor path '$anchor' is not a parent of the current path '$path'."); + } + + // take the first eligible child if there are any + // TODO do we need to traverse away from the anchor up to the depth here? + if (null === $depth || PathHelper::getPathDepth($path) - PathHelper::getPathDepth($anchor) < $depth) { + $childNames = $node->getNodeNames()->getArrayCopy(); + $result = $this->checkChildren($childNames, $path, $ignoreRole, $class); + if ($result) { + return $result; } } - $nodename = $node->getName(); + $parent = $node->getParent(); + $parentPath = PathHelper::getParentPath($path); - do { - $parentNode = $node->getParent(); - $childNames = $parentNode->getNodeNames(); - $result = $this->checkChildren($childNames, $reverse, $parentNode->getPath(), $ignoreRole, $class, $nodename); - if ($result || !$anchor) { + // take the first eligible sibling + if (0 === strpos($parentPath, $anchor)) { + $childNames = $parent->getNodeNames()->getArrayCopy(); + $key = array_search($node->getName(), $childNames); + $childNames = array_slice($childNames, $key + 1); + $result = $this->checkChildren($childNames, $parentPath, $ignoreRole, $class); + if ($result) { return $result; } + } - $node = $parentNode; - if ($nodename) { - $reverse = !$reverse; - $nodename = null; + // take the first eligible parent, traverse up + while ('/' !== $parentPath) { + $parent = $parent->getParent(); + if (false === strpos($parent->getPath(), $anchor)) { + return null; } - } while (!$anchor || $anchor !== $node->getPath()); + + $childNames = $parent->getNodeNames()->getArrayCopy(); + $key = array_search(PathHelper::getNodeName($parentPath), $childNames); + $childNames = array_slice($childNames, $key + 1); + $parentPath = $parent->getPath(); + $result = $this->checkChildren($childNames, $parentPath, $ignoreRole, $class); + if ($result) { + return $result; + } + } return null; } + /** + * Search for a following document + * + * @param string|object $path document instance or path from which to search + * @param Boolean $reverse if to traverse back + * @param Boolean $ignoreRole if to ignore the role + * @param null|string $class the class to filter by + * + * @return null|object + */ + private function search($path, $reverse = false, $ignoreRole = false, $class = null) + { + if (is_object($path)) { + $path = $this->getDm()->getUnitOfWork()->getDocumentId($path); + } + + if (null === $path || '/' === $path) { + return null; + } + + $node = $this->getDm()->getPhpcrSession()->getNode($path); + $parentNode = $node->getParent(); + $childNames = $parentNode->getNodeNames()->getArrayCopy(); + if ($reverse) { + $childNames = array_reverse($childNames); + } + + $key = array_search($node->getName(), $childNames); + $childNames = array_slice($childNames, $key + 1); + return $this->checkChildren($childNames, $parentNode->getPath(), $ignoreRole, $class); + } + /** * Gets the previous document. * - * @param string|object $current document instance or path - * @param string|object $parent document instance or path - * @param null|integer $depth - * @param Boolean $ignoreRole - * @param null|string $class + * @param string|object $path document instance or path from which to search + * @param null|string|object $anchor document instance or path which serves as an anchor from which to flatten the hierarchy + * @param null|integer $depth depth up to which to traverse down the tree when an anchor is provided + * @param Boolean $ignoreRole if to ignore the role + * @param null|string $class the class to filter by * * @return null|object */ - public function getPrev($current, $parent = null, $depth = null, $ignoreRole = false, $class = null) + public function getPrev($current, $anchor = null, $depth = null, $ignoreRole = false, $class = null) { - return $this->search($current, $parent, $depth, true, $ignoreRole, $class); + if ($anchor) { + return $this->searchDepthPrev($current, $anchor, $depth, true, $ignoreRole, $class); + } + + return $this->search($current, true, $ignoreRole, $class); } /** * Gets the next document. * - * @param string|object $current document instance or path - * @param string|object $parent document instance or path - * @param null|integer $depth - * @param Boolean $ignoreRole - * @param null|string $class + * @param string|object $path document instance or path from which to search + * @param null|string|object $anchor document instance or path which serves as an anchor from which to flatten the hierarchy + * @param null|integer $depth depth up to which to traverse down the tree when an anchor is provided + * @param Boolean $ignoreRole if to ignore the role + * @param null|string $class the class to filter by * * @return null|object */ - public function getNext($current, $parent = null, $depth = null, $ignoreRole = false, $class = null) + public function getNext($current, $anchor = null, $depth = null, $ignoreRole = false, $class = null) { - return $this->search($current, $parent, $depth, false, $ignoreRole, $class); + if ($anchor) { + return $this->searchDepthNext($current, $anchor, $depth, $ignoreRole, $class); + } + + return $this->search($current, false, $ignoreRole, $class); } /** * Gets the previous linkable document. * - * @param string|object $current document instance or path - * @param string|object $parent document instance or path - * @param null|integer $depth - * @param Boolean $ignoreRole - * + * @param string|object $path document instance or path from which to search + * @param null|string|object $anchor document instance or path which serves as an anchor from which to flatten the hierarchy + * @param null|integer $depth depth up to which to traverse down the tree when an anchor is provided + * @param Boolean $ignoreRole if to ignore the role + * * @return null|object */ - public function getPrevLinkable($current, $parent = null, $depth = null, $ignoreRole = false) + public function getPrevLinkable($current, $anchor = null, $depth = null, $ignoreRole = false) { - return $this->search($current, $parent, $depth, true, $ignoreRole, 'Symfony\Cmf\Component\Routing\RouteAwareInterface'); + return $this->getPrev($current, $anchor, $depth, $ignoreRole, 'Symfony\Cmf\Component\Routing\RouteAwareInterface'); } /** * Gets the next linkable document. * - * @param string|object $current document instance or path - * @param string|object $parent document instance or path - * @param null|integer $depth - * @param Boolean $ignoreRole + * @param string|object $path document instance or path from which to search + * @param null|string|object $anchor document instance or path which serves as an anchor from which to flatten the hierarchy + * @param null|integer $depth depth up to which to traverse down the tree when an anchor is provided + * @param Boolean $ignoreRole if to ignore the role * * @return null|object */ - public function getNextLinkable($current, $parent = null, $depth = null, $ignoreRole = false) + public function getNextLinkable($current, $anchor = null, $depth = null, $ignoreRole = false) { - return $this->search($current, $parent, $depth, false, $ignoreRole, 'Symfony\Cmf\Component\Routing\RouteAwareInterface'); + return $this->getNext($current, $anchor, $depth, $ignoreRole, 'Symfony\Cmf\Component\Routing\RouteAwareInterface'); } } diff --git a/Tests/Functional/PublishWorkflow/PublishWorkflowTest.php b/Tests/Functional/PublishWorkflow/PublishWorkflowTest.php new file mode 100644 index 00000000..e3801267 --- /dev/null +++ b/Tests/Functional/PublishWorkflow/PublishWorkflowTest.php @@ -0,0 +1,104 @@ +pwc = $this->getContainer()->get('cmf_core.publish_workflow.checker'); + } + + public function testPublishable() + { + $doc = $this->getMock('Symfony\Cmf\Bundle\CoreBundle\PublishWorkflow\PublishableInterface'); + $doc->expects($this->any()) + ->method('isPublishable') + ->will($this->returnValue(true)) + ; + + $this->assertTrue($this->pwc->isGranted(PublishWorkflowChecker::VIEW_ATTRIBUTE, $doc)); + $this->assertTrue($this->pwc->isGranted(PublishWorkflowChecker::VIEW_ANONYMOUS_ATTRIBUTE, $doc)); + } + + public function testPublishPeriod() + { + $doc = $this->getMock('Symfony\Cmf\Bundle\CoreBundle\Tests\Functional\PublishWorkflow\PublishModel'); + $doc->expects($this->any()) + ->method('isPublishable') + ->will($this->returnValue(true)) + ; + $doc->expects($this->any()) + ->method('getPublishEndDate') + ->will($this->returnValue(new \DateTime('01/01/1980'))) + ; + + $this->assertFalse($this->pwc->isGranted(PublishWorkflowChecker::VIEW_ATTRIBUTE, $doc)); + $this->assertFalse($this->pwc->isGranted(PublishWorkflowChecker::VIEW_ANONYMOUS_ATTRIBUTE, $doc)); + } + + public function testIgnoreRoleHas() + { + $doc = $this->getMock('Symfony\Cmf\Bundle\CoreBundle\Tests\Functional\PublishWorkflow\PublishModel'); + $doc->expects($this->any()) + ->method('isPublishable') + ->will($this->returnValue(false)) + ; + $roles = array( + new Role('ROLE_CAN_VIEW_NON_PUBLISHED') + ); + $token = new UsernamePasswordToken('test', 'pass', 'testprovider', $roles); + $context = $this->getContainer()->get('security.context'); + $context->setToken($token); + + $this->assertTrue($this->pwc->isGranted(PublishWorkflowChecker::VIEW_ATTRIBUTE, $doc)); + $this->assertFalse($this->pwc->isGranted(PublishWorkflowChecker::VIEW_ANONYMOUS_ATTRIBUTE, $doc)); + } + + public function testIgnoreRoleNotHas() + { + $doc = $this->getMock('Symfony\Cmf\Bundle\CoreBundle\Tests\Functional\PublishWorkflow\PublishModel'); + $doc->expects($this->any()) + ->method('isPublishable') + ->will($this->returnValue(false)) + ; + $roles = array( + new Role('OTHER_ROLE') + ); + $token = new UsernamePasswordToken('test', 'pass', 'testprovider', $roles); + /** @var $context SecurityContext */ + $context = $this->getContainer()->get('security.context'); + $context->setToken($token); + + $this->assertFalse($this->pwc->isGranted(PublishWorkflowChecker::VIEW_ATTRIBUTE, $doc)); + $this->assertFalse($this->pwc->isGranted(PublishWorkflowChecker::VIEW_ANONYMOUS_ATTRIBUTE, $doc)); + } +} + +abstract class PublishModel implements PublishableInterface, PublishTimePeriodInterface {} \ No newline at end of file diff --git a/Tests/Functional/Templating/Helper/CmfHelperHierarchyTest.php b/Tests/Functional/Templating/Helper/CmfHelperHierarchyTest.php new file mode 100644 index 00000000..238e459f --- /dev/null +++ b/Tests/Functional/Templating/Helper/CmfHelperHierarchyTest.php @@ -0,0 +1,212 @@ +getContainer(); + $managerRegistry = $container->get('doctrine_phpcr'); + /** @var $session SessionInterface */ + $session = $managerRegistry->getConnection(); + $root = $session->getRootNode(); + if ($root->hasNode('a')) { + $session->removeItem('/a'); + } + + /* + * /a + * /a/b + * /a/b/c + * /a/b/d + * /a/b/e + * /a/f + * /a/f/g + * /a/f/g/h + * /a/i + */ + $a = $root->addNode('a'); + $b = $a->addNode('b'); + $c = $b->addNode('c'); + $c->addMixin('phpcr:managed'); + $c->setProperty('phpcr:class', 'Symfony\Cmf\Bundle\CoreBundle\Tests\Resources\Document\RouteAware'); + $b->addNode('d'); + $e = $b->addNode('e'); + $e->addMixin('phpcr:managed'); + $e->setProperty('phpcr:class', 'Symfony\Cmf\Bundle\CoreBundle\Tests\Resources\Document\RouteAware'); + $f = $a->addNode('f'); + $g = $f->addNode('g'); + $g->addNode('h'); + $a->addNode('i'); + + $session->save(); + + $this->pwc = $this->getMock('Symfony\Component\Security\Core\SecurityContextInterface'); + $this->pwc->expects($this->any()) + ->method('isGranted') + ->will($this->returnValue(true)) + ; + + $this->extension = new CmfHelper($this->pwc, $managerRegistry, 'default'); + } + + public function testGetDescendants() + { + $this->assertEquals(array(), $this->extension->getDescendants(null)); + + $expected = array('/a/b', '/a/b/c', '/a/b/d', '/a/b/e', '/a/f', '/a/f/g', '/a/f/g/h', '/a/i'); + $this->assertEquals($expected, $this->extension->getDescendants('/a')); + + $expected = array('/a/b', '/a/f', '/a/i'); + $this->assertEquals($expected, $this->extension->getDescendants('/a', 1)); + } + + /** + * @dataProvider getPrevData + */ + public function testGetPrev($expected, $path, $anchor = null, $depth = null, $class = 'Doctrine\ODM\PHPCR\Document\Generic') + { + $prev = $this->extension->getPrev($path, $anchor, $depth); + if (null === $expected) { + $this->assertNull($prev); + } else { + $this->assertInstanceOf($class, $prev); + $this->assertEquals($expected, $prev->getId()); + } + } + + public static function getPrevData() + { + return array( + array(null, null), + array(null, '/a'), + array(null, '/a/b'), + array(null, '/a/b/c'), + array('/a/b/c', '/a/b/d', null, null, 'Symfony\Cmf\Bundle\CoreBundle\Tests\Resources\Document\RouteAware'), + array('/a/b/d', '/a/b/e'), + array('/a/b', '/a/f'), + array(null, '/a/f/g'), + array(null, '/a/f/g/h'), + array(null, '/a', '/a'), + array('/a', '/a/b', '/a'), + array('/a/b', '/a/b/c', '/a'), + array('/a/b/c', '/a/b/d', '/a', null, 'Symfony\Cmf\Bundle\CoreBundle\Tests\Resources\Document\RouteAware'), + array('/a/b/d', '/a/b/e', '/a'), + array('/a/b/e', '/a/f', '/a', null, 'Symfony\Cmf\Bundle\CoreBundle\Tests\Resources\Document\RouteAware'), + array('/a/f', '/a/f/g', '/a'), + array('/a/f/g', '/a/f/g/h', '/a'), + array('/a/f/g/h', '/a/i', '/a'), + array('/a/f/g', '/a/i', '/a', 2), + ); + } + + /** + * @dataProvider getNextData + */ + public function testGetNext($expected, $path, $anchor = null, $depth = null, $class = 'Doctrine\ODM\PHPCR\Document\Generic') + { + $next = $this->extension->getNext($path, $anchor, $depth); + if (null === $expected) { + $this->assertNull($next); + } else { + $this->assertInstanceOf($class, $next); + $this->assertEquals($expected, $next->getId()); + } + } + + public static function getNextData() + { + return array( + array(null, null), + array(null, '/a'), + array('/a/f', '/a/b'), + array('/a/b/d', '/a/b/c'), + array('/a/b/e', '/a/b/d', null, null, 'Symfony\Cmf\Bundle\CoreBundle\Tests\Resources\Document\RouteAware'), + array(null, '/a/b/e'), + array('/a/i', '/a/f'), + array(null, '/a/f/g'), + array(null, '/a/f/g/h'), + array('/a/b', '/a', '/a'), + array('/a/b/c', '/a/b', '/a', null, 'Symfony\Cmf\Bundle\CoreBundle\Tests\Resources\Document\RouteAware'), + array('/a/b/d', '/a/b/c', '/a'), + array('/a/b/e', '/a/b/d', '/a', null, 'Symfony\Cmf\Bundle\CoreBundle\Tests\Resources\Document\RouteAware'), + array('/a/f', '/a/b/e', '/a'), + array('/a/f/g', '/a/f', '/a'), + array('/a/f/g/h', '/a/f/g', '/a'), + array('/a/i', '/a/f/g/h', '/a'), + array(null, '/a/i', '/a'), + array(null, '/a/b/e', '/a/b'), + array('/a/i', '/a/f/g', '/a', 2), + ); + } + + /** + * @dataProvider getPrevLinkableData + */ + public function testGetPrevLinkable($expected, $path, $anchor = null, $depth = null) + { + $prev = $this->extension->getPrevLinkable($path, $anchor, $depth); + if (null === $expected) { + $this->assertNull($prev); + } else { + $this->assertInstanceOf('Symfony\Cmf\Bundle\CoreBundle\Tests\Resources\Document\RouteAware', $prev); + $this->assertEquals($expected, $prev->getId()); + } + } + + public static function getPrevLinkableData() + { + // TODO: expand test case + return array( + array(null, null), + array(null, '/a/b/c'), + array('/a/b/c', '/a/b/d'), + array('/a/b/c', '/a/b/e'), + ); + } + + /** + * @dataProvider getNextLinkableData + */ + public function testGetNextLinkable($expected, $path, $anchor = null, $depth = null) + { + $next = $this->extension->getNextLinkable($path, $anchor, $depth); + if (null === $expected) { + $this->assertNull($next); + } else { + $this->assertInstanceOf('Symfony\Cmf\Bundle\CoreBundle\Tests\Resources\Document\RouteAware', $next); + $this->assertEquals($expected, $next->getId()); + } + } + + public static function getNextLinkableData() + { + // TODO: expand test case + return array( + array(null, null), + array('/a/b/e', '/a/b/c'), + array('/a/b/e', '/a/b/d'), + array(null, '/a/b/e'), + ); + } +} \ No newline at end of file diff --git a/Tests/Functional/Twig/Extension/CmfExtensionHierarchyTest.php b/Tests/Functional/Twig/Extension/CmfExtensionHierarchyTest.php deleted file mode 100644 index 3829588e..00000000 --- a/Tests/Functional/Twig/Extension/CmfExtensionHierarchyTest.php +++ /dev/null @@ -1,122 +0,0 @@ -getContainer(); - $managerRegistry = $container->get('doctrine_phpcr'); - $session = $managerRegistry->getConnection(); - $root = $session->getRootNode(); - if ($root->hasNode('a')) { - $session->removeItem('/a'); - } - - $a = $root->addNode('a'); - $b = $a->addNode('b'); - $c = $b->addNode('c'); - $d = $b->addNode('d'); - $e = $b->addNode('e'); - $f = $a->addNode('f'); - $g = $f->addNode('g'); - $h = $g->addNode('h'); - - $session->save(); - - $this->pwc = $this->getMock('Symfony\Cmf\Bundle\CoreBundle\PublishWorkflow\PublishWorkflowCheckerInterface'); - $this->pwc->expects($this->any()) - ->method('checkIsPublished') - ->will($this->returnValue(true)); - - $this->extension = new CmfExtension($this->pwc, $managerRegistry, 'default'); - } - - public function testGetDescendants() - { - $this->assertEquals(array(), $this->extension->getDescendants(null)); - - $this->assertEquals(array('/a/b', '/a/b/c', '/a/b/d', '/a/b/e', '/a/f', '/a/f/g', '/a/f/g/h'), $this->extension->getDescendants('/a')); - - $this->assertEquals(array('/a/b', '/a/f'), $this->extension->getDescendants('/a', 1)); - } - - /** - * @dataProvider getPrevData - */ - public function testGetPrev($expected, $path) - { - $prev = $this->extension->getPrev($path); - if (null === $expected) { - $this->assertNull($prev); - } else { - $this->assertInstanceOf('Doctrine\ODM\PHPCR\Document\Generic', $prev); - $this->assertEquals($expected, $prev->getId()); - } - } - - public static function getPrevData() - { - return array( - array(null, null), - array(null, '/a'), - array('/a', '/a/b'), - array('/a/b', '/a/b/c'), - array('/a/b/c', '/a/b/d'), - array('/a/b/d', '/a/b/e'), - array('/a/b/e', '/a/f'), - array('/a/f', '/a/f/g'), - array('/a/f/g', '/a/f/g/h'), - ); - } - - /** - * @dataProvider getNextData - */ - public function testGetNext($expected, $path) - { - $next = $this->extension->getNext($path); - if (null === $expected) { - $this->assertNull($next); - } else { - $this->assertInstanceOf('Doctrine\ODM\PHPCR\Document\Generic', $next); - $this->assertEquals($expected, $next->getId()); - } - } - - public static function getNextData() - { - return array( - array(null, null), - array('/a/b', '/a'), - array('/a/b/c', '/a/b'), - array('/a/b/d', '/a/b/c'), - array('/a/b/e', '/a/b/d'), - array('/a/f', '/a/b/e'), - array('/a/f/g', '/a/f'), - array('/a/f/g/h', '/a/f/g'), - array(null, '/a/f/g/h'), - ); - } - - public function testGetPrevLinkable() - { - $this->assertNull($this->extension->getPrevLinkable(null)); - - $this->markTestIncomplete('TODO: write test'); - } - - public function testGetNextLinkable() - { - $this->assertNull($this->extension->getNextLinkable(null)); - - $this->markTestIncomplete('TODO: write test'); - } -} diff --git a/Tests/Functional/Twig/ServiceTest.php b/Tests/Functional/Twig/ServiceTest.php new file mode 100644 index 00000000..09229eb3 --- /dev/null +++ b/Tests/Functional/Twig/ServiceTest.php @@ -0,0 +1,16 @@ +getContainer()->get('twig'); + $ext = $twig->getExtension('cmf'); + $this->assertNotEmpty($ext); + } +} + diff --git a/Tests/Resources/Document/RouteAware.php b/Tests/Resources/Document/RouteAware.php new file mode 100644 index 00000000..08996528 --- /dev/null +++ b/Tests/Resources/Document/RouteAware.php @@ -0,0 +1,24 @@ +id; + } + + public function getRoutes() + { + } +} \ No newline at end of file diff --git a/Tests/Unit/Admin/Extensions/PublishTimePeriodExtensionTest.php b/Tests/Unit/Admin/Extensions/PublishTimePeriodExtensionTest.php new file mode 100644 index 00000000..528e6f89 --- /dev/null +++ b/Tests/Unit/Admin/Extensions/PublishTimePeriodExtensionTest.php @@ -0,0 +1,29 @@ +formMapper = $this->getMockBuilder( + 'Sonata\AdminBundle\Form\FormMapper' + )->disableOriginalConstructor()->getMock(); + + $this->extension = new PublishTimePeriodExtension(); + } + + public function testFormMapper() + { + $this->formMapper->expects($this->once()) + ->method('with') + ->will($this->returnSelf()); + $this->formMapper->expects($this->exactly(2)) + ->method('add') + ->will($this->returnSelf()); + + $this->extension->configureFormFields($this->formMapper); + } +} diff --git a/Tests/Unit/Admin/Extensions/PublishWorkflowExtensionTest.php b/Tests/Unit/Admin/Extensions/PublishableExtensionTest.php similarity index 69% rename from Tests/Unit/Admin/Extensions/PublishWorkflowExtensionTest.php rename to Tests/Unit/Admin/Extensions/PublishableExtensionTest.php index 361f6315..d15f0282 100644 --- a/Tests/Unit/Admin/Extensions/PublishWorkflowExtensionTest.php +++ b/Tests/Unit/Admin/Extensions/PublishableExtensionTest.php @@ -2,9 +2,10 @@ namespace Symfony\Cmf\Bundle\CoreBundle\Tests\Unit\Admin\Extension; -use Symfony\Cmf\Bundle\CoreBundle\Admin\Extension\PublishWorkflowExtension; -class PublishWorkflowExtensionTest extends \PHPUnit_Framework_Testcase +use Symfony\Cmf\Bundle\CoreBundle\Admin\Extension\PublishableExtension; + +class PublishableExtensionTest extends \PHPUnit_Framework_Testcase { public function setUp() { @@ -12,7 +13,7 @@ public function setUp() 'Sonata\AdminBundle\Form\FormMapper' )->disableOriginalConstructor()->getMock(); - $this->extension = new PublishWorkflowExtension; + $this->extension = new PublishableExtension(); } public function testFormMapper() @@ -20,7 +21,7 @@ public function testFormMapper() $this->formMapper->expects($this->once()) ->method('with') ->will($this->returnSelf()); - $this->formMapper->expects($this->exactly(3)) + $this->formMapper->expects($this->exactly(1)) ->method('add') ->will($this->returnSelf()); diff --git a/Tests/Unit/PublishWorkflow/PublishWorkflowCheckerTest.php b/Tests/Unit/PublishWorkflow/PublishWorkflowCheckerTest.php index 680a6b9f..281d0343 100644 --- a/Tests/Unit/PublishWorkflow/PublishWorkflowCheckerTest.php +++ b/Tests/Unit/PublishWorkflow/PublishWorkflowCheckerTest.php @@ -2,188 +2,159 @@ namespace Symfony\Cmf\Bundle\CoreBundle\Tests\Unit\PublishWorkflow; +use Symfony\Cmf\Bundle\CoreBundle\PublishWorkflow\PublishableWriteInterface; use Symfony\Cmf\Bundle\CoreBundle\PublishWorkflow\PublishWorkflowChecker; +use Symfony\Component\DependencyInjection\ContainerInterface; +use Symfony\Component\Security\Core\Authentication\Token\AnonymousToken; +use Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface; use Symfony\Component\Security\Core\SecurityContextInterface; class PublishWorkflowCheckerTest extends \PHPUnit_Framework_Testcase { + /** + * @var PublishWorkflowChecker + */ + private $pwfc; + + /** + * @var string + */ + private $role; + + /** + * @var ContainerInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $container; + + /** + * @var SecurityContextInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $sc; + + /** + * @var PublishableWriteInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $doc; + + /** + * @var AccessDecisionManagerInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $adm; + public function setUp() { $this->role = 'IS_FOOBAR'; + $this->container = $this->getMock('Symfony\Component\DependencyInjection\ContainerInterface'); $this->sc = $this->getMock('Symfony\Component\Security\Core\SecurityContextInterface'); - $this->doc = $this->getMock('Symfony\Cmf\Bundle\CoreBundle\PublishWorkflow\PublishWorkflowInterface'); + $this->container + ->expects($this->any()) + ->method('get') + ->with('security.context') + ->will($this->returnValue($this->sc)) + ; + $this->doc = $this->getMock('Symfony\Cmf\Bundle\CoreBundle\PublishWorkflow\PublishableWriteInterface'); + $this->adm = $this->getMock('Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface'); $this->stdClass = new \stdClass; - $this->pwfc = new PublishWorkflowChecker($this->role, $this->sc); + $this->pwfc = new PublishWorkflowChecker($this->container, $this->adm, $this->role); } - public function testDocDoesntImplementInterface() + /** + * Calling + */ + public function testIsGranted() { - $res = $this->pwfc->checkIsPublished($this->stdClass); - $this->assertTrue($res); + $token = new AnonymousToken('', ''); + $this->sc->expects($this->any()) + ->method('getToken') + ->will($this->returnValue($token)) + ; + $this->sc->expects($this->never()) + ->method('isGranted') + ; + $this->adm->expects($this->once()) + ->method('decide') + ->with($token, array(PublishWorkflowChecker::VIEW_ANONYMOUS_ATTRIBUTE), $this->doc) + ->will($this->returnValue(true)) + ; + + $this->assertTrue($this->pwfc->isGranted(PublishWorkflowChecker::VIEW_ANONYMOUS_ATTRIBUTE, $this->doc)); } - public function providePublishWorkflowChecker() + public function testNotHasBypassRole() { - return array( - array(array( - 'expected' => true, - 'granted_role' => 'IS_FOOBAR', - 'is_publishable' => false, - )), - array(array( - 'expected' => true, - 'is_publishable' => true, - )), - array(array( - 'expected' => true, - 'granted_role' => 'TEST-3', - 'start_date' => new \DateTime('2000-01-01'), - 'end_date' => new \DateTime('2030-01-01'), - 'is_publishable' => true, - )), - array(array( - 'expected' => false, - 'granted_role' => 'UNAUTH_ROLE', - 'start_date' => new \DateTime('01/01/2000'), - 'end_date' => new \DateTime('01/01/2001'), - 'is_publishable' => true, - )), - array(array( - 'expected' => false, - 'granted_role' => 'UNAUTH_ROLE', - 'start_date' => new \DateTime('01/01/2000'), - 'end_date' => new \DateTime('01/01/2030'), - 'is_publishable' => false, - )), - array(array( - 'expected' => true, - 'granted_role' => 'UNAUTH_ROLE', - 'start_date' => new \DateTime('01/01/2000'), - 'end_date' => null, - 'is_publishable' => true, - )), - array(array( - 'expected' => false, - 'granted_role' => 'UNAUTH_ROLE', - 'start_date' => null, - 'end_date' => new \DateTime('01/01/2000'), - 'is_publishable' => true, - )), - array(array( - 'expected' => true, - 'granted_role' => 'UNAUTH_ROLE', - 'start_date' => null, - 'end_date' => new \DateTime('01/01/2030'), - 'is_publishable' => true, - )), - array(array( - 'expected' => true, - 'granted_role' => 'UNAUTH_ROLE', - 'start_date' => null, - 'end_date' => null, - 'is_publishable' => null, - )), - array(array( - 'expected' => true, - 'granted_role' => 'TEST-3', - 'start_date' => new \DateTime('2000-01-01'), - 'end_date' => new \DateTime('2030-01-01'), - 'is_publishable' => null, - )), - array(array( - 'expected' => false, - 'granted_role' => 'UNAUTH_ROLE', - 'start_date' => new \DateTime('01/01/2000'), - 'end_date' => new \DateTime('01/01/2001'), - 'is_publishable' => null, - )), - array(array( - 'expected' => false, - 'granted_role' => 'UNAUTH_ROLE', - 'start_date' => new \DateTime('01/01/2000'), - 'end_date' => new \DateTime('01/01/2030'), - 'is_publishable' => false, - )), - array(array( - 'expected' => true, - 'granted_role' => 'UNAUTH_ROLE', - 'start_date' => new \DateTime('01/01/2000'), - 'end_date' => null, - 'is_publishable' => null, - )), - array(array( - 'expected' => false, - 'granted_role' => 'UNAUTH_ROLE', - 'start_date' => null, - 'end_date' => new \DateTime('01/01/2000'), - 'is_publishable' => null, - )), - array(array( - 'expected' => true, - 'granted_role' => 'UNAUTH_ROLE', - 'start_date' => null, - 'end_date' => new \DateTime('01/01/2030'), - 'is_publishable' => null, - )), - // Test overwrite current time - array(array( - 'expected' => false, - 'is_publishable' => true, - 'end_date' => new \DateTime('01/01/2000'), - 'current_time' => new \DateTime('01/01/2001'), - )), - array(array( - 'expected' => true, - 'is_publishable' => true, - 'end_date' => new \DateTime('01/01/2000'), - 'current_time' => new \DateTime('01/01/1980'), - )), - ); + $token = new AnonymousToken('', ''); + $this->sc->expects($this->any()) + ->method('getToken') + ->will($this->returnValue($token)) + ; + $this->sc->expects($this->once()) + ->method('isGranted') + ->with($this->role) + ->will($this->returnValue(false)) + ; + $this->adm->expects($this->once()) + ->method('decide') + ->with($token, array(PublishWorkflowChecker::VIEW_ATTRIBUTE), $this->doc) + ->will($this->returnValue(true)) + ; + + $this->assertTrue($this->pwfc->isGranted(PublishWorkflowChecker::VIEW_ATTRIBUTE, $this->doc)); } - /** - * @dataProvider providePublishWorkflowChecker - */ - public function testPublishWorkflowChecker($options) + public function testHasBypassRole() { - $options = array_merge(array( - 'expected' => false, - 'granted_role' => 'NONE', - 'start_date' => null, - 'end_date' => null, - 'is_publishable' => null, - 'current_time' => null, - ), $options); - + $token = new AnonymousToken('', ''); $this->sc->expects($this->any()) + ->method('getToken') + ->will($this->returnValue($token)) + ; + $this->sc->expects($this->once()) ->method('isGranted') - ->will($this->returnCallback(function ($given) use ($options) { - return $given === $options['granted_role']; - })); + ->with($this->role) + ->will($this->returnValue(true)) + ; + $this->adm->expects($this->never()) + ->method('decide') + ; - $this->doc->expects($this->any()) - ->method('getPublishStartDate') - ->will($this->returnValue($options['start_date'])); - - $this->doc->expects($this->any()) - ->method('getPublishEndDate') - ->will($this->returnValue($options['end_date'])); + $this->assertTrue($this->pwfc->isGranted(PublishWorkflowChecker::VIEW_ATTRIBUTE, $this->doc)); + } - $this->doc->expects($this->any()) - ->method('isPublishable') - ->will($this->returnValue($options['is_publishable'])); + public function testNoFirewall() + { + $token = new AnonymousToken('', ''); + $this->sc->expects($this->any()) + ->method('getToken') + ->will($this->returnValue(null)) + ; + $this->sc->expects($this->never()) + ->method('isGranted') + ; + $this->adm->expects($this->once()) + ->method('decide') + ->with($token, array(PublishWorkflowChecker::VIEW_ATTRIBUTE), $this->doc) + ->will($this->returnValue(true)) + ; - if ($options['current_time']) { - $this->pwfc->setCurrentTime($options['current_time']); - } + $this->assertTrue($this->pwfc->isGranted(PublishWorkflowChecker::VIEW_ATTRIBUTE, $this->doc)); + } - $res = $this->pwfc->checkIsPublished($this->doc); + public function testSetToken() + { + $token = new AnonymousToken('x', 'y'); + $this->pwfc->setToken($token); + $this->assertSame($token, $this->pwfc->getToken()); + } - if (true === $options['expected']) { - $this->assertTrue($res); - } else { - $this->assertFalse($res); - } + public function testSupportsClass() + { + $class = 'Test\Class'; + $this->adm->expects($this->once()) + ->method('supportsClass') + ->with($class) + ->will($this->returnValue(true)) + ; + $this->assertTrue($this->pwfc->supportsClass($class)); } } diff --git a/Tests/Unit/PublishWorkflow/Voter/PublishTimePeriodVoterTest.php b/Tests/Unit/PublishWorkflow/Voter/PublishTimePeriodVoterTest.php new file mode 100644 index 00000000..458aed21 --- /dev/null +++ b/Tests/Unit/PublishWorkflow/Voter/PublishTimePeriodVoterTest.php @@ -0,0 +1,131 @@ +voter = new PublishTimePeriodVoter(); + $this->token = new AnonymousToken('', ''); + } + + public function providePublishWorkflowChecker() + { + return array( + array( + 'expected' => VoterInterface::ACCESS_GRANTED, + 'startDate' => new \DateTime('01/01/2000'), + 'endDate' => new \DateTime('01/02/2030'), + ), + array( + 'expected' => VoterInterface::ACCESS_DENIED, + 'startDate' => new \DateTime('01/01/2000'), + 'endDate' => new \DateTime('01/01/2001'), + ), + array( + 'expected' => VoterInterface::ACCESS_GRANTED, + 'startDate' => new \DateTime('01/01/2000'), + 'endDate' => null, + ), + array( + 'expected' => VoterInterface::ACCESS_DENIED, + 'startDate' => new \DateTime('01/01/2030'), + 'endDate' => null, + ), + array( + 'expected' => VoterInterface::ACCESS_GRANTED, + 'startDate' => null, + 'endDate' => new \DateTime('01/01/2030'), + ), + array( + 'expected' => VoterInterface::ACCESS_DENIED, + 'startDate' => null, + 'endDate' => new \DateTime('01/01/2000'), + ), + array( + 'expected' => VoterInterface::ACCESS_GRANTED, + 'startDate' => null, + 'endDate' => null, + ), + // unsupported attribute + array( + 'expected' => VoterInterface::ACCESS_ABSTAIN, + 'startDate' => new \DateTime('01/01/2000'), + 'endDate' => new \DateTime('01/01/2030'), + 'attributes' => array(PublishWorkflowChecker::VIEW_ATTRIBUTE, 'other'), + ), + // Test overwrite current time + array( + 'expected' => VoterInterface::ACCESS_DENIED, + 'startDate' => null, + 'endDate' => new \DateTime('01/01/2030'), + 'attributes' => PublishWorkflowChecker::VIEW_ATTRIBUTE, + 'currentTime' => new \DateTime('02/02/2030'), + ), + array( + 'expected' => VoterInterface::ACCESS_GRANTED, + 'startDate' => null, + 'endDate' => new \DateTime('01/01/2000'), + 'attributes' => PublishWorkflowChecker::VIEW_ATTRIBUTE, + 'currentTime' => new \DateTime('01/01/1980'), + ), + ); + } + + /** + * @dataProvider providePublishWorkflowChecker + */ + public function testPublishWorkflowChecker($expected, $startDate, $endDate, $attributes = PublishWorkflowChecker::VIEW_ATTRIBUTE, $currentTime = false) + { + $attributes = (array) $attributes; + $doc = $this->getMock('Symfony\Cmf\Bundle\CoreBundle\PublishWorkflow\PublishTimePeriodInterface'); + + $doc->expects($this->any()) + ->method('getPublishStartDate') + ->will($this->returnValue($startDate)) + ; + + $doc->expects($this->any()) + ->method('getPublishEndDate') + ->will($this->returnValue($endDate)) + ; + + if (false !== $currentTime) { + $this->voter->setCurrentTime($currentTime); + } + + $this->assertEquals($expected, $this->voter->vote($this->token, $doc, $attributes)); + + } + + public function testUnsupportedClass() + { + $result = $this->voter->vote( + $this->token, + $this, + array(PublishWorkflowChecker::VIEW_ATTRIBUTE) + ); + $this->assertEquals(VoterInterface::ACCESS_ABSTAIN, $result); + } +} diff --git a/Tests/Unit/PublishWorkflow/Voter/PublishableVoterTest.php b/Tests/Unit/PublishWorkflow/Voter/PublishableVoterTest.php new file mode 100644 index 00000000..5cd367ad --- /dev/null +++ b/Tests/Unit/PublishWorkflow/Voter/PublishableVoterTest.php @@ -0,0 +1,97 @@ +voter = new PublishableVoter(); + $this->token = new AnonymousToken('', ''); + } + + public function providePublishWorkflowChecker() + { + return array( + array( + 'expected' => VoterInterface::ACCESS_GRANTED, + 'isPublishable' => true, + 'attributes' => PublishWorkflowChecker::VIEW_ATTRIBUTE, + ), + array( + 'expected' => VoterInterface::ACCESS_DENIED, + 'isPublishable' => false, + 'attributes' => PublishWorkflowChecker::VIEW_ATTRIBUTE, + ), + array( + 'expected' => VoterInterface::ACCESS_GRANTED, + 'isPublishable' => true, + 'attributes' => array( + PublishWorkflowChecker::VIEW_ANONYMOUS_ATTRIBUTE, + PublishWorkflowChecker::VIEW_ATTRIBUTE, + ), + ), + array( + 'expected' => VoterInterface::ACCESS_DENIED, + 'isPublishable' => false, + 'attributes' => PublishWorkflowChecker::VIEW_ANONYMOUS_ATTRIBUTE, + ), + array( + 'expected' => VoterInterface::ACCESS_ABSTAIN, + 'isPublishable' => true, + 'attributes' => 'other', + ), + array( + 'expected' => VoterInterface::ACCESS_ABSTAIN, + 'isPublishable' => true, + 'attributes' => array(PublishWorkflowChecker::VIEW_ATTRIBUTE, 'other'), + ), + ); + } + + /** + * @dataProvider providePublishWorkflowChecker + * + * use for voters! + */ + public function testPublishWorkflowChecker($expected, $isPublishable, $attributes) + { + $attributes = (array) $attributes; + $doc = $this->getMock('Symfony\Cmf\Bundle\CoreBundle\PublishWorkflow\PublishableInterface'); + $doc->expects($this->any()) + ->method('isPublishable') + ->will($this->returnValue($isPublishable)) + ; + + $this->assertEquals($expected, $this->voter->vote($this->token, $doc, $attributes)); + } + + public function testUnsupportedClass() + { + $result = $this->voter->vote( + $this->token, + $this, + array(PublishWorkflowChecker::VIEW_ATTRIBUTE) + ); + $this->assertEquals(VoterInterface::ACCESS_ABSTAIN, $result); + } +} diff --git a/Tests/Unit/Templating/Helper/CmfHelperTest.php b/Tests/Unit/Templating/Helper/CmfHelperTest.php index c5fa191c..d36498b5 100644 --- a/Tests/Unit/Templating/Helper/CmfHelperTest.php +++ b/Tests/Unit/Templating/Helper/CmfHelperTest.php @@ -2,6 +2,7 @@ namespace Symfony\Cmf\Bundle\CoreBundle\Tests\Unit\Twig; +use Symfony\Cmf\Bundle\CoreBundle\PublishWorkflow\PublishWorkflowChecker; use Symfony\Cmf\Bundle\CoreBundle\Templating\Helper\CmfHelper; class CmfHelperTest extends \PHPUnit_Framework_TestCase @@ -10,11 +11,14 @@ class CmfHelperTest extends \PHPUnit_Framework_TestCase private $managerRegistry; private $manager; private $uow; + /** + * @var CmfHelper + */ private $extension; public function setUp() { - $this->pwc = $this->getMock('Symfony\Cmf\Bundle\CoreBundle\PublishWorkflow\PublishWorkflowCheckerInterface'); + $this->pwc = $this->getMock('Symfony\Component\Security\Core\SecurityContextInterface'); $this->managerRegistry = $this->getMockBuilder('Doctrine\Bundle\PHPCRBundle\ManagerRegistry') ->disableOriginalConstructor() @@ -92,6 +96,19 @@ public function testGetPath() $this->assertEquals('/foo/bar', $this->extension->getPath($document)); } + public function testGetPathInvalid() + { + $document = new \stdClass(); + + $this->uow->expects($this->once()) + ->method('getDocumentId') + ->with($document) + ->will($this->throwException(new \Exception('test'))); + ; + + $this->assertFalse($this->extension->getPath($document)); + } + public function testFind() { $document = new \stdClass(); @@ -132,17 +149,32 @@ public function testFindManyIgnoreRole() $this->manager->expects($this->any()) ->method('find') - ->will($this->onConsecutiveCalls($documentA, null, $documentA, $documentB)) + ->will($this->onConsecutiveCalls($documentA, $documentB)) ; $this->pwc->expects($this->any()) - ->method('checkIsPublished') - ->with($documentA) + ->method('isGranted') ->will($this->onConsecutiveCalls(false, true)) ; - $this->assertEquals(array($documentA), $this->extension->findMany(array('/foo', 'bar'), false, false, null)); - $this->assertEquals(array($documentB), $this->extension->findMany(array('/foo', 'bar'), false, false, false)); + $this->assertEquals(array($documentB), $this->extension->findMany(array('/foo', '/bar'), false, false, true)); + } + + public function testFindManyIgnoreWorkflow() + { + $documentA = new \stdClass(); + $documentB = new \stdClass(); + + $this->manager->expects($this->any()) + ->method('find') + ->will($this->onConsecutiveCalls($documentA, $documentB)) + ; + + $this->pwc->expects($this->never()) + ->method('isGranted') + ; + + $this->assertEquals(array($documentA, $documentB), $this->extension->findMany(array('/foo', '/bar'), false, false, null)); } public function testFindManyLimitOffset() @@ -160,6 +192,24 @@ public function testFindManyLimitOffset() $this->assertEquals(array($documentB), $this->extension->findMany(array('/foo', 'bar'), 1, 1, null)); } + /** + * @expectedException \Symfony\Component\Config\Definition\Exception\InvalidConfigurationException + */ + public function testFindManyNoWorkflow() + { + $this->extension = new CmfHelper(null, $this->managerRegistry, 'foo'); + + $documentA = new \stdClass(); + + $this->manager->expects($this->any()) + ->method('find') + ->with(null, '/foo') + ->will($this->returnValue($documentA)) + ; + + $this->extension->findMany(array('/foo', '/bar'), false, false); + } + public function testIsPublished() { $this->assertFalse($this->extension->isPublished(null)); @@ -167,8 +217,8 @@ public function testIsPublished() $document = new \stdClass(); $this->pwc->expects($this->any()) - ->method('checkIsPublished') - ->with($document) + ->method('isGranted') + ->with(PublishWorkflowChecker::VIEW_ANONYMOUS_ATTRIBUTE, $document) ->will($this->onConsecutiveCalls(false, true)) ; @@ -176,6 +226,15 @@ public function testIsPublished() $this->assertTrue($this->extension->isPublished($document)); } + /** + * @expectedException \Symfony\Component\Config\Definition\Exception\InvalidConfigurationException + */ + public function testIsPublishedNoWorkflow() + { + $this->extension = new CmfHelper(null, $this->managerRegistry, 'foo'); + $this->extension->isPublished(new \stdClass()); + } + public function testGetLocalesFor() { $this->assertEquals(array(), $this->extension->getLocalesFor(null)); @@ -227,6 +286,19 @@ public function testGetChild() $this->assertEquals($child, $this->extension->getChild($parent, 'bar')); } + public function testGetChildError() + { + $parent = new \stdClass(); + + $this->uow->expects($this->once()) + ->method('getDocumentId') + ->with($parent) + ->will($this->throwException(new \Exception('test'))) + ; + + $this->assertFalse($this->extension->getChild($parent, 'bar')); + } + public function testGetChildren() { $parent = new \stdClass(); diff --git a/Tests/Unit/Twig/Extension/CmfExtensionTest.php b/Tests/Unit/Twig/Extension/CmfExtensionTest.php new file mode 100644 index 00000000..4876004c --- /dev/null +++ b/Tests/Unit/Twig/Extension/CmfExtensionTest.php @@ -0,0 +1,26 @@ +cmfHelper = $this->getMockBuilder( + 'Symfony\Cmf\Bundle\CoreBundle\Templating\Helper\CmfHelper' + )->disableOriginalConstructor()->getMock(); + + $this->cmfExtension = new CmfExtension($this->cmfHelper); + $this->env = new \Twig_Environment(); + $this->env->addExtension($this->cmfExtension); + } + + + public function testFunctions() + { + $functions = $this->cmfExtension->getFunctions(); + $this->assertCount(15, $functions); + } +} diff --git a/Twig/Extension/CmfExtension.php b/Twig/Extension/CmfExtension.php index 5a71a902..0c11623b 100644 --- a/Twig/Extension/CmfExtension.php +++ b/Twig/Extension/CmfExtension.php @@ -4,8 +4,15 @@ use Symfony\Cmf\Bundle\CoreBundle\Templating\Helper\CmfHelper; -class CmfExtension extends CmfHelper implements \Twig_ExtensionInterface +class CmfExtension extends \Twig_Extension { + protected $cmfHelper; + + public function __construct(CmfHelper $cmfHelper) + { + $this->cmfHelper = $cmfHelper; + } + /** * Get list of available functions * @@ -13,101 +20,34 @@ class CmfExtension extends CmfHelper implements \Twig_ExtensionInterface */ public function getFunctions() { - $functions = array('cmf_is_published' => new \Twig_Function_Method($this, 'isPublished')); - - if ($this->dm) { - $functions['cmf_child'] = new \Twig_Function_Method($this, 'getChild'); - $functions['cmf_children'] = new \Twig_Function_Method($this, 'getChildren'); - $functions['cmf_prev'] = new \Twig_Function_Method($this, 'getPrev'); - $functions['cmf_next'] = new \Twig_Function_Method($this, 'getNext'); - $functions['cmf_find'] = new \Twig_Function_Method($this, 'find'); - $functions['cmf_find_many'] = new \Twig_Function_Method($this, 'findMany'); - $functions['cmf_descendants'] = new \Twig_Function_Method($this, 'getDescendants'); - $functions['cmf_nodename'] = new \Twig_Function_Method($this, 'getNodeName'); - $functions['cmf_parent_path'] = new \Twig_Function_Method($this, 'getParentPath'); - $functions['cmf_path'] = new \Twig_Function_Method($this, 'getPath'); - $functions['cmf_document_locales'] = new \Twig_Function_Method($this, 'getLocalesFor'); - - if (interface_exists('Symfony\Cmf\Component\Routing\RouteAwareInterface')) { - $functions['cmf_prev_linkable'] = new \Twig_Function_Method($this, 'getPrevLinkable'); - $functions['cmf_next_linkable'] = new \Twig_Function_Method($this, 'getNextLinkable'); - $functions['cmf_linkable_children'] = new \Twig_Function_Method($this, 'getLinkableChildren'); - } + $functions = array( + new \Twig_SimpleFunction('cmf_is_published', array($this->cmfHelper, 'isPublished')), + new \Twig_SimpleFunction('cmf_child', array($this->cmfHelper, 'getChild')), + new \Twig_SimpleFunction('cmf_children', array($this->cmfHelper, 'getChildren')), + new \Twig_SimpleFunction('cmf_prev', array($this->cmfHelper, 'getPrev')), + new \Twig_SimpleFunction('cmf_next', array($this->cmfHelper, 'getNext')), + new \Twig_SimpleFunction('cmf_find', array($this->cmfHelper, 'find')), + new \Twig_SimpleFunction('cmf_find_many', array($this->cmfHelper, 'findMany')), + new \Twig_SimpleFunction('cmf_descendants', array($this->cmfHelper, 'getDescendants')), + new \Twig_SimpleFunction('cmf_nodename', array($this->cmfHelper, 'getNodeName')), + new \Twig_SimpleFunction('cmf_parent_path', array($this->cmfHelper, 'getParentPath')), + new \Twig_SimpleFunction('cmf_path', array($this->cmfHelper, 'getPath')), + new \Twig_SimpleFunction('cmf_document_locales', array($this->cmfHelper, 'getLocalesFor')), + ); + + if (interface_exists('Symfony\Cmf\Component\Routing\RouteAwareInterface')) { + $functions = array_merge($functions, array( + new \Twig_SimpleFunction('cmf_prev_linkable', array($this->cmfHelper, 'getPrevLinkable')), + new \Twig_SimpleFunction('cmf_next_linkable', array($this->cmfHelper, 'getNextLinkable')), + new \Twig_SimpleFunction('cmf_linkable_children', array($this->cmfHelper, 'getLinkableChildren')), + )); } return $functions; } - // from \Twig_Extension - - /** - * Initializes the runtime environment. - * - * This is where you can load some file that contains filter functions for instance. - * - * @param Twig_Environment $environment The current Twig_Environment instance - */ - public function initRuntime(\Twig_Environment $environment) - { - } - - /** - * Returns the token parser instances to add to the existing list. - * - * @return array An array of Twig_TokenParserInterface or Twig_TokenParserBrokerInterface instances - */ - public function getTokenParsers() - { - return array(); - } - - /** - * Returns the node visitor instances to add to the existing list. - * - * @return array An array of Twig_NodeVisitorInterface instances - */ - public function getNodeVisitors() - { - return array(); - } - - /** - * Returns a list of filters to add to the existing list. - * - * @return array An array of filters - */ - public function getFilters() - { - return array(); - } - - /** - * Returns a list of tests to add to the existing list. - * - * @return array An array of tests - */ - public function getTests() - { - return array(); - } - - /** - * Returns a list of operators to add to the existing list. - * - * @return array An array of operators - */ - public function getOperators() - { - return array(); - } - - /** - * Returns a list of global variables to add to the existing list. - * - * @return array An array of global variables - */ - public function getGlobals() + public function getName() { - return array(); + return 'cmf'; } } diff --git a/composer.json b/composer.json index 00ebe817..b2477f7d 100644 --- a/composer.json +++ b/composer.json @@ -17,7 +17,7 @@ "symfony/framework-bundle": "~2.1" }, "require-dev": { - "symfony-cmf/routing": "~1.1-dev", + "symfony-cmf/routing-bundle": "~1.1-dev", "symfony-cmf/testing": "~1.0-dev", "sonata-project/admin-bundle": "2.2.*" },