diff --git a/bundles/core.rst b/bundles/core.rst
index 8264d689..01548d84 100644
--- a/bundles/core.rst
+++ b/bundles/core.rst
@@ -27,48 +27,359 @@ Configuration
# app/config/config.yml
cmf_core:
- document_manager_name: default
- role: IS_AUTHENTICATED_ANONYMOUSLY # used by the publish workflow checker
+ document_manager_name: ~ # used for the twig functions to fetch documents
+ publish_workflow:
+ enabled: true
+ view_non_published_role: ROLE_CAN_VIEW_NON_PUBLISHED
+ request_listener: true
.. code-block:: xml
-
-
-
+
+
+
.. code-block:: php
// app/config/config.php
$container->loadFromExtension('cmf_core', array(
- 'document_manager_name' => 'default',
- // used by the publish workflow checker
- 'role' => 'IS_AUTHENTICATED_ANONYMOUSLY',
+ 'document_manager_name' => null,
+ 'publish_workflow' => array(
+ 'enabled' => true,
+ 'view_non_published_role' => 'ROLE_CAN_VIEW_NON_PUBLISHED',
+ 'request_listener' => true,
+ ),
));
+The publish workflow is enabled by default. If you do not want to use it, you
+can set ``cmf_core.publish_workflow.enabled: false`` to gain some performance.
+
.. _bundle-core-publish_workflow:
-Publish Workflow Checker
-------------------------
+Publish Workflow
+----------------
-The Bundle provides a ``cmf_core.publish_workflow_checker`` service
-which implements ``PublishWorkflowCheckerInterface``. This interface defines a
-single method ``checkIsPublished()``.
+The publish workflow system allows to control what content is available on the
+site. This is similar to the `Symfony2 Security component`_. But contrary to the
+security context, the publish check can be executed even when no firewall is in
+place and the security context thus has no token (see `Symfony2 Authorization`_).
-.. code-block:: php
+The publish workflow is also tied into the security workflow: The core bundle
+registers a security voter that forwards security checks to the publish
+workflow. This means that if you always have a firewall, you can just use
+the normal security context and the twig function ``is_granted`` to check for
+publication.
+
+A good introduction to the Symfony core security is the `Security Chapter`_ in
+the Symfony2 book.
+
+Check if Content is Published
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+The Bundle provides the ``cmf_core.publish_workflow.checker`` service which
+implements the :class:`Symfony\\Component\\Security\\Core\\SecurityContextInterface`
+of the Symfony security component. The method to check publication is, like
+with the security context,
+:method:`Symfony\\Component\\Security\\Core\\SecurityContextInterface::isGranted`.
+
+This method is used as when doing `ACL checks`_: The first argument is the
+desired action, the second the content object you want to do the action on.
+
+In 1.0, the only actions supported by the default voters are ``VIEW`` and
+``VIEW_ANONYMOUS``. Having the right to view means that the current user is
+allowed to see this content either because it is published or because of his
+specific permissions. In some contexts, your application might not want to
+show unpublished content even to a privileged user so as not to confuse him.
+For this, the "view anonymous" permission is used.
+
+The workflow checker is configured with a role that is allowed to bypass
+publication checks so that it can see unpublished content. This role should be
+given to editors. The default name of the role is ``ROLE_CAN_VIEW_NON_PUBLISHED``.
+
+.. configuration-block::
+
+ .. code-block:: yaml
+
+ # app/config/security.yml
+ security:
+ role_hierarchy:
+ ROLE_EDITOR: ROLE_CAN_VIEW_NON_PUBLISHED
+
+ .. code-block:: xml
+
+
+
+ ROLE_CAN_VIEW_NON_PUBLISHED
+
+
+ .. code-block:: php
+
+ // app/config/security.php
+ $container->loadFromExtension('security', array(
+ 'role_hierarchy' => array(
+ 'ROLE_EDITOR' => 'ROLE_CAN_VIEW_NON_PUBLISHED',
+ ),
+ ));
- $publishWorkflowChecker = $container->get('cmf_core.publish_workflow_checker');
+Once a user with ``ROLE_EDITOR`` is logged in - meaning there is a firewall in place for the path
+in question - he will have the permission to view unpublished content as well::
- // if to ignore the role when deciding if to consider the document as published
- $ignoreRole = false;
+ use Symfony\Cmf\Bundle\CoreBundle\PublishWorkflow\PublishWorkflowChecker;
- if ($publishWorkflowChecker->checkIsPublished($document, $ignoreRole)) {
+ // check if current user is allowed to see this document
+ $publishWorkflowChecker = $container->get('cmf_core.publish_workflow.checker');
+ if ($publishWorkflowChecker->isGranted(
+ PublishWorkflowChecker::VIEW_ATTRIBUTE,
+ $document)
+ ) {
+ // ...
+ }
+ // check if the document is published. even if the current role would allow
+ // to see the document, this will still return false if the documet is not
+ // published
+ if ($publishWorkflowChecker->isGranted(
+ PublishWorkflowChecker::VIEW_ANONYMOUS_ATTRIBUTE,
+ $document
+ )) {
// ...
}
+.. _bundle-core-publish_workflow-twig_function:
+
+To check publication in a template, use the twig function ``cmf_is_published``:
+
+.. configuration-block::
+
+ .. code-block:: jinja
+
+ {# check if document is published, regardless of current users role #}
+ {% if cmf_is_published(page) %}
+ {# ... output the document #}
+ {% endif %}
+
+ {#
+ check if current logged in user is allowed to view the document either
+ because it is published or because the current user may view unpublished
+ documents.
+ #}
+ {% if is_granted('VIEW', page) %}
+ {# ... output the document #}
+ {% endif %}
+
+ .. code-block:: php
+
+
+ isPublished($page)) : ?>
+
+
+
+
+ isGranted('VIEW', $page)) : ?>
+
+
+
+Code that loads content should do the publish checks. Note that the twig
+functions already check for publication. Thanks to a
+:ref:`request listener `, routes and
+the main content provided by the
+:ref:`DynamicRouter ` are checked automatically
+as well.
+
+It is possible to set the security token explicitly on the workflow checker.
+But by default, the checker will acquire the token from the default security
+context, and if there is none (typically when there is no firewall in place for
+that URL), an
+:class:`Symfony\\Component\\Security\\Core\\Authentication\\Token\\AnonymousToken`
+is created on the fly.
+
+If you check for ``VIEW`` and not ``VIEW_ANONYMOUS``, the first check is
+whether the security context knows the current user and if that user is granted
+the bypass role. If so, access is granted, otherwise the decision is delegated to a
+:class:`Symfony\\Component\\Security\\Core\\Authorization\\AccessDecisionManager`
+which calls all voters with the requested attributes, the object and the token.
+
+The decision manager is configured for an unanimous vote with "allow if all
+abstain". This means a single voter saying ``ACCESS_DENIED`` is enough for
+the content to be considered not published. If all voters abstain (for example
+when the content in question does not implement any workflow features) the
+content is still considered published.
+
+Publish Voters
+~~~~~~~~~~~~~~
+
+A voter has to implement the
+:class:`Symfony\\Component\\Security\\Core\\Authorization\\Voter\\VoterInterface`.
+It will get passed a content object and has to decide whether it is published
+according to its rules. The CoreBundle provides a couple of generic voters
+that check the content for having an interface exposing the methods they need.
+If the content implements the interface, they check the parameter and return
+``ACCESS_GRANTED`` or ``ACCESS_DENIED``, otherwise they return
+``ACCESS_ABSTAIN``.
+
+As voting is unanimous, each voter returns ``ACCESS_GRANTED`` if its criteria
+is met, but if a single voter returns ``ACCESS_DENIED``, the content is
+considered not published.
+
+You can also implement your :ref:`own voters `
+for additional publication behaviour.
+
+PublishableVoter
+................
+
+This voter checks on the ``PublishableInterface`` which simply has a method to
+return a boolean value.
+
+* **isPublishable**: If the object should be considered for publication or not.
+
+TimePeriodVoter
+...............
+
+This voter checks on the ``PublishTimePeriodInterface`` which defines a start
+and end date. A date may be null to indicate "always started" resp.
+"never ending".
+
+* **getPublishStartDate**: If non-null, the date from which the document
+ should start being published;
+* **getPublishEndDate**: If non-null, the date from which the document
+ should stop being published.
+
+.. _bundle-core-workflow_custom_voters:
+
+Custom Voters
+.............
+
+To build voters with custom logic, you need to implement
+:class:`Symfony\\Component\\Security\\Core\\Authentication\\Voter\\VoterInterface`
+and define a service with the tag ``cmf_published_voter``. This is similar
+to the ``security.voter`` tag, but adds your voter to the publish workflow. As
+with the security voters, you can specify a priority, though it is of limited
+use as the access decision must be unanimous. If you have more expensive checks,
+you can lower the priority of those voters.
+
+.. configuration-block::
+
+ .. code-block:: yaml
+
+ services:
+ acme.security.publishable_voter:
+ class: %my_namespace.security.publishable_voter.class%
+ tags:
+ - { name: cmf_published_voter, priority: 30 }
+
+ .. code-block:: xml
+
+
+
+
+
+ .. code-block:: php
+
+ use Symfony\Component\DependencyInjection\Definition;
+
+ $container
+ ->register(
+ 'acme.security.publishable_voter',
+ '%acme.security.publishable_voter.class%'
+ )
+ ->addTag('cmf_published_voter', array('priority' => 30))
+ ;
+
+As the workflow checker will create an
+:class:`Symfony\\Component\\Security\\Core\\Authentication\\Token\\AnonymousToken` on
+the fly if the security context has none, voters must be able to handle this
+situation when accessing the user. Also when accessing the security context,
+they first must check if it has a token and otherwise not call it to avoid
+triggering an exception. If a voter only gives access if there is a current
+user fulfills some requirement, it simply has to return ``ACCESS_DENIED`` if
+there is no current user.
+
+.. _bundle-core-workflow-request_listener:
+
+Publication Request Listener
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+The :ref:`DynamicRouter ` places the route
+object and the main content - if the route has a main content - into the
+request attributes. Unless you disable the
+``cmf_core.publish_workflow.request_listener``, this listener will listen
+on all requests and check publication of both the route object and the main
+content object.
+
+This means that custom templates for ``templates_by_class`` and the controllers
+of ``controllers_by_class`` need not check for publication explicitly as its
+already done.
+
+Editing publication information: Publish Workflow Sonata Admin Extension
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+There is a write interface for each publish workflow too, defining setter
+methods. The core bundle provides extensions for SonataAdminBundle to easily
+add editing of the publish workflow fields to all or selected admins.
+
+Instead of implementing ``PublishableInterface`` resp.
+``PublishTimePeriodInterface`` you models instead need to implement the
+``PublishableWriteInterface`` and / or ``PublishTimePeriodWriteInterface``.
+
+To enable the extensions in your admin classes, simply define the extension
+configuration in the ``sonata_admin`` section of your project configuration:
+
+.. configuration-block::
+
+ .. code-block:: yaml
+
+ # app/config/config.yml
+ sonata_admin:
+ # ...
+ extensions:
+ symfony_cmf_core.publish_workflow.admin_extension.publishable:
+ implements:
+ - Symfony\Cmf\Bundle\CoreBundle\PublishWorkflow\PublishableWriteInterface
+ symfony_cmf_core.publish_workflow.admin_extension.time_period:
+ implements:
+ - Symfony\Cmf\Bundle\CoreBundle\PublishWorkflow\PublishTimePeriodWriteInterface
+
+ .. code-block:: xml
+
+
+
+
+
+
+ Symfony\Cmf\Bundle\CoreBundle\PublishWorkflow\PublishableWriteInterface
+
+
+
+
+ Symfony\Cmf\Bundle\CoreBundle\PublishWorkflow\PublishTimePeriodWriteInterface
+
+
+
+
+ .. code-block:: php
+
+ // app/config/config.php
+ $container->loadFromExtension('sonata_admin', array(
+ 'extensions' => array(
+ 'symfony_cmf_core.admin_extension.publish_workflow' => array(
+ 'implements' => array(
+ 'Symfony\Cmf\Bundle\CoreBundle\PublishWorkflow\PublishWorkflowInterface',
+ ),
+ ),
+ ),
+ ));
+
+See the `Sonata Admin extension documentation`_ for more information.
+
Dependency Injection Tags
-------------------------
@@ -100,11 +411,22 @@ service must have the ``setRequest`` method or you will get a fatal error::
service definition and call ``$this->container->get('request')`` in your
code when you actually need the request.
-This tag is automatically translated to a `synchronized service`_ but as
-Symfony 2.2 does not have that feature, you can use this tag for bundles that
-you want to be able to use with Symfony 2.2. In custom applications that run
-with Symfony 2.3, there is no need for this tag, just use the synchronized
-service feature.
+For Symfony 2.3, this tag is automatically translated to a
+`synchronized service`_ but as Symfony 2.2 does not have that feature, you can
+use this tag for bundles that you want to be able to use with Symfony 2.2. In
+custom applications that run with Symfony 2.3, there is no need for this tag,
+just use the synchronized service feature.
+
+cmf_published_voter
+~~~~~~~~~~~~~~~~~~~
+
+Used to activate :ref:`custom voters ` for the
+:ref:`publish workflow ` . Tagging a service with
+``cmf_published_voter`` integrates it into the access decision of the publish
+workflow.
+
+This tag has the attribute *priority*. The lower the priority number, the
+earlier the voter gets to vote.
Templating
----------
@@ -112,11 +434,15 @@ Templating
Twig
~~~~
-The bundle provides a Twig extension, implementing the following functions:
+<<<<<<< HEAD
+The core bundle contains a Twig extension that provides a set of useful
+functions for your templates. The functions respect the
+:ref:`publish workflow ` if it is
* **cmf_find**: returns the document for the provided path
* **cmf_find_many**: returns an array of documents for the provided paths
-* **cmf_is_published**: checks if the provided document is published
+* **cmf_is_published**: checks if the provided document is published, see
+ :ref:`bundle-core-publish_workflow-twig_function`.
* **cmf_prev**: returns the previous document by examining the child nodes of
the provided parent
* **cmf_prev_linkable**: returns the previous linkable document by examining
@@ -256,4 +582,9 @@ contains the following methods:
.. _`CoreBundle`: https://github.com/symfony-cmf/CoreBundle#readme
+.. _`Symfony2 security component`: http://www.symfony.com/doc/current/components/security/index.html
+.. _`Symfony2 Authorization`: http://www.symfony.com/doc/current/components/security/authorization.html
+.. _`Security Chapter`: http://www.symfony.com/doc/current/book/security.html
+.. _`ACL checks`: http://www.symfony.com/doc/current/cookbook/security/acl.html
+.. _`Sonata Admin extension documentation`: http://sonata-project.org/bundles/admin/master/doc/reference/extensions.html
.. _`synchronized service`: http://symfony.com/doc/current/cookbook/service_container/scopes.html#using-a-synchronized-service
diff --git a/bundles/routing.rst b/bundles/routing.rst
index 424e5a31..35c4c739 100644
--- a/bundles/routing.rst
+++ b/bundles/routing.rst
@@ -101,6 +101,8 @@ will look like this
See also official Symfony2 `documentation for DependencyInjection tags`_
+.. _bundles-routing-dynamic_router:
+
Dynamic Router
--------------