diff --git a/.php_cs b/.php_cs
new file mode 100644
index 0000000..926df8c
--- /dev/null
+++ b/.php_cs
@@ -0,0 +1,10 @@
+in(__DIR__)
+;
+
+return Symfony\CS\Config\Config::create()
+ ->fixers(array('-psr0'))
+ ->finder($finder)
+;
\ No newline at end of file
diff --git a/Controller/SabreDavController.php b/Controller/SabreDavController.php
index 28e4d76..0960373 100644
--- a/Controller/SabreDavController.php
+++ b/Controller/SabreDavController.php
@@ -14,12 +14,12 @@
use Sabre\DAV\Server;
use Secotrust\Bundle\SabreDavBundle\SabreDav\HttpRequest;
use Secotrust\Bundle\SabreDavBundle\SabreDav\HttpResponse;
-use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\StreamedResponse;
+use Symfony\Component\Routing\RouterInterface;
/**
- * Class SabreDavController
+ * Class SabreDavController.
*/
class SabreDavController
{
@@ -29,20 +29,16 @@ class SabreDavController
private $dav;
/**
- * @var EventDispatcherInterface
- */
- private $dispatcher;
-
- /**
- * Constructor
+ * Constructor.
*
- * @param Server $dav
- * @param EventDispatcherInterface $dispatcher
+ * @param Server $dav
+ * @param RouterInterface $router
*/
- public function __construct(Server $dav, EventDispatcherInterface $dispatcher)
+ public function __construct(Server $dav, RouterInterface $router, $base_uri = '')
{
+ $router->getContext()->setBaseUrl($router->getContext()->getBaseUrl() . $base_uri);
$this->dav = $dav;
- $this->dispatcher = $dispatcher; // TODO needed?
+ $this->dav->setBaseUri($router->generate('secotrust_sabre_dav', array()));
}
/**
diff --git a/DependencyInjection/Compiler/CollectionPass.php b/DependencyInjection/Compiler/CollectionPass.php
index 7812c6a..ec588b5 100644
--- a/DependencyInjection/Compiler/CollectionPass.php
+++ b/DependencyInjection/Compiler/CollectionPass.php
@@ -16,7 +16,7 @@
use Symfony\Component\DependencyInjection\Reference;
/**
- * Class CollectionPass
+ * Class CollectionPass.
*/
class CollectionPass implements CompilerPassInterface
{
diff --git a/DependencyInjection/Compiler/PluginPass.php b/DependencyInjection/Compiler/PluginPass.php
index 99ec3de..f3b09ea 100644
--- a/DependencyInjection/Compiler/PluginPass.php
+++ b/DependencyInjection/Compiler/PluginPass.php
@@ -17,7 +17,7 @@
use Symfony\Component\Filesystem\Filesystem;
/**
- * Class PluginPass
+ * Class PluginPass.
*/
class PluginPass implements CompilerPassInterface
{
diff --git a/DependencyInjection/Configuration.php b/DependencyInjection/Configuration.php
index afafee6..c24515a 100644
--- a/DependencyInjection/Configuration.php
+++ b/DependencyInjection/Configuration.php
@@ -15,36 +15,67 @@
use Symfony\Component\Config\Definition\ConfigurationInterface;
/**
- * Class Configuration
+ * Class Configuration.
*/
class Configuration implements ConfigurationInterface
{
/**
- * {@inheritDoc}
+ * {@inheritdoc}
*/
public function getConfigTreeBuilder()
{
$treeBuilder = new TreeBuilder();
$rootNode = $treeBuilder->root('secotrust_sabre_dav');
- // TODO needs "little" improvement ;-)
+ $default_base_uri = '/app_dev.php/remote';
$rootNode
->children()
- ->scalarNode('root_dir')->example('%kernel.root_dir%/../web/dav')->defaultNull()->end()
- ->scalarNode('base_uri')->example('/app_dev.php/dav/')->isRequired()->end()
+ ->scalarNode('root_dir')
+ ->example('%kernel.root_dir%/../web/dav/')
+ ->end()
+ ->scalarNode('browser_logo')
+ ->example('%kernel.root_dir%/../web/logo/sabredav.png')
+ ->defaultValue('')
+ ->end()
+ ->scalarNode('favicon')
+ ->example('%kernel.root_dir%/../web/logo/favicon.ico')
+ ->defaultValue('')
+ ->end()
+ ->scalarNode('security_service')
+ ->example('sabredav.security_service')
+ ->defaultValue('')
+ ->end()
+ ->scalarNode('base_uri')
+ ->example($default_base_uri)
+ ->end()
->arrayNode('plugins')
->addDefaultsIfNotSet()
->children()
->booleanNode('acl')->defaultFalse()->end()
->booleanNode('auth')->defaultFalse()->end()
->booleanNode('browser')->defaultFalse()->end()
- ->booleanNode('caldav')->defaultFalse()->end()
->booleanNode('lock')->defaultTrue()->end()
->booleanNode('temp')->defaultTrue()->end()
->booleanNode('mount')->defaultFalse()->end()
->booleanNode('patch')->defaultFalse()->end()
->booleanNode('content_type')->defaultFalse()->end()
+ ->booleanNode('webdav')->defaultFalse()->end()
+ ->booleanNode('principal')->defaultFalse()->end()
+ ->booleanNode('carddav')->defaultFalse()->end()
+ ->booleanNode('caldav')->defaultFalse()->end()
+ ->end()
+ ->end()
+ ->arrayNode('settings')
+ ->addDefaultsIfNotSet()
+ ->children()
+ ->scalarNode('cards_class')->defaultValue('')->end()
+ ->scalarNode('addressbooks_class')->defaultValue('')->end()
+ ->scalarNode('calendarobjects_class')->defaultValue('')->end()
+ ->scalarNode('calendar_class')->defaultValue('')->end()
+ ->scalarNode('principals_class')->defaultValue('')->end()
+ ->scalarNode('principalgroups_class')->defaultValue('')->end()
+ ->end()
->end()
->end()
->end();
diff --git a/DependencyInjection/SecotrustSabreDavExtension.php b/DependencyInjection/SecotrustSabreDavExtension.php
index fd1bbb1..db58cbe 100644
--- a/DependencyInjection/SecotrustSabreDavExtension.php
+++ b/DependencyInjection/SecotrustSabreDavExtension.php
@@ -17,12 +17,12 @@
use Symfony\Component\DependencyInjection\Loader;
/**
- * Class SecotrustSabreDavExtension
+ * Class SecotrustSabreDavExtension.
*/
class SecotrustSabreDavExtension extends Extension
{
/**
- * {@inheritDoc}
+ * {@inheritdoc}
*/
public function load(array $configs, ContainerBuilder $container)
{
@@ -32,20 +32,40 @@ public function load(array $configs, ContainerBuilder $container)
$loader = new Loader\XmlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));
$loader->load('services/services.xml');
- if ($config['base_uri']) {
+ if (isset($config['base_uri'])) {
$container->getDefinition('secotrust.sabredav.server')->addMethodCall('setBaseUri', array($config['base_uri']));
+ $container->setParameter('secotrust.sabredav.base_uri', $config['base_uri']);
}
- if ($config['root_dir']) {
- $container->getDefinition('secotrust.sabredav_root')->replaceArgument(0, $config['root_dir']);
- } else {
- $container->getDefinition('secotrust.sabredav_root')->clearTag('secotrust.sabredav.collection');
- }
-
+ // load all plugins
foreach ($config['plugins'] as $plugin => $enabled) {
if ($enabled) {
$loader->load(sprintf('services/plugins/%s.xml', $plugin));
}
}
+
+ // no root dir is set, but webdav plugin is active: throw exception
+ if (!empty($config['root_dir']) && $config['plugins']['webdav']) {
+ //replace argument
+ $container->getDefinition('secotrust.sabredav_root')->replaceArgument(0, $config['root_dir']);
+ }
+
+ // add logo to browser-plugin
+ if ($config['plugins']['browser']) {
+ $container->setParameter('secotrust.sabredav.browser_plugin.logo', $config['browser_logo']);
+ $container->setParameter('secotrust.sabredav.browser_plugin.favicon', $config['favicon']);
+ }
+
+ // add security-service-class
+ if ($config['security_service']) {
+ $container->setParameter('secotrust.sabredav.acl.securityService', $config['security_service']);
+ }
+
+ $container->setParameter('secotrust.cards_class', $config['settings']['cards_class']);
+ $container->setParameter('secotrust.addressbooks_class', $config['settings']['addressbooks_class']);
+ $container->setParameter('secotrust.calendarobjects_class', $config['settings']['calendarobjects_class']);
+ $container->setParameter('secotrust.calendar_class', $config['settings']['calendar_class']);
+ $container->setParameter('secotrust.principals_class', $config['settings']['principals_class']);
+ $container->setParameter('secotrust.principalgroups_class', $config['settings']['principalgroups_class']);
}
}
diff --git a/Entity/AddressbookInterface.php b/Entity/AddressbookInterface.php
new file mode 100644
index 0000000..365e248
--- /dev/null
+++ b/Entity/AddressbookInterface.php
@@ -0,0 +1,114 @@
+
+ * and use the desired setting, if you want to delete the cards
+ * - either only from the current Addressbook
+ * - or from the cards-table too
+ *
+ * @param \Secotrust\Bundle\SabreDavBundle\Entity\CardInterface $card
+ *
+ * @return bool
+ */
+ public function removeCard(CardInterface $card);
+
+ /**
+ * Remove all Cards from current Addressbook.
+ *
+ * Possible solution: use $cards = $this->getCards() and
+ * $this->removeCard($card) to remove all cards
+ */
+ public function removeAllCards();
+}
diff --git a/Entity/CardInterface.php b/Entity/CardInterface.php
new file mode 100644
index 0000000..97147f2
--- /dev/null
+++ b/Entity/CardInterface.php
@@ -0,0 +1,88 @@
+getVCard());
+ */
+ public function getETag();
+}
diff --git a/Entity/PrincipalInterface.php b/Entity/PrincipalInterface.php
new file mode 100644
index 0000000..4c6d723
--- /dev/null
+++ b/Entity/PrincipalInterface.php
@@ -0,0 +1,60 @@
+
-
+ secotrust.sabredav.controller:execAction
+ /?.*
+
diff --git a/Resources/config/services/plugins/acl.xml b/Resources/config/services/plugins/acl.xml
index 6433203..c557111 100644
--- a/Resources/config/services/plugins/acl.xml
+++ b/Resources/config/services/plugins/acl.xml
@@ -5,12 +5,30 @@
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
- Sabre\DAVACL\Plugin
+ Secotrust\Bundle\SabreDavBundle\SabreDav\AclPlugin
+ Secotrust\Bundle\SabreDavBundle\SabreDav\Acl\SecurityManager
+ true
+ true
+ principals
+
+
+
+ %sabredav.acl.hideNodesFromListings%
+
+
+ %sabredav.acl.accessToNodesWithoutACL%
+
+
+ %sabredav.acl.defaultUsernamePath%
+
+
+
+
diff --git a/Resources/config/services/plugins/auth.xml b/Resources/config/services/plugins/auth.xml
index 6417561..dd59029 100644
--- a/Resources/config/services/plugins/auth.xml
+++ b/Resources/config/services/plugins/auth.xml
@@ -7,15 +7,17 @@
Secotrust\Bundle\SabreDavBundle\SabreDav\AuthBackendSabre\DAV\Auth\Plugin
+ SabreDAV
-
+
+ %secotrust.sabredav.auth.realm%
- SabreDAV
+ %secotrust.sabredav.auth.realm%
diff --git a/Resources/config/services/plugins/browser.xml b/Resources/config/services/plugins/browser.xml
index 8a34d5f..87cbd8f 100644
--- a/Resources/config/services/plugins/browser.xml
+++ b/Resources/config/services/plugins/browser.xml
@@ -5,12 +5,19 @@
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
- Sabre\DAV\Browser\Plugin
+ Secotrust\Bundle\SabreDavBundle\SabreDav\BrowserPlugin
+
+ %secotrust.sabredav.browser_plugin.logo%
+ %secotrust.sabredav.browser_plugin.favicon%
+ false
+
+ %secotrust.sabredav.browser.config%
+
diff --git a/Resources/config/services/plugins/caldav.xml b/Resources/config/services/plugins/caldav.xml
index 43d2f8a..33aa0b2 100644
--- a/Resources/config/services/plugins/caldav.xml
+++ b/Resources/config/services/plugins/caldav.xml
@@ -5,12 +5,22 @@
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
+ Secotrust\Bundle\SabreDavBundle\SabreDav\CalDavBackendSabre\CalDAV\Plugin
+ Sabre\CalDAV\CalendarRoot
+
+
+
+
+
+
+
+
diff --git a/Resources/config/services/plugins/carddav.xml b/Resources/config/services/plugins/carddav.xml
new file mode 100644
index 0000000..34846da
--- /dev/null
+++ b/Resources/config/services/plugins/carddav.xml
@@ -0,0 +1,29 @@
+
+
+
+
+
+ Secotrust\Bundle\SabreDavBundle\SabreDav\CardDavBackend
+ Sabre\CardDAV\Plugin
+ Sabre\CardDAV\AddressBookRoot
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Resources/config/services/plugins/notifications.xml b/Resources/config/services/plugins/notifications.xml
new file mode 100644
index 0000000..a6761eb
--- /dev/null
+++ b/Resources/config/services/plugins/notifications.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+ Sabre\CalDAV\Notifications\Plugin
+
+
+
+
+
+
+
+
diff --git a/Resources/config/services/plugins/principal.xml b/Resources/config/services/plugins/principal.xml
new file mode 100644
index 0000000..1d7feb3
--- /dev/null
+++ b/Resources/config/services/plugins/principal.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+ Secotrust\Bundle\SabreDavBundle\SabreDav\PrincipalBackend
+ Sabre\DAVACL\Plugin
+ Sabre\DAVACL\PrincipalCollection
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Resources/config/services/plugins/schedule.xml b/Resources/config/services/plugins/schedule.xml
new file mode 100644
index 0000000..0a31be7
--- /dev/null
+++ b/Resources/config/services/plugins/schedule.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+ Sabre\CalDAV\Schedule\Plugin
+
+
+
+
+
+
+
+
diff --git a/Resources/config/services/plugins/webdav.xml b/Resources/config/services/plugins/webdav.xml
new file mode 100644
index 0000000..f85ce5e
--- /dev/null
+++ b/Resources/config/services/plugins/webdav.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+ Sabre\DAV\FS\Directory
+
+
+
+
+
+
+
+
+
diff --git a/Resources/config/services/services.xml b/Resources/config/services/services.xml
index 3493065..0892cf2 100644
--- a/Resources/config/services/services.xml
+++ b/Resources/config/services/services.xml
@@ -7,20 +7,22 @@
Secotrust\Bundle\SabreDavBundle\Controller\SabreDavControllerSabre\DAV\Server
- Sabre\DAV\FS\Directory
+
+
+
+
+
+
-
+
+ %secotrust.sabredav.base_uri%
-
-
-
-
-
+
diff --git a/Resources/doc/index.md b/Resources/doc/index.md
index 212f0a0..eb88f8c 100644
--- a/Resources/doc/index.md
+++ b/Resources/doc/index.md
@@ -1,3 +1,83 @@
# SecotrustSabreDavBundle #
This bundle is still WIP.
+
+## Setup
+First add this bundle to your composer dependencies:
+
+`> composer require secotrust/sabredav-bundle dev-master`
+
+Then register it in your AppKernel.php.
+
+```php
+class AppKernel extends Kernel
+{
+ public function registerBundles()
+ {
+ $bundles = array(
+ new Secotrust\Bundle\SabreDavBundle\SecotrustSabreDavBundle(),
+ // ...
+```
+
+Add DAV routes.
+
+```yaml
+# app/config/routing.yml
+dav:
+ resource: "@SecotrustSabreDavBundle/Resources/config/routing.xml"
+ prefix: dav
+```
+
+Define a service to use for file access tagged with `secotrust.sabredav.collection`.
+
+```yaml
+# app/config/services.yml
+services:
+ gaufrette.adapter:
+ class: Gaufrette\Adapter\Local
+ arguments:
+ - "%kernel.root_dir%/../var/uploads"
+ gaufrette.filesystem:
+ class: Gaufrette\Filesystem
+ arguments:
+ - @gaufrette.adapter
+ gaufrette.dav.collection:
+ class: Secotrust\Bundle\SabreDavBundle\SabreDav\Gaufrette\Collection
+ arguments:
+ - @gaufrette.filesystem
+ tags:
+ - { name: secotrust.sabredav.collection }
+```
+
+## Add principal Collection
+
+```yaml
+# app/config/config.yml
+secotrust_sabre_dav:
+ plugins:
+ #...
+ principal: true
+ settings:
+ principals_class: Symfony\Component\Security\Core\User\User
+```
+
+## DAV-Clients
+
+Full List of supported SabreDav Clients: [http://sabre.io/dav/clients/](http://sabre.io/dav/clients/)
+
+### Thunderbird
++ SogoConnector: [http://www.sogo.nu/downloads/frontends.html](http://www.sogo.nu/downloads/frontends.html)
+
+### Microsoft Outlook / Windows 8
++ [http://german.evomailserver.com/download.php](http://german.evomailserver.com/download.php): EVO Kollaborateur (Untested),
++ [https://help.atmail.com/hc/en-us/articles/200907874-Syncing-Outlook-Contacts-and-Calendars-with-Atmail-DavSync](https://help.atmail.com/hc/en-us/articles/200907874-Syncing-Outlook-Contacts-and-Calendars-with-Atmail-DavSync)
++ [http://forum.xda-developers.com/showthread.php?t=2478215](http://forum.xda-developers.com/showthread.php?t=2478215)
++ [http://www.outlookdav.com/](http://www.outlookdav.com/): no freeware
+
+### Mac OS X >= 10.8 / iOS >=7:
++ native implementation
+
+### Android Smartphones
++ [http://dmfs.org/carddav/](http://dmfs.org/carddav/)
++ [http://dmfs.org/calcav/](http://dmfs.org/caldav/)
+
diff --git a/SabreDav/Acl/SecurityManager.php b/SabreDav/Acl/SecurityManager.php
new file mode 100644
index 0000000..3f4fd40
--- /dev/null
+++ b/SabreDav/Acl/SecurityManager.php
@@ -0,0 +1,61 @@
+container = $container;
+ $this->token = $container->get('security.token_storage')->getToken();
+ $this->authorizationChecker = $container->get('security.authorization_checker');
+ }
+
+ /**
+ * returns the ACL list in the following format:
+ * return array('read', 'write', 'delete');.
+ *
+ * consider:
+ * null will be returned to tell the AclPlugin to use the default Node-Acl (e.g. if no ACL was found for this entry)
+ *
+ * an empty array will be returned, if the user has no permissions for the current object.
+ *
+ * @param string $username
+ * @param $objectClass
+ * @param $objectIdentifier
+ * @param null $groupIdentifier
+ */
+ public function getACL($username, $objectClass, $objectIdentifier, $groupIdentifier = null)
+ {
+ return;
+ }
+}
diff --git a/SabreDav/AclPlugin.php b/SabreDav/AclPlugin.php
new file mode 100644
index 0000000..a523077
--- /dev/null
+++ b/SabreDav/AclPlugin.php
@@ -0,0 +1,219 @@
+authChecker = $authChecker;
+ $this->container = $container;
+ }
+
+ /**
+ * By default nodes that are inaccessible by the user, can still be seen
+ * in directory listings (PROPFIND on parent with Depth: 1).
+ *
+ * @param bool $flag
+ */
+ public function setHideNodesFromListings($flag = false)
+ {
+ $this->hideNodesFromListings = (bool) $flag;
+ }
+
+ /**
+ * By default ACL is only enforced for nodes that have ACL support (the
+ * ones that implement IACL). For any other node, access is
+ * always granted.
+ *
+ * To override this behaviour you can turn this setting off. This is useful
+ * if you plan to fully support ACL in the entire tree.
+ *
+ * @param bool $flag
+ */
+ public function setAccessToNodesWithoutACL($flag = true)
+ {
+ $this->allowAccessToNodesWithoutACL = (bool) $flag;
+ }
+
+ /**
+ * This string is prepended to the username of the currently logged in
+ * user. This allows the plugin to determine the principal path based on
+ * the username.
+ *
+ * @param string $usernamePath
+ */
+ public function setDefaultUsernamePath($usernamePath = 'principals')
+ {
+ $this->defaultUsernamePath = $usernamePath;
+ }
+
+ /**
+ * add a principal to the admin-list to automatically receive {DAV:}all privileges.
+ *
+ * @param string $principal
+ *
+ * @return bool
+ */
+ public function addAdminPrincipal($principal)
+ {
+ if (strpos($principal, $this->defaultUsernamePath.'/') !== 0) {
+ $principal = $this->defaultUsernamePath.'/'.$principal;
+ }
+
+ if (!in_array($principal, $this->adminPrincipals)) {
+ $this->adminPrincipals[] = $principal;
+
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * remove principal from admin-list.
+ *
+ * @param string $principal
+ *
+ * @return bool
+ */
+ public function removeAdminPrincipal($principal)
+ {
+ if (strpos($principal, $this->defaultUsernamePath.'/') !== 0) {
+ $principal = $this->defaultUsernamePath.'/'.$principal;
+ }
+
+ if (false !== ($key = \array_search($principal, $this->adminPrincipals))) {
+ unset($this->adminPrincipals[$key]);
+
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Returns the full ACL list.
+ *
+ * Either a uri or a DAV\INode may be passed.
+ *
+ * null will be returned if the node doesn't support ACLs.
+ *
+ * @param string|\Sabre\DAV\INode $node
+ *
+ * @return array
+ */
+ public function getACL($node)
+ {
+ if (is_string($node)) {
+ $node = $this->server->tree->getNodeForPath($node);
+ }
+
+ if (!$node instanceof IACL) {
+ return;
+ }
+
+ $username = $this->server->httpRequest->getCurrentUsername();
+ $acl = array();
+
+ $this->davSecurity = $this->container->get('secotrust.sabredav_acl_securityManager');
+
+ if (!is_null($this->davSecurity) && (
+ $node instanceof \Sabre\CalDAV\Calendar ||
+ $node instanceof \Sabre\CalDAV\CalendarObject ||
+ $node instanceof \Sabre\CardDAV\AddressBook ||
+ $node instanceof \Sabre\CardDAV\Card
+ )) {
+ $objectClass = '';
+ $objectIdentifier = array('name' => $node->getName());
+
+ if ($node instanceof \Sabre\CardDAV\AddressBook) {
+ $objectClass = $this->container->getParameter('secotrust.addressbooks_class');
+ $objectIdentifier = $node->getProperties(['id']);
+ $this->groupNode = $objectIdentifier;
+ } elseif ($node instanceof \Sabre\CardDAV\Calendar) {
+ $objectClass = $this->container->getParameter('secotrust.calendar_class');
+ $this->groupNode = $objectIdentifier;
+ } elseif ($node instanceof \Sabre\CardDAV\CalendarObject) {
+ $objectClass = $this->container->getParameter('secotrust.calendarobject_class');
+ } elseif ($node instanceof \Sabre\CardDAV\Card) {
+ $objectClass = $this->container->getParameter('secotrust.cards_class');
+ }
+
+ // load the permission-list from the davSecurity-Service
+ $permissionList = $this->davSecurity->getACL(
+ $username, $objectClass, $objectIdentifier, $this->groupNode
+ );
+
+ if ($permissionList === null) {
+ // use the "default" ACL from the current node
+ $acl = $node->getACL();
+ } else {
+ // write permissions to DAV-ACL
+ foreach ($permissionList as $permission) {
+ $acl[] = array(
+ 'privilege' => '{DAV:}'.$permission,
+ 'principal' => $this->defaultUsernamePath.'/'.$username,
+ 'protected' => true,
+ );
+ }
+ }
+ } elseif (!($node instanceof \Sabre\DAVACL\Principal && $node->getName() !== $username)) {
+ // get node-acl; if node is a principal-node
+ // and the name is not like the current username, don't display the node-acl
+ $acl = $node->getACL();
+ }
+
+ // add admin-privileges for all adminPrincipals
+ foreach ($this->adminPrincipals as $adminPrincipal) {
+ $acl[] = array(
+ 'principal' => $adminPrincipal,
+ 'privilege' => '{DAV:}all',
+ 'protected' => true,
+ );
+ }
+
+ return $acl;
+ }
+}
diff --git a/SabreDav/Auth/BasicAuth.php b/SabreDav/Auth/BasicAuth.php
new file mode 100644
index 0000000..a72dd81
--- /dev/null
+++ b/SabreDav/Auth/BasicAuth.php
@@ -0,0 +1,102 @@
+user_manager = $user_manager;
+ parent::__construct($realm, $request, $response);
+ }
+
+ /**
+ * find username in the user-manager.
+ *
+ * @param string $username
+ *
+ * @return \FOS\UserBundle\Model\UserInterface
+ */
+ private function getUser($username)
+ {
+ $user = $this->user_manager->findUserByUsername($username);
+
+ return $user;
+ }
+
+ /**
+ * Return user-credentials; returned password is encoded via "security.encoder_factory".
+ *
+ * @param \Symfony\Component\Security\Core\Encoder\EncoderFactoryInterface $encoder_service
+ *
+ * @return array|bool
+ */
+ public function getCredentials(EncoderFactoryInterface $encoder_service = null)
+ {
+ if (($user = $this->request->getRawServerValue('PHP_AUTH_USER')) && ($pass = $this->request->getRawServerValue('PHP_AUTH_PW'))) {
+ $credentials = array($user, $pass);
+ } else {
+
+ // Most other webservers
+ $auth = $this->request->getHeader('Authorization');
+
+ // Apache could prefix environment variables with REDIRECT_ when urls
+ // are passed through mod_rewrite
+ if (!$auth) {
+ $auth = $this->request->getRawServerValue('REDIRECT_HTTP_AUTHORIZATION');
+ }
+
+ if (!$auth) {
+ return false;
+ }
+
+ if (strpos(strtolower($auth), 'basic') !== 0) {
+ return false;
+ }
+
+ $credentials = explode(':', base64_decode(substr($auth, 6)), 2);
+ }
+
+ $user = $this->getUser($credentials[0]);
+
+ if (!$user) {
+ return false;
+ }
+
+ if ($encoder_service === null) {
+ // don't return password, because it isn't encoded
+ return array($credentials[0], '');
+ }
+
+ $encoder = $encoder_service->getEncoder($user);
+ $encoded_pass = $encoder->encodePassword($credentials[1], $user->getSalt());
+
+ return array($credentials[0], $encoded_pass);
+ }
+}
diff --git a/SabreDav/AuthBackend.php b/SabreDav/AuthBackend.php
index b2b4ff1..696f1e7 100644
--- a/SabreDav/AuthBackend.php
+++ b/SabreDav/AuthBackend.php
@@ -13,43 +13,231 @@
use Sabre\DAV\Auth\Backend\BackendInterface;
use Sabre\DAV\Exception;
-use Sabre\DAV\Server;
-use Symfony\Component\Security\Core\SecurityContextInterface;
+use Sabre\HTTP\RequestInterface;
+use Sabre\HTTP\ResponseInterface;
+use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Symfony\Component\EventDispatcher\Event;
class AuthBackend implements BackendInterface
{
/**
- * @var SecurityContextInterface
+ * @var ContainerInterface
*/
- private $context;
+ private $container;
/**
- * Constructor
+ * @var \FOS\UserBundle\Model\UserManagerInterface
+ */
+ private $user_manager;
+
+ /**
+ * @var \Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface
+ */
+ private $token_storage;
+
+ /**
+ * @var \Symfony\Component\Serializer\Encoder\EncoderInterface
+ */
+ private $encoder_service;
+
+ /**
+ * @var \Symfony\Component\EventDispatcher\EventDispatcherInterface
+ */
+ private $dispatcher;
+
+ /**
+ * @var string
+ */
+ protected $currentUser;
+
+ /**
+ * Authentication Realm.
+ *
+ * The realm is often displayed by browser clients when showing the
+ * authentication dialog.
+ *
+ * @var string
+ */
+ protected $realm = 'SabreDAV';
+
+ /**
+ * This is the prefix that will be used to generate principal urls.
+ *
+ * @var string
+ */
+ protected $principalPrefix = 'principals/';
+
+ /**
+ * Constructor.
*
- * @param SecurityContextInterface $context
+ * @param ContainerInterface $container
+ * @param $realm
*/
- public function __construct(SecurityContextInterface $context)
+ public function __construct(ContainerInterface $container, $realm)
{
- $this->context = $context;
+ $this->container = $container;
+ $this->realm = $realm;
+
+ $this->user_manager = $this->container->get('fos_user.user_manager');
+ $this->token_storage = $this->container->get('security.token_storage');
+ $this->encoder_service = $this->container->get('security.encoder_factory');
+ $this->dispatcher = $this->container->get('event_dispatcher');
}
/**
- * @inheritdoc
+ * Checks if username and password are valid. (Checked by the FOSUserManager)
+ * Returns.
+ *
+ * @param $username
+ * @param $passwordHash
+ *
+ * @return bool
+ */
+ public function validateUserPass($username, $passwordHash)
+ {
+ $user = $this->user_manager->findUserByUsername($username);
+
+ if (is_null($user)) {
+ return false;
+ }
+
+ if ($passwordHash === $user->getPassword()) {
+ // $this->userLoginAction($user, $passwordHash);
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * add the symfony-login "manually".
+ *
+ * use the symfony token-storage for the generated UsernamePasswordToken
+ * to access the (logged in) user later (e.g. to check for roles or permissions)
+ *
+ * the given $passwordHash must match the encrypted password in the user-object
+ *
+ * before and after the generation/setting of the token, the events "secotrust.user_login.before"
+ * and "secotrust.user_login.after" are called, if some EventListeners are configured
+ *
+ * @param \FOS\UserBundle\Model\UserInterface $user
+ * @param $passwordHash
*/
- public function authenticate(Server $server, $realm)
+ private function userLoginAction(\FOS\UserBundle\Model\UserInterface $user, $passwordHash)
{
- if (null === $this->context->getToken()) {
- throw new Exception('The security token is NULL');
+ // call the pre-login-event
+ $event = new Event();
+ $this->dispatcher->dispatch('secotrust.user_login.before', $event);
+
+ if ($user->getPassword() !== $passwordHash) {
+ // stop the login-action, when the password doesn't match
+ return;
}
- return $this->context->getToken()->isAuthenticated();
+ $token = new UsernamePasswordToken($user, null, 'secured_area', $user->getRoles());
+ $this->token_storage->setToken($token);
+
+ // call the post-login-event
+ $event = new Event();
+ $this->dispatcher->dispatch('secotrust.user_login.after', $event);
}
/**
- * @inheritdoc
+ * {@inheritdoc}
*/
public function getCurrentUser()
{
- return $this->context->getToken()->getUsername();
+ return $this->currentUser;
+ }
+
+ /**
+ * When this method is called, the backend must check if authentication was
+ * successful.
+ *
+ * The returned value must be one of the following
+ *
+ * [true, "principals/username"]
+ * [false, "reason for failure"]
+ *
+ * If authentication was successful, it's expected that the authentication
+ * backend returns a so-called principal url.
+ *
+ * Examples of a principal url:
+ *
+ * principals/admin
+ * principals/user1
+ * principals/users/joe
+ * principals/uid/123457
+ *
+ * If you don't use WebDAV ACL (RFC3744) we recommend that you simply
+ * return a string such as:
+ *
+ * principals/users/[username]
+ *
+ * @param RequestInterface $request
+ * @param ResponseInterface $response
+ *
+ * @return array
+ *
+ * @throws Exception
+ */
+ public function check(RequestInterface $request, ResponseInterface $response)
+ {
+ $auth = new Auth\BasicAuth($this->realm, $request, $response, $this->user_manager);
+ $userpass = $auth->getCredentials($this->encoder_service);
+
+ // No username was given
+ if ($userpass === false) {
+ return [false, "No 'Authorization' header found. Either the client didn't send one, or the server is mis-configured"];
+ }
+
+ // Authenticates the user
+ if (!$this->validateUserPass($userpass[0], $userpass[1])) {
+ return [false, 'Username or password was incorrect'];
+ }
+
+ $this->currentUser = $userpass[0];
+ $request->setCurrentUsername($this->currentUser);
+
+ return [true, $this->principalPrefix.$userpass[0]];
+ }
+
+ /**
+ * This method is called when a user could not be authenticated, and
+ * authentication was required for the current request.
+ *
+ * This gives you the opportunity to set authentication headers. The 401
+ * status code will already be set.
+ *
+ * In this case of Basic Auth, this would for example mean that the
+ * following header needs to be set:
+ *
+ * $response->addHeader('WWW-Authenticate', 'Basic realm=SabreDAV');
+ *
+ * Keep in mind that in the case of multiple authentication backends, other
+ * WWW-Authenticate headers may already have been set, and you'll want to
+ * append your own WWW-Authenticate header instead of overwriting the
+ * existing one.
+ *
+ * @param RequestInterface $request
+ * @param ResponseInterface $response
+ */
+ public function challenge(RequestInterface $request, ResponseInterface $response)
+ {
+ $auth = new Auth\BasicAuth($this->realm, $request, $response, $this->user_manager);
+ $userpass = $auth->getCredentials($this->encoder_service);
+
+ if (!$userpass) {
+ $auth->requireLogin();
+ }
+
+ // Authenticates the user
+ if (!$this->validateUserPass($userpass[0], $userpass[1])) {
+ $auth->requireLogin();
+ }
+
+ $this->currentUser = $userpass[0];
+ $request->setCurrentUsername($this->currentUser);
}
}
diff --git a/SabreDav/BrowserPlugin.php b/SabreDav/BrowserPlugin.php
new file mode 100644
index 0000000..18e4227
--- /dev/null
+++ b/SabreDav/BrowserPlugin.php
@@ -0,0 +1,68 @@
+ $value) {
+ $this->config[$key] = $value;
+ }
+ }
+
+ /**
+ * @param string $key
+ *
+ * @return string
+ */
+ public function getBrowserConfig($key)
+ {
+ if (isset($this->config[$key])) {
+ return $this->config[$key];
+ }
+
+ return;
+ }
+
+ /**
+ * This method returns a local pathname to an asset.
+ *
+ * The logo and favicon can be overwritten
+ *
+ * @param string $assetName
+ *
+ * @return string
+ */
+ protected function getLocalAssetPath($assetName)
+ {
+ // load path to logo from parameters
+ if ($assetName === 'sabredav.png' && $this->getBrowserConfig('browser_logo')) {
+ return $this->getBrowserConfig('browser_logo');
+ } elseif ($assetName === 'favicon.ico' && $this->getBrowserConfig('favicon')) {
+ return $this->getBrowserConfig('favicon');
+ }
+
+ return parent::getLocalAssetPath($assetName);
+ }
+}
diff --git a/SabreDav/CalDavBackend.php b/SabreDav/CalDavBackend.php
new file mode 100644
index 0000000..c14f853
--- /dev/null
+++ b/SabreDav/CalDavBackend.php
@@ -0,0 +1,339 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Secotrust\Bundle\SabreDavBundle\SabreDav;
+
+use Sabre\CalDAV\Backend\BackendInterface;
+use Sabre\DAV\Exception;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
+
+class CalDavBackend implements BackendInterface
+{
+ /**
+ * @var ContainerInterface
+ */
+ private $_em;
+
+ /**
+ * @var string
+ */
+ private $calendar_class;
+
+ /**
+ * @var string
+ */
+ private $calendarobjects_class;
+
+ /**
+ * Constructor
+ *
+ * @param ContainerInterface $container
+ */
+ public function __construct(ContainerInterface $container)
+ {
+ $this->_em = $container->get('doctrine')->getManager();
+
+ $this->calendar_class = $container->getParameter('secotrust.calendar_class');
+ $this->calendarobjects_class = $container->getParameter('secotrust.calendarobjects_class');
+ }
+
+ /**
+ * Returns a list of calendars for a principal.
+ *
+ * Every project is an array with the following keys:
+ * * id, a unique id that will be used by other functions to modify the
+ * calendar. This can be the same as the uri or a database key.
+ * * uri, which the basename of the uri with which the calendar is
+ * accessed.
+ * * principaluri. The owner of the calendar. Almost always the same as
+ * principalUri passed to this method.
+ *
+ * Furthermore it can contain webdav properties in clark notation. A very
+ * common one is '{DAV:}displayname'.
+ *
+ * @param string $principalUri
+ * @return array
+ */
+ public function getCalendarsForUser($principalUri)
+ {
+ return array($principalUri);
+ }
+
+ /**
+ * Creates a new calendar for a principal.
+ *
+ * If the creation was a success, an id must be returned that can be used to reference
+ * this calendar in other methods, such as updateCalendar.
+ *
+ * @param string $principalUri
+ * @param string $calendarUri
+ * @param array $properties
+ * @return void
+ */
+ public function createCalendar($principalUri, $calendarUri, array $properties)
+ {
+ return;
+ }
+
+ /**
+ * Updates properties for a calendar.
+ *
+ * The mutations array uses the propertyName in clark-notation as key,
+ * and the array value for the property value. In the case a property
+ * should be deleted, the property value will be null.
+ *
+ * This method must be atomic. If one property cannot be changed, the
+ * entire operation must fail.
+ *
+ * If the operation was successful, true can be returned.
+ * If the operation failed, false can be returned.
+ *
+ * Deletion of a non-existent property is always successful.
+ *
+ * Lastly, it is optional to return detailed information about any
+ * failures. In this case an array should be returned with the following
+ * structure:
+ *
+ * array(
+ * 403 => array(
+ * '{DAV:}displayname' => null,
+ * ),
+ * 424 => array(
+ * '{DAV:}owner' => null,
+ * )
+ * )
+ *
+ * In this example it was forbidden to update {DAV:}displayname.
+ * (403 Forbidden), which in turn also caused {DAV:}owner to fail
+ * (424 Failed Dependency) because the request needs to be atomic.
+ *
+ * @param mixed $calendarId
+ * @param $calendarId
+ * @param \Sabre\DAV\PropPatch $propPatch
+ * @return bool|array
+ */
+ public function updateCalendar($calendarId, \Sabre\DAV\PropPatch $propPatch)
+ {
+ return true;
+ }
+
+ /**
+ * Delete a calendar and all it's objects
+ *
+ * @param mixed $calendarId
+ * @return void
+ */
+ public function deleteCalendar($calendarId)
+ {
+ return;
+ }
+
+ /**
+ * Returns all calendar objects within a calendar.
+ *
+ * Every item contains an array with the following keys:
+ * * id - unique identifier which will be used for subsequent updates
+ * * calendardata - The iCalendar-compatible calendar data
+ * * uri - a unique key which will be used to construct the uri. This can be any arbitrary string.
+ * * lastmodified - a timestamp of the last modification time
+ * * etag - An arbitrary string, surrounded by double-quotes. (e.g.:
+ * ' "abcdef"')
+ * * calendarid - The calendarid as it was passed to this function.
+ * * size - The size of the calendar objects, in bytes.
+ *
+ * Note that the etag is optional, but it's highly encouraged to return for
+ * speed reasons.
+ *
+ * The calendardata is also optional. If it's not returned
+ * 'getCalendarObject' will be called later, which *is* expected to return
+ * calendardata.
+ *
+ * If neither etag or size are specified, the calendardata will be
+ * used/fetched to determine these numbers. If both are specified the
+ * amount of times this is needed is reduced by a great degree.
+ *
+ * @param mixed $calendarId
+ * @return array
+ */
+ public function getCalendarObjects($calendarId)
+ {
+ return array($calendarId);
+ }
+
+ /**
+ * Returns information from a single calendar object, based on it's object
+ * uri.
+ *
+ * The returned array must have the same keys as getCalendarObjects. The
+ * 'calendardata' object is required here though, while it's not required
+ * for getCalendarObjects.
+ *
+ * This method must return null if the object did not exist.
+ *
+ * @param mixed $calendarId
+ * @param string $objectUri
+ * @return array|null
+ */
+ public function getCalendarObject($calendarId, $objectUri)
+ {
+ return array($calendarId);
+ }
+
+ /**
+ * Returns a list of calendar objects.
+ *
+ * This method should work identical to getCalendarObject, but instead
+ * return all the calendar objects in the list as an array.
+ *
+ * If the backend supports this, it may allow for some speed-ups.
+ *
+ * @param mixed $calendarId
+ * @param array $uris
+ * @return array
+ */
+ function getMultipleCalendarObjects($calendarId, array $uris)
+ {
+ return array($calendarId);
+ }
+
+ /**
+ * Creates a new calendar object.
+ *
+ * It is possible return an etag from this function, which will be used in
+ * the response to this PUT request. Note that the ETag must be surrounded
+ * by double-quotes.
+ *
+ * However, you should only really return this ETag if you don't mangle the
+ * calendar-data. If the result of a subsequent GET to this object is not
+ * the exact same as this request body, you should omit the ETag.
+ *
+ * @param mixed $calendarId
+ * @param string $objectUri
+ * @param string $calendarData
+ * @return string|null
+ */
+ public function createCalendarObject($calendarId, $objectUri, $calendarData)
+ {
+ return null;
+ }
+
+ /**
+ * Updates an existing calendarobject, based on it's uri.
+ *
+ * It is possible return an etag from this function, which will be used in
+ * the response to this PUT request. Note that the ETag must be surrounded
+ * by double-quotes.
+ *
+ * However, you should only really return this ETag if you don't mangle the
+ * calendar-data. If the result of a subsequent GET to this object is not
+ * the exact same as this request body, you should omit the ETag.
+ *
+ * @param mixed $calendarId
+ * @param string $objectUri
+ * @param string $calendarData
+ * @return string|null
+ */
+ public function updateCalendarObject($calendarId, $objectUri, $calendarData)
+ {
+ return null;
+ }
+
+ /**
+ * Deletes an existing calendar object.
+ *
+ * @param mixed $calendarId
+ * @param string $objectUri
+ * @return void
+ */
+ public function deleteCalendarObject($calendarId, $objectUri)
+ {
+ return null;
+ }
+
+ /**
+ * Performs a calendar-query on the contents of this calendar.
+ *
+ * The calendar-query is defined in RFC4791 : CalDAV. Using the
+ * calendar-query it is possible for a client to request a specific set of
+ * object, based on contents of iCalendar properties, date-ranges and
+ * iCalendar component types (VTODO, VEVENT).
+ *
+ * This method should just return a list of (relative) urls that match this
+ * query.
+ *
+ * The list of filters are specified as an array. The exact array is
+ * documented by Sabre\CalDAV\CalendarQueryParser.
+ *
+ * Note that it is extremely likely that getCalendarObject for every path
+ * returned from this method will be called almost immediately after. You
+ * may want to anticipate this to speed up these requests.
+ *
+ * This method provides a default implementation, which parses *all* the
+ * iCalendar objects in the specified calendar.
+ *
+ * This default may well be good enough for personal use, and calendars
+ * that aren't very large. But if you anticipate high usage, big calendars
+ * or high loads, you are strongly adviced to optimize certain paths.
+ *
+ * The best way to do so is override this method and to optimize
+ * specifically for 'common filters'.
+ *
+ * Requests that are extremely common are:
+ * * requests for just VEVENTS
+ * * requests for just VTODO
+ * * requests with a time-range-filter on either VEVENT or VTODO.
+ *
+ * ..and combinations of these requests. It may not be worth it to try to
+ * handle every possible situation and just rely on the (relatively
+ * easy to use) CalendarQueryValidator to handle the rest.
+ *
+ * Note that especially time-range-filters may be difficult to parse. A
+ * time-range filter specified on a VEVENT must for instance also handle
+ * recurrence rules correctly.
+ * A good example of how to interprete all these filters can also simply
+ * be found in Sabre\CalDAV\CalendarQueryFilter. This class is as correct
+ * as possible, so it gives you a good idea on what type of stuff you need
+ * to think of.
+ *
+ * @param mixed $calendarId
+ * @param array $filters
+ * @return array
+ */
+ public function calendarQuery($calendarId, array $filters)
+ {
+ return array($calendarId);
+ }
+
+ /**
+ * Searches through all of a users calendars and calendar objects to find
+ * an object with a specific UID.
+ *
+ * This method should return the path to this object, relative to the
+ * calendar home, so this path usually only contains two parts:
+ *
+ * calendarpath/objectpath.ics
+ *
+ * If the uid is not found, return null.
+ *
+ * This method should only consider * objects that the principal owns, so
+ * any calendars owned by other principals that also appear in this
+ * collection should be ignored.
+ *
+ * @param string $principalUri
+ * @param string $uid
+ * @return string|null
+ */
+ function getCalendarObjectByUID($principalUri, $uid)
+ {
+ return null;
+ }
+}
diff --git a/SabreDav/CardDavBackend.php b/SabreDav/CardDavBackend.php
new file mode 100644
index 0000000..aea5129
--- /dev/null
+++ b/SabreDav/CardDavBackend.php
@@ -0,0 +1,536 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Secotrust\Bundle\SabreDavBundle\SabreDav;
+
+use Sabre\CardDAV\Backend\AbstractBackend;
+use Sabre\CardDAV\Backend\SyncSupport;
+use Sabre\CardDAV;
+use Sabre\DAV\Exception\BadRequest;
+use Secotrust\Bundle\SabreDavBundle\Entity\CardInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+class CardDavBackend extends AbstractBackend implements SyncSupport
+{
+ /**
+ * @var \Doctrine\ORM\EntityManager
+ */
+ private $_em;
+
+ /**
+ * @var string
+ */
+ private $addressbooks_class;
+
+ /**
+ * @var string
+ */
+ private $cards_class;
+
+ /**
+ * Create array with Card-Data.
+ *
+ * @param CardInterface $entity
+ * @param bool $show_id
+ *
+ * @return array|bool
+ */
+ private function getCardArray($entity, $show_id = false)
+ {
+ if (!($entity instanceof CardInterface)) {
+ return false;
+ }
+
+ $card = array(
+ 'id' => $entity->getId(),
+ 'carddata' => $entity->getVCard(),
+ 'uri' => $entity->getVCardUid().'.vcf',
+ 'lastmodified' => $entity->getLastmodified(),
+ 'size' => strlen($entity->getVCard()),
+ 'etag' => $entity->getETag(),
+ );
+
+ if ($show_id === false) {
+ unset($card['id']);
+ }
+
+ return $card;
+ }
+
+ /**
+ * Constructor.
+ *
+ * @param ContainerInterface $container
+ */
+ public function __construct(ContainerInterface $container)
+ {
+ $this->_em = $container->get('doctrine')->getManager();
+
+ $this->addressbooks_class = $container->getParameter('secotrust.addressbooks_class');
+ $this->cards_class = $container->getParameter('secotrust.cards_class');
+ }
+
+ /**
+ * Returns the list of addressbooks for a specific user.
+ *
+ * Every addressbook should have the following properties:
+ * id - an arbitrary unique id
+ * uri - the 'basename' part of the url
+ * principaluri - Same as the passed parameter
+ *
+ * Any additional clark-notation property may be passed besides this. Some
+ * common ones are :
+ * {DAV:}displayname
+ * {urn:ietf:params:xml:ns:carddav}addressbook-description
+ * {http://calendarserver.org/ns/}getctag
+ *
+ * @param string $principalUri
+ *
+ * @return array
+ */
+ public function getAddressBooksForUser($principalUri)
+ {
+ $addressBooks = array();
+
+ $entities = $this->_em->getRepository($this->addressbooks_class)->findAllPrincipalAddressbooks($principalUri);
+
+ foreach ($entities as $entity) {
+ $addressBooks[] = array(
+ 'id' => $entity->getId(),
+ 'uri' => $entity->getUriLabel(),
+ 'principaluri' => $principalUri,
+ '{DAV:}displayname' => $entity->getLabel(),
+ '{'.CardDAV\Plugin::NS_CARDDAV.'}addressbook-description' => $entity->getDescription(),
+ '{http://calendarserver.org/ns/}getctag' => $entity->getSyncToken(),
+ '{http://sabredav.org/ns}sync-token' => $entity->getSyncToken(),
+ );
+ }
+
+ return $addressBooks;
+ }
+
+ /**
+ * Updates properties for an address book.
+ *
+ * The list of mutations is stored in a Sabre\DAV\PropPatch object.
+ * To do the actual updates, you must tell this object which properties
+ * you're going to process with the handle() method.
+ *
+ * Calling the handle method is like telling the PropPatch object "I
+ * promise I can handle updating this property".
+ *
+ * Read the PropPatch documenation for more info and examples.
+ *
+ * @param string $addressBookId
+ * @param \Sabre\DAV\PropPatch $propPatch
+ */
+ public function updateAddressBook($addressBookId, \Sabre\DAV\PropPatch $propPatch)
+ {
+ $supportedProperties = [
+ '{DAV:}displayname',
+ '{'.CardDAV\Plugin::NS_CARDDAV.'}addressbook-description',
+ ];
+
+ $addressbook = $this->_em->getRepository($this->addressbooks_class)->find($addressBookId);
+
+ if (!$addressbook) {
+ return;
+ }
+
+ $propPatch->handle($supportedProperties, function ($mutations) use ($addressbook) {
+
+ $updates = [];
+ foreach ($mutations as $property => $newValue) {
+ switch ($property) {
+ case '{DAV:}displayname' :
+ $updates['setLabel'] = $newValue;
+ break;
+ case '{'.CardDAV\Plugin::NS_CARDDAV.'}addressbook-description' :
+ $updates['setDescription'] = $newValue;
+ break;
+ }
+ }
+
+ foreach ($updates as $setter => $value) {
+ if (method_exists($addressbook, $setter)) {
+ $addressbook->$setter($value);
+ }
+ }
+
+ $this->_em->persist($addressbook);
+ $this->_em->flush();
+
+ return true;
+ });
+ }
+
+ /**
+ * Creates a new address book.
+ *
+ * @param string $principalUri
+ * @param string $url Just the 'basename' of the url.
+ * @param array $properties
+ *
+ * @throws BadRequest
+ */
+ public function createAddressBook($principalUri, $url, array $properties)
+ {
+ $values = array(
+ 'setLabel' => null,
+ 'setDescription' => null,
+ 'principaluri' => $principalUri,
+ 'uri' => $url,
+ );
+
+ foreach ($properties as $property => $newValue) {
+ switch ($property) {
+ case '{DAV:}displayname' :
+ $values['setLabel'] = $newValue;
+ break;
+ case '{'.CardDAV\Plugin::NS_CARDDAV.'}addressbook-description' :
+ $values['setDescription'] = $newValue;
+ break;
+ default :
+ throw new BadRequest('Unknown property: '.$property);
+ }
+ }
+
+ // check if current addressbooks-class can be instantiated
+ if ((new \ReflectionClass($this->addressbooks_class))->isAbstract()) {
+ return;
+ }
+
+ $addressbook = new $this->addressbooks_class();
+
+ foreach ($values as $setter => $value) {
+ if (method_exists($addressbook, $setter)) {
+ $addressbook->$setter($value);
+ }
+ }
+
+ $this->_em->persist($addressbook);
+ $this->_em->flush();
+
+ return $addressbook->getId();
+ }
+
+ /**
+ * Deletes an entire addressbook and all its contents.
+ *
+ * @param mixed $addressBookId
+ */
+ public function deleteAddressBook($addressBookId)
+ {
+ $addressbook = $this->_em->getRepository($this->addressbooks_class)->find($addressBookId);
+
+ if (!$addressbook) {
+ return;
+ }
+
+ $addressbook->removeAllCards();
+ $this->_em->delete($addressbook);
+ }
+
+ /**
+ * Returns all cards for a specific addressbook id.
+ *
+ * This method should return the following properties for each card:
+ * * carddata - raw vcard data
+ * * uri - Some unique url
+ * * lastmodified - A unix timestamp
+ *
+ * It's recommended to also return the following properties:
+ * * etag - A unique etag. This must change every time the card changes.
+ * * size - The size of the card in bytes.
+ *
+ * If these last two properties are provided, less time will be spent
+ * calculating them. If they are specified, you can also ommit carddata.
+ * This may speed up certain requests, especially with large cards.
+ *
+ * @param mixed $addressbookId
+ *
+ * @return array
+ */
+ public function getCards($addressbookId)
+ {
+ $contactGroup = $this->_em->getRepository($this->addressbooks_class)->findOneById($addressbookId);
+ $entities = $contactGroup->getContactCollection();
+
+ $cards = array();
+ foreach ($entities as $entity) {
+ $cards[] = $this->getCardArray($entity, true);
+ }
+
+ return $cards;
+ }
+
+ /**
+ * Returns a specfic card.
+ *
+ * The same set of properties must be returned as with getCards. The only
+ * exception is that 'carddata' is absolutely required.
+ *
+ * @param mixed $addressBookId
+ * @param string $cardUri
+ *
+ * @return array
+ */
+ public function getCard($addressBookId, $cardUri)
+ {
+ $entity = $this->_em->getRepository($this->cards_class)->findSingleCardByUid($cardUri, $addressBookId);
+
+ return $this->getCardArray($entity, true);
+ }
+
+ /**
+ * Creates a new card.
+ *
+ * The addressbook id will be passed as the first argument. This is the
+ * same id as it is returned from the getAddressbooksForUser method.
+ *
+ * The cardUri is a base uri, and doesn't include the full path. The
+ * cardData argument is the vcard body, and is passed as a string.
+ *
+ * It is possible to return an ETag from this method. This ETag is for the
+ * newly created resource, and must be enclosed with double quotes (that
+ * is, the string itself must contain the double quotes).
+ *
+ * You should only return the ETag if you store the carddata as-is. If a
+ * subsequent GET request on the same card does not have the same body,
+ * byte-by-byte and you did return an ETag here, clients tend to get
+ * confused.
+ *
+ * If you don't return an ETag, you can just return null.
+ *
+ * @param mixed $addressBookId
+ * @param string $cardUri
+ * @param string $cardData
+ *
+ * @return string|null
+ */
+ public function createCard($addressBookId, $cardUri, $cardData)
+ {
+ $addressbook = $this->_em->getRepository($this->addressbooks_class)->find($addressBookId);
+
+ if ((new \ReflectionClass($this->cards_class))->isAbstract()) {
+ return;
+ }
+
+ $card = new $this->cards_class();
+ $card->setVCard($cardData);
+ $card->setVCardUid($cardUri);
+ $addressbook->addCard($card);
+
+ $this->_em->persist($card);
+ $this->_em->flush();
+
+ return $card->getETag();
+ }
+
+ /**
+ * Updates a card.
+ *
+ * The addressbook id will be passed as the first argument. This is the
+ * same id as it is returned from the getAddressbooksForUser method.
+ *
+ * The cardUri is a base uri, and doesn't include the full path. The
+ * cardData argument is the vcard body, and is passed as a string.
+ *
+ * It is possible to return an ETag from this method. This ETag should
+ * match that of the updated resource, and must be enclosed with double
+ * quotes (that is: the string itself must contain the actual quotes).
+ *
+ * You should only return the ETag if you store the carddata as-is. If a
+ * subsequent GET request on the same card does not have the same body,
+ * byte-by-byte and you did return an ETag here, clients tend to get
+ * confused.
+ *
+ * If you don't return an ETag, you can just return null.
+ *
+ * @param mixed $addressBookId
+ * @param string $cardUri
+ * @param string $cardData
+ *
+ * @return string|null
+ */
+ public function updateCard($addressBookId, $cardUri, $cardData)
+ {
+ $card = $this->_em->getRepository($this->cards_class)->findSingleCardByUid($cardUri, $addressBookId);
+
+ if (!$card) {
+ return;
+ }
+
+ $card->setVCard($cardData);
+
+ $this->_em->flush();
+
+ return $card->getEtag();
+ }
+
+ /**
+ * Deletes a card.
+ *
+ * @param mixed $addressBookId
+ * @param string $cardUri
+ *
+ * @return bool
+ */
+ public function deleteCard($addressBookId, $cardUri)
+ {
+ $addressbook = $this->_em->getRepository($this->addressbooks_class)->find($addressBookId);
+
+ $card = $addressbook->findCard($cardUri);
+
+ if ($card instanceof CardInterface) {
+ return $addressbook->removeCard($card);
+ }
+
+ return false;
+ }
+
+ /**
+ * The getChanges method returns all the changes that have happened, since
+ * the specified syncToken in the specified address book.
+ *
+ * This function should return an array, such as the following:
+ *
+ * [
+ * 'syncToken' => 'The current synctoken',
+ * 'added' => [
+ * 'new.txt',
+ * ],
+ * 'modified' => [
+ * 'modified.txt',
+ * ],
+ * 'deleted' => [
+ * 'foo.php.bak',
+ * 'old.txt'
+ * ]
+ * ];
+ *
+ * The returned syncToken property should reflect the *current* syncToken
+ * of the calendar, as reported in the {http://sabredav.org/ns}sync-token
+ * property. This is needed here too, to ensure the operation is atomic.
+ *
+ * If the $syncToken argument is specified as null, this is an initial
+ * sync, and all members should be reported.
+ *
+ * The modified property is an array of nodenames that have changed since
+ * the last token.
+ *
+ * The deleted property is an array with nodenames, that have been deleted
+ * from collection.
+ *
+ * The $syncLevel argument is basically the 'depth' of the report. If it's
+ * 1, you only have to report changes that happened only directly in
+ * immediate descendants. If it's 2, it should also include changes from
+ * the nodes below the child collections. (grandchildren)
+ *
+ * The $limit argument allows a client to specify how many results should
+ * be returned at most. If the limit is not specified, it should be treated
+ * as infinite.
+ *
+ * If the limit (infinite or not) is higher than you're willing to return,
+ * you should throw a Sabre\DAV\Exception\TooMuchMatches() exception.
+ *
+ * If the syncToken is expired (due to data cleanup) or unknown, you must
+ * return null.
+ *
+ * The limit is 'suggestive'. You are free to ignore it.
+ *
+ * @param string $addressBookId
+ * @param string $syncToken
+ * @param int $syncLevel
+ * @param int $limit
+ *
+ * @return array
+ */
+ public function getChangesForAddressBook($addressBookId, $syncToken, $syncLevel, $limit = null)
+ {
+
+ // the "Doctrine2 behavioral extensions" (https://github.com/Atlantic18/DoctrineExtensions)
+ // are used to log the addressbook-changes
+ $loggableClass = 'Gedmo\Loggable\Entity\LogEntry';
+
+ if (!class_exists($loggableClass)) {
+ return;
+ }
+
+ /* @var $addressbook \Secotrust\Bundle\SabreDavBundle\Entity\AddressbookInterface */
+ $addressbook = $this->_em->getRepository($this->addressbooks_class)->find($addressBookId);
+
+ if ($addressbook->getSynctoken() === 0) {
+ return;
+ }
+
+ $result = [
+ 'syncToken' => $addressbook->getSynctoken(),
+ 'added' => [],
+ 'modified' => [],
+ 'deleted' => [],
+ ];
+
+ if ($syncToken) {
+
+ // Fetching all changes
+ $repo = $this->_em->getRepository($loggableClass);
+ $logs = $repo->getLogEntries($addressbook);
+
+ $changes = [];
+
+ // This loop ensures that any duplicates are overwritten, only the
+ // last change on a node is relevant.
+ foreach ($logs as $log) {
+ $changes[$addressbook->getUri()] = $log->getAction();
+ }
+
+ foreach ($changes as $uri => $operation) {
+ switch ($operation) {
+ case 'create':
+ $result['added'][] = $uri;
+ break;
+ case 'update':
+ $result['modified'][] = $uri;
+ break;
+ case 'remove':
+ $result['deleted'][] = $uri;
+ break;
+ }
+ }
+ } else {
+ // No synctoken supplied, this is the initial sync.
+ $result['added'] = $addressbook->getUri();
+ }
+
+ return $result;
+ }
+
+ /**
+ * Adds a change record to the addressbookchanges table.
+ *
+ * @param mixed $addressBookId
+ * @param string $objectUri
+ * @param int $operation 1 = add, 2 = modify, 3 = delete
+ */
+ protected function addChange($addressBookId, $objectUri, $operation)
+ {
+
+ // it is suggested to use the Loggable-Extension for Doctrine to manage
+ // the changes in the entities
+ // https://github.com/Atlantic18/DoctrineExtensions/blob/master/doc/loggable.md
+ //
+ // if the extension is configured in the right way, the changes are logged automatically
+ // configuration-example: https://github.com/Atlantic18/DoctrineExtensions/blob/master/doc/loggable.md#entity-mapping
+
+ return;
+ }
+}
diff --git a/SabreDav/Gaufrette/Collection.php b/SabreDav/Gaufrette/Collection.php
index 7bfbfdd..e71f738 100644
--- a/SabreDav/Gaufrette/Collection.php
+++ b/SabreDav/Gaufrette/Collection.php
@@ -29,7 +29,7 @@ class Collection extends BaseCollection
/**
* @param Filesystem $filesystem
- * @param string $prefix
+ * @param string $prefix
*/
public function __construct(Filesystem $filesystem, $prefix = '')
{
@@ -38,7 +38,7 @@ public function __construct(Filesystem $filesystem, $prefix = '')
}
/**
- * @inheritdoc
+ * {@inheritdoc}
*/
public function getChildren()
{
@@ -58,25 +58,25 @@ public function getChildren()
}
/**
- * @inheritdoc
+ * {@inheritdoc}
*/
public function getChild($name)
{
$key = $this->prefix.$name;
if (!$this->filesystem->has($key)) {
- throw new Exception\NotFound('The file with name: ' . $name . ' could not be found');
+ throw new Exception\NotFound('The file with name: '.$name.' could not be found');
}
if ($this->filesystem->getAdapter()->isDirectory($key)) {
- return new Collection($this->filesystem, $key.'/');
+ return new self($this->filesystem, $key.'/');
}
return new File($this->filesystem->get($key));
}
/**
- * @inheritdoc
+ * {@inheritdoc}
*/
public function childExists($name)
{
@@ -84,7 +84,7 @@ public function childExists($name)
}
/**
- * @inheritdoc
+ * {@inheritdoc}
*/
public function getName()
{
@@ -92,7 +92,7 @@ public function getName()
}
/**
- * @inheritdoc
+ * {@inheritdoc}
*/
public function getLastModified()
{
diff --git a/SabreDav/Gaufrette/File.php b/SabreDav/Gaufrette/File.php
index b2492a6..de28812 100644
--- a/SabreDav/Gaufrette/File.php
+++ b/SabreDav/Gaufrette/File.php
@@ -11,9 +11,7 @@
namespace Secotrust\Bundle\SabreDavBundle\SabreDav\Gaufrette;
-use Sabre\DAV\Exception;
use Sabre\DAV\File as BaseFile;
-use Sabre\DAV\Sabre;
class File extends BaseFile
{
@@ -23,7 +21,7 @@ class File extends BaseFile
protected $file;
/**
- * Constructor
+ * Constructor.
*
* @param \Gaufrette\File $file
*/
@@ -33,7 +31,7 @@ public function __construct(\Gaufrette\File $file)
}
/**
- * @inheritdoc
+ * {@inheritdoc}
*/
public function getName()
{
@@ -41,7 +39,7 @@ public function getName()
}
/**
- * @inheritdoc
+ * {@inheritdoc}
*/
public function getSize()
{
@@ -49,7 +47,7 @@ public function getSize()
}
/**
- * @inheritdoc
+ * {@inheritdoc}
*/
public function getLastModified()
{
@@ -57,7 +55,7 @@ public function getLastModified()
}
/**
- * @inheritdoc
+ * {@inheritdoc}
*/
public function put($data)
{
@@ -65,7 +63,7 @@ public function put($data)
}
/**
- * @inheritdoc
+ * {@inheritdoc}
*/
public function get()
{
@@ -73,7 +71,7 @@ public function get()
}
/**
- * @inheritdoc
+ * {@inheritdoc}
*/
public function delete()
{
diff --git a/SabreDav/HttpRequest.php b/SabreDav/HttpRequest.php
index 35d822e..3d0f072 100644
--- a/SabreDav/HttpRequest.php
+++ b/SabreDav/HttpRequest.php
@@ -15,7 +15,7 @@
use Symfony\Component\HttpFoundation\Request;
/**
- * Class HttpRequest
+ * Class HttpRequest.
*/
class HttpRequest extends BaseRequest
{
@@ -25,14 +25,38 @@ class HttpRequest extends BaseRequest
private $request;
/**
- * Constructor
+ * @var string
+ */
+ private $currentUsername;
+
+ /**
+ * Constructor.
*
* @param Request $request
*/
public function __construct(Request $request)
{
- parent::__construct($request->server->all(), $request->request->all());
- $this->setBody($request->getContent(true), true);
- $this->request = $request; // TODO needed?
+ parent::__construct($request->getMethod(), $request->getRequestUri(), $request->headers->all(), $request->getContent(true));
+ $this->request = $request;
+ }
+
+ /**
+ * set the current username.
+ *
+ * @param string $username
+ */
+ public function setCurrentUsername($username)
+ {
+ $this->currentUsername = $username;
+ }
+
+ /**
+ * get the current username.
+ *
+ * @return string
+ */
+ public function getCurrentUsername()
+ {
+ return $this->currentUsername;
}
}
diff --git a/SabreDav/HttpResponse.php b/SabreDav/HttpResponse.php
index 9a12989..37bc79c 100644
--- a/SabreDav/HttpResponse.php
+++ b/SabreDav/HttpResponse.php
@@ -15,7 +15,7 @@
use Symfony\Component\HttpFoundation\StreamedResponse;
/**
- * Class HttpResponse
+ * Class HttpResponse.
*/
class HttpResponse extends BaseResponse
{
@@ -25,12 +25,13 @@ class HttpResponse extends BaseResponse
private $response;
/**
- * Constructor
+ * Constructor.
*
* @param StreamedResponse $response
*/
public function __construct(StreamedResponse $response)
{
- $this->response = $response; // TODO needed?
+ parent::__construct($response->getStatusCode(), $response->headers->all());
+ $this->response = $response;
}
}
diff --git a/SabreDav/PrincipalBackend.php b/SabreDav/PrincipalBackend.php
new file mode 100644
index 0000000..953ccf1
--- /dev/null
+++ b/SabreDav/PrincipalBackend.php
@@ -0,0 +1,426 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Secotrust\Bundle\SabreDavBundle\SabreDav;
+
+use Sabre\DAV\Exception;
+use Sabre\DAV\MkCol;
+use Sabre\DAVACL\PrincipalBackend\AbstractBackend;
+use Sabre\DAVACL\PrincipalBackend\CreatePrincipalSupport;
+use FOS\UserBundle\Model\UserInterface;
+use FOS\UserBundle\Model\GroupInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+class PrincipalBackend extends AbstractBackend implements CreatePrincipalSupport
+{
+ /**
+ * @var \Doctrine\ORM\EntityManager
+ */
+ private $_em;
+
+ /**
+ * @var \FOS\UserBundle\Model\UserManagerInterface
+ */
+ private $user_manager;
+
+ /**
+ * @var \FOS\UserBundle\Model\GroupManagerInterface
+ */
+ private $group_manager;
+
+ /**
+ * @var string
+ */
+ private $principals_class;
+
+ /**
+ * @var string
+ */
+ private $principalgroups_class;
+
+ /**
+ * A list of additional fields to support.
+ *
+ * @var array
+ */
+ protected $fieldMap = array(
+ /*
+ * This property can be used to display the users' real name.
+ */
+ '{DAV:}displayname' => array(
+ 'getter' => 'getUsername',
+ 'setter' => 'setUsername',
+ ),
+ /*
+ * This property is actually used by the CardDAV plugin, where it gets
+ * mapped to {http://calendarserver.orgi/ns/}me-card.
+ *
+ * The reason we don't straight-up use that property, is because
+ * me-card is defined as a property on the users' addressbook
+ * collection.
+ */
+ '{http://sabredav.org/ns}vcard-url' => array(
+ 'getter' => 'getVCardUrl',
+ 'setter' => 'setVCardUrl',
+ ),
+ /*
+ * This is the users' primary email-address.
+ */
+ '{http://sabredav.org/ns}email-address' => array(
+ 'getter' => 'getEmail',
+ 'setter' => 'setEmail',
+ ),
+ );
+
+ /**
+ * Constructor.
+ *
+ * @param ContainerInterface $container
+ */
+ public function __construct(ContainerInterface $container)
+ {
+ $this->_em = $container->get('doctrine')->getManager();
+ $this->principals_class = $container->getParameter('secotrust.principals_class');
+ $this->principalgroups_class = $container->getParameter('secotrust.principalgroups_class');
+ $this->user_manager = $container->get('fos_user.user_manager');
+
+ if ($container->has('fos_user.group_manager')) {
+ $this->group_manager = $container->get('fos_user.group_manager');
+ }
+ }
+
+ /**
+ * get Array with Principal-Data from User-Object.
+ *
+ * @param UserInterface|GroupInterface $principalObject
+ * @param bool $show_id
+ *
+ * @return array
+ *
+ * @throws Exception
+ */
+ private function getPrincipalArray($principalObject, $show_id = false)
+ {
+ if (!($principalObject instanceof UserInterface) && !($principalObject instanceof GroupInterface)) {
+ throw new Exception('$principalObject must be of type UserInterface of GroupInterface');
+ }
+
+ $principal = array();
+ if ($show_id) {
+ $principal['id'] = $principalObject->getId();
+ }
+
+ if ($principalObject instanceof UserInterface) {
+ $principal['uri'] = 'principals/'.$principalObject->getUsername();
+ } else {
+ $principal['uri'] = 'principals/'.$principalObject->getName();
+ }
+
+ // get all fields from $this->fieldMap, additional to 'uri' and 'id'
+ foreach ($this->fieldMap as $key => $value) {
+ if (!method_exists($principalObject, $value['getter'])) {
+ continue;
+ }
+
+ $valueGetter = call_user_func(array($principalObject, $value['getter']));
+
+ if ($valueGetter) {
+ $principal[$key] = $valueGetter;
+ }
+ }
+
+ return $principal;
+ }
+
+ /**
+ * Returns a list of principals based on a prefix.
+ *
+ * This prefix will often contain something like 'principals'. You are only
+ * expected to return principals that are in this base path.
+ *
+ * You are expected to return at least a 'uri' for every user, you can
+ * return any additional properties if you wish so. Common properties are:
+ * {DAV:}displayname
+ * {http://sabredav.org/ns}email-address - This is a custom SabreDAV
+ * field that's actually injected in a number of other properties. If
+ * you have an email address, use this property.
+ *
+ * @param string $prefixPath
+ *
+ * @return array
+ */
+ public function getPrincipalsByPrefix($prefixPath)
+ {
+ $userlist = $this->user_manager->findUsers();
+ $principals = array();
+
+ foreach ($userlist as $user) {
+
+ // due to the lack of the implementation of prefixes, return all users
+ $principals[] = $this->getPrincipalArray($user);
+ }
+
+ return $principals;
+ }
+
+ /**
+ * Returns a specific principal, specified by it's path.
+ * The returned structure should be the exact same as from
+ * getPrincipalsByPrefix.
+ *
+ * @param string $path
+ * @param bool $getObject
+ *
+ * @return array|GroupInterface|UserInterface|void
+ *
+ * @throws Exception
+ */
+ public function getPrincipalByPath($path, $getObject = false)
+ {
+ $name = str_replace('principals/', '', $path);
+
+ // get username from path-string, if string contains additional slashes (e.g. admin/calendar-proxy-read)
+ if (!(strpos($name, '/') === false)) {
+ $name = substr($name, 0, strpos($name, '/'));
+ }
+
+ $user = $this->user_manager->findUserByUsername($name);
+
+ if ($user === null) {
+ if (!$this->group_manager) {
+ return;
+ }
+
+ // search in group-manager
+ $group = $this->group_manager->findGroupByName($name);
+
+ if ($group === null) {
+ return;
+ }
+
+ if ($getObject === true) {
+ return $group;
+ }
+
+ return $this->getPrincipalArray($group, true);
+ }
+
+ if ($getObject === true) {
+ return $user;
+ }
+
+ return $this->getPrincipalArray($user, true);
+ }
+
+ /**
+ * Updates one ore more webdav properties on a principal.
+ *
+ * The list of mutations is stored in a Sabre\DAV\PropPatch object.
+ * To do the actual updates, you must tell this object which properties
+ * you're going to process with the handle() method.
+ *
+ * Calling the handle method is like telling the PropPatch object "I
+ * promise I can handle updating this property".
+ *
+ * Read the PropPatch documenation for more info and examples.
+ *
+ * @param string $path
+ * @param \Sabre\DAV\PropPatch $propPatch
+ */
+ public function updatePrincipal($path, \Sabre\DAV\PropPatch $propPatch)
+ {
+ $principal = $this->getPrincipalByPath($path, true);
+
+ if (empty($principal)) {
+ return;
+ }
+
+ $propPatch->handle(array_keys($this->fieldMap), function ($properties) use ($principal) {
+
+ foreach ($properties as $key => $value) {
+ $setter = $this->fieldMap[$key]['setter'];
+ $principal->$setter($value);
+ }
+
+ $this->_em->flush();
+
+ return true;
+ });
+ }
+
+ /**
+ * This method is used to search for principals matching a set of
+ * properties.
+ *
+ * This search is specifically used by RFC3744's principal-property-search
+ * REPORT.
+ *
+ * The actual search should be a unicode-non-case-sensitive search. The
+ * keys in searchProperties are the WebDAV property names, while the values
+ * are the property values to search on.
+ *
+ * By default, if multiple properties are submitted to this method, the
+ * various properties should be combined with 'AND'. If $test is set to
+ * 'anyof', it should be combined using 'OR'.
+ *
+ * This method should simply return an array with full principal uri's.
+ *
+ * If somebody attempted to search on a property the backend does not
+ * support, you should simply return 0 results.
+ *
+ * You can also just return 0 results if you choose to not support
+ * searching at all, but keep in mind that this may stop certain features
+ * from working.
+ *
+ * @param string $prefixPath
+ * @param array $searchProperties
+ * @param string $test
+ *
+ * @return array
+ */
+ public function searchPrincipals($prefixPath, array $searchProperties, $test = 'allof')
+ {
+ foreach ($searchProperties as $property => $value) {
+ switch ($property) {
+
+ case '{DAV:}displayname' :
+ $searchArray['email'] = $value;
+ break;
+ case '{http://sabredav.org/ns}email-address' :
+ $searchArray['email'] = $value;
+ break;
+ default :
+ // Unsupported property
+ return array();
+ }
+ }
+
+ $principals = $this->_em->getRepository($this->principals_class)->searchPrincipals($prefixPath, $searchArray, $test);
+
+ return $principals;
+ }
+
+ /**
+ * Returns the list of members for a group-principal.
+ *
+ * @param string $principal
+ *
+ * @return array
+ *
+ * @throws Exception
+ */
+ public function getGroupMemberSet($principal)
+ {
+ $groupMemberSet = array();
+
+ $principalObject = $this->getPrincipalByPath($principal, true);
+
+ if (!$principalObject) {
+ throw new Exception('Principal not found');
+ }
+
+ // add current principal to group-list
+ $principalArray = $this->getPrincipalArray($principalObject);
+ $groupMemberSet[] = $principalArray['uri'];
+
+ if ($this->principalgroups_class === '') {
+ // groups-class is not defined: return current principal as only group-member
+ return $groupMemberSet;
+ }
+
+ //TODO: list all group memberships for current group (FOSUserBundle)
+
+ return $groupMemberSet;
+ }
+
+ /**
+ * Returns the list of groups a principal is a member of (each element of the list contains a URI).
+ *
+ * @param string $principal
+ *
+ * @return array
+ */
+ public function getGroupMembership($principal)
+ {
+ $principal_data = $this->getPrincipalByPath($principal, true);
+
+ if (!$principal_data) {
+ return array();
+ }
+
+ $groupMembership = array($principal);
+
+ if ($this->principalgroups_class !== '') {
+ foreach ($principal_data->getGroups() as $group) {
+ $groupPrincipal = $this->getPrincipalArray($group);
+ $groupMembership[] = $groupPrincipal['uri'];
+ }
+ }
+
+ return $groupMembership;
+ }
+
+ /**
+ * Updates the list of group members for a group principal.
+ *
+ * The principals should be passed as a list of uri's.
+ *
+ * @param string $principal
+ * @param array $members
+ *
+ * @throws Exception
+ */
+ public function setGroupMemberSet($principal, array $members)
+ {
+ $groupPrincipal = $this->getPrincipalByPath($principal);
+
+ if (!$groupPrincipal || !($groupPrincipal instanceof GroupInterface)) {
+ throw new Exception('(Group-)Principal not found');
+ }
+
+ // check if update of user-groups is possible; break if no group-manager or principalgroups_class
+ if ($this->principalgroups_class === '' || !$this->group_manager) {
+ return;
+ }
+
+ $memberObjects = array($groupPrincipal);
+
+ foreach ($members as $memberUri) {
+ $memberObjects[] = $this->getPrincipalByPath($memberUri);
+ }
+
+ // TODO: Implement the addition/deletion of new/old members
+ }
+
+ /**
+ * Creates a new principal.
+ *
+ * This method receives a full path for the new principal. The mkCol object
+ * contains any additional webdav properties specified during the creation
+ * of the principal.
+ *
+ * @param string $path
+ * @param MkCol $mkCol
+ */
+ public function createPrincipal($path, MkCol $mkCol)
+ {
+
+ // create new user
+ $username = str_replace('principal/', '', $path);
+
+ $user = $this->user_manager->createUser();
+ $user->setUsername($username);
+ $user->setForename($username);
+
+ $this->_em->persist($user);
+ $this->_em->flush();
+ }
+}
diff --git a/SecotrustSabreDavBundle.php b/SecotrustSabreDavBundle.php
index 0cbc778..eb3c0ec 100644
--- a/SecotrustSabreDavBundle.php
+++ b/SecotrustSabreDavBundle.php
@@ -17,7 +17,7 @@
use Symfony\Component\DependencyInjection\ContainerBuilder;
/**
- * Class SecotrustSabreDavBundle
+ * Class SecotrustSabreDavBundle.
*/
class SecotrustSabreDavBundle extends Bundle
{
diff --git a/composer.json b/composer.json
index 0febc05..d2e9eac 100644
--- a/composer.json
+++ b/composer.json
@@ -22,20 +22,20 @@
],
"require": {
"php": ">=5.3.3",
- "symfony/framework-bundle": "~2.2",
- "sabre/dav": "1.8.*"
+ "symfony/framework-bundle": "~2.2 || ^3.0",
+ "friendsofsymfony/user-bundle": ">=1.3.5",
+ "sabre/dav": "~3.1.0"
},
"suggest": {
- "sabre/vobject": "~3.0.0",
- "knplabs/knp-gaufrette-bundle": "0.2.*"
+ "knplabs/knp-gaufrette-bundle": "0.2.*",
+ "gedmo/doctrine-extensions": "~2.3"
},
"autoload": {
- "psr-0": { "Secotrust\\Bundle\\SabreDavBundle": "" }
+ "psr-4": { "Secotrust\\Bundle\\SabreDavBundle\\": "" }
},
- "target-dir": "Secotrust/Bundle/SabreDavBundle",
"extra": {
"branch-alias": {
- "dev-master": "1.0.x-dev"
+ "dev-master": "2.0.x-dev"
}
}
}