You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

338 lines
9.5 KiB

9 years ago
10 years ago
10 years ago
10 years ago
  1. <?php
  2. /**
  3. * @copyright Copyright (c) 2016, ownCloud, Inc.
  4. *
  5. * @author Bjoern Schiessle <bjoern@schiessle.org>
  6. * @author Björn Schießle <bjoern@schiessle.org>
  7. * @author Joas Schilling <coding@schilljs.com>
  8. * @author Thomas Müller <thomas.mueller@tmit.eu>
  9. *
  10. * @license AGPL-3.0
  11. *
  12. * This code is free software: you can redistribute it and/or modify
  13. * it under the terms of the GNU Affero General Public License, version 3,
  14. * as published by the Free Software Foundation.
  15. *
  16. * This program is distributed in the hope that it will be useful,
  17. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  18. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  19. * GNU Affero General Public License for more details.
  20. *
  21. * You should have received a copy of the GNU Affero General Public License, version 3,
  22. * along with this program. If not, see <http://www.gnu.org/licenses/>
  23. *
  24. */
  25. namespace OCA\DAV\CardDAV;
  26. use OC\Accounts\AccountManager;
  27. use OCP\AppFramework\Http;
  28. use OCP\ICertificateManager;
  29. use OCP\ILogger;
  30. use OCP\IUser;
  31. use OCP\IUserManager;
  32. use Sabre\DAV\Client;
  33. use Sabre\DAV\Xml\Response\MultiStatus;
  34. use Sabre\DAV\Xml\Service;
  35. use Sabre\HTTP\ClientHttpException;
  36. use Sabre\VObject\Reader;
  37. class SyncService {
  38. /** @var CardDavBackend */
  39. private $backend;
  40. /** @var IUserManager */
  41. private $userManager;
  42. /** @var ILogger */
  43. private $logger;
  44. /** @var array */
  45. private $localSystemAddressBook;
  46. /** @var AccountManager */
  47. private $accountManager;
  48. /** @var string */
  49. protected $certPath;
  50. /**
  51. * SyncService constructor.
  52. *
  53. * @param CardDavBackend $backend
  54. * @param IUserManager $userManager
  55. * @param ILogger $logger
  56. * @param AccountManager $accountManager
  57. */
  58. public function __construct(CardDavBackend $backend, IUserManager $userManager, ILogger $logger, AccountManager $accountManager) {
  59. $this->backend = $backend;
  60. $this->userManager = $userManager;
  61. $this->logger = $logger;
  62. $this->accountManager = $accountManager;
  63. $this->certPath = '';
  64. }
  65. /**
  66. * @param string $url
  67. * @param string $userName
  68. * @param string $addressBookUrl
  69. * @param string $sharedSecret
  70. * @param string $syncToken
  71. * @param int $targetBookId
  72. * @param string $targetPrincipal
  73. * @param array $targetProperties
  74. * @return string
  75. * @throws \Exception
  76. */
  77. public function syncRemoteAddressBook($url, $userName, $addressBookUrl, $sharedSecret, $syncToken, $targetBookId, $targetPrincipal, $targetProperties) {
  78. // 1. create addressbook
  79. $book = $this->ensureSystemAddressBookExists($targetPrincipal, $targetBookId, $targetProperties);
  80. $addressBookId = $book['id'];
  81. // 2. query changes
  82. try {
  83. $response = $this->requestSyncReport($url, $userName, $addressBookUrl, $sharedSecret, $syncToken);
  84. } catch (ClientHttpException $ex) {
  85. if ($ex->getCode() === Http::STATUS_UNAUTHORIZED) {
  86. // remote server revoked access to the address book, remove it
  87. $this->backend->deleteAddressBook($addressBookId);
  88. $this->logger->info('Authorization failed, remove address book: ' . $url, ['app' => 'dav']);
  89. throw $ex;
  90. }
  91. }
  92. // 3. apply changes
  93. // TODO: use multi-get for download
  94. foreach ($response['response'] as $resource => $status) {
  95. $cardUri = basename($resource);
  96. if (isset($status[200])) {
  97. $vCard = $this->download($url, $userName, $sharedSecret, $resource);
  98. $existingCard = $this->backend->getCard($addressBookId, $cardUri);
  99. if ($existingCard === false) {
  100. $this->backend->createCard($addressBookId, $cardUri, $vCard['body']);
  101. } else {
  102. $this->backend->updateCard($addressBookId, $cardUri, $vCard['body']);
  103. }
  104. } else {
  105. $this->backend->deleteCard($addressBookId, $cardUri);
  106. }
  107. }
  108. return $response['token'];
  109. }
  110. /**
  111. * @param string $principal
  112. * @param string $id
  113. * @param array $properties
  114. * @return array|null
  115. * @throws \Sabre\DAV\Exception\BadRequest
  116. */
  117. public function ensureSystemAddressBookExists($principal, $id, $properties) {
  118. $book = $this->backend->getAddressBooksByUri($principal, $id);
  119. if (!is_null($book)) {
  120. return $book;
  121. }
  122. $this->backend->createAddressBook($principal, $id, $properties);
  123. return $this->backend->getAddressBooksByUri($principal, $id);
  124. }
  125. /**
  126. * Check if there is a valid certPath we should use
  127. *
  128. * @return string
  129. */
  130. protected function getCertPath() {
  131. // we already have a valid certPath
  132. if ($this->certPath !== '') {
  133. return $this->certPath;
  134. }
  135. /** @var ICertificateManager $certManager */
  136. $certManager = \OC::$server->getCertificateManager(null);
  137. $certPath = $certManager->getAbsoluteBundlePath();
  138. if (file_exists($certPath)) {
  139. $this->certPath = $certPath;
  140. }
  141. return $this->certPath;
  142. }
  143. /**
  144. * @param string $url
  145. * @param string $userName
  146. * @param string $addressBookUrl
  147. * @param string $sharedSecret
  148. * @return Client
  149. */
  150. protected function getClient($url, $userName, $sharedSecret) {
  151. $settings = [
  152. 'baseUri' => $url . '/',
  153. 'userName' => $userName,
  154. 'password' => $sharedSecret,
  155. ];
  156. $client = new Client($settings);
  157. $certPath = $this->getCertPath();
  158. $client->setThrowExceptions(true);
  159. if ($certPath !== '' && strpos($url, 'http://') !== 0) {
  160. $client->addCurlSetting(CURLOPT_CAINFO, $this->certPath);
  161. }
  162. return $client;
  163. }
  164. /**
  165. * @param string $url
  166. * @param string $userName
  167. * @param string $addressBookUrl
  168. * @param string $sharedSecret
  169. * @param string $syncToken
  170. * @return array
  171. */
  172. protected function requestSyncReport($url, $userName, $addressBookUrl, $sharedSecret, $syncToken) {
  173. $client = $this->getClient($url, $userName, $sharedSecret);
  174. $body = $this->buildSyncCollectionRequestBody($syncToken);
  175. $response = $client->request('REPORT', $addressBookUrl, $body, [
  176. 'Content-Type' => 'application/xml'
  177. ]);
  178. return $this->parseMultiStatus($response['body']);
  179. }
  180. /**
  181. * @param string $url
  182. * @param string $userName
  183. * @param string $sharedSecret
  184. * @param string $resourcePath
  185. * @return array
  186. */
  187. protected function download($url, $userName, $sharedSecret, $resourcePath) {
  188. $client = $this->getClient($url, $userName, $sharedSecret);
  189. return $client->request('GET', $resourcePath);
  190. }
  191. /**
  192. * @param string|null $syncToken
  193. * @return string
  194. */
  195. private function buildSyncCollectionRequestBody($syncToken) {
  196. $dom = new \DOMDocument('1.0', 'UTF-8');
  197. $dom->formatOutput = true;
  198. $root = $dom->createElementNS('DAV:', 'd:sync-collection');
  199. $sync = $dom->createElement('d:sync-token', $syncToken);
  200. $prop = $dom->createElement('d:prop');
  201. $cont = $dom->createElement('d:getcontenttype');
  202. $etag = $dom->createElement('d:getetag');
  203. $prop->appendChild($cont);
  204. $prop->appendChild($etag);
  205. $root->appendChild($sync);
  206. $root->appendChild($prop);
  207. $dom->appendChild($root);
  208. return $dom->saveXML();
  209. }
  210. /**
  211. * @param string $body
  212. * @return array
  213. * @throws \Sabre\Xml\ParseException
  214. */
  215. private function parseMultiStatus($body) {
  216. $xml = new Service();
  217. /** @var MultiStatus $multiStatus */
  218. $multiStatus = $xml->expect('{DAV:}multistatus', $body);
  219. $result = [];
  220. foreach ($multiStatus->getResponses() as $response) {
  221. $result[$response->getHref()] = $response->getResponseProperties();
  222. }
  223. return ['response' => $result, 'token' => $multiStatus->getSyncToken()];
  224. }
  225. /**
  226. * @param IUser $user
  227. */
  228. public function updateUser($user) {
  229. $systemAddressBook = $this->getLocalSystemAddressBook();
  230. $addressBookId = $systemAddressBook['id'];
  231. $converter = new Converter($this->accountManager);
  232. $name = $user->getBackendClassName();
  233. $userId = $user->getUID();
  234. $cardId = "$name:$userId.vcf";
  235. $card = $this->backend->getCard($addressBookId, $cardId);
  236. if ($card === false) {
  237. $vCard = $converter->createCardFromUser($user);
  238. if ($vCard !== null) {
  239. $this->backend->createCard($addressBookId, $cardId, $vCard->serialize());
  240. }
  241. } else {
  242. $vCard = $converter->createCardFromUser($user);
  243. if (is_null($vCard)) {
  244. $this->backend->deleteCard($addressBookId, $cardId);
  245. } else {
  246. $this->backend->updateCard($addressBookId, $cardId, $vCard->serialize());
  247. }
  248. }
  249. }
  250. /**
  251. * @param IUser|string $userOrCardId
  252. */
  253. public function deleteUser($userOrCardId) {
  254. $systemAddressBook = $this->getLocalSystemAddressBook();
  255. if ($userOrCardId instanceof IUser){
  256. $name = $userOrCardId->getBackendClassName();
  257. $userId = $userOrCardId->getUID();
  258. $userOrCardId = "$name:$userId.vcf";
  259. }
  260. $this->backend->deleteCard($systemAddressBook['id'], $userOrCardId);
  261. }
  262. /**
  263. * @return array|null
  264. */
  265. public function getLocalSystemAddressBook() {
  266. if (is_null($this->localSystemAddressBook)) {
  267. $systemPrincipal = "principals/system/system";
  268. $this->localSystemAddressBook = $this->ensureSystemAddressBookExists($systemPrincipal, 'system', [
  269. '{' . Plugin::NS_CARDDAV . '}addressbook-description' => 'System addressbook which holds all users of this instance'
  270. ]);
  271. }
  272. return $this->localSystemAddressBook;
  273. }
  274. public function syncInstance(\Closure $progressCallback = null) {
  275. $systemAddressBook = $this->getLocalSystemAddressBook();
  276. $this->userManager->callForAllUsers(function($user) use ($systemAddressBook, $progressCallback) {
  277. $this->updateUser($user);
  278. if (!is_null($progressCallback)) {
  279. $progressCallback();
  280. }
  281. });
  282. // remove no longer existing
  283. $allCards = $this->backend->getCards($systemAddressBook['id']);
  284. foreach($allCards as $card) {
  285. $vCard = Reader::read($card['carddata']);
  286. $uid = $vCard->UID->getValue();
  287. // load backend and see if user exists
  288. if (!$this->userManager->userExists($uid)) {
  289. $this->deleteUser($card['uri']);
  290. }
  291. }
  292. }
  293. }