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 Christoph Wurst <christoph@winzerhof-wurst.at> 8 * @author Julius Härtl <jus@bitgrid.net> 9 * @author Lukas Reschke <lukas@statuscode.ch> 10 * @author Morris Jobke <hey@morrisjobke.de> 11 * @author Samuel <faust64@gmail.com> 12 * 13 * @license AGPL-3.0 14 * 15 * This code is free software: you can redistribute it and/or modify 16 * it under the terms of the GNU Affero General Public License, version 3, 17 * as published by the Free Software Foundation. 18 * 19 * This program is distributed in the hope that it will be useful, 20 * but WITHOUT ANY WARRANTY; without even the implied warranty of 21 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 22 * GNU Affero General Public License for more details. 23 * 24 * You should have received a copy of the GNU Affero General Public License, version 3, 25 * along with this program. If not, see <http://www.gnu.org/licenses/> 26 * 27 */ 28namespace OCA\FederatedFileSharing; 29 30use OCA\FederatedFileSharing\Events\FederatedShareAddedEvent; 31use OCP\AppFramework\Http; 32use OCP\BackgroundJob\IJobList; 33use OCP\EventDispatcher\IEventDispatcher; 34use OCP\Federation\ICloudFederationFactory; 35use OCP\Federation\ICloudFederationProviderManager; 36use OCP\Http\Client\IClientService; 37use OCP\ILogger; 38use OCP\OCS\IDiscoveryService; 39 40class Notifications { 41 public const RESPONSE_FORMAT = 'json'; // default response format for ocs calls 42 43 /** @var AddressHandler */ 44 private $addressHandler; 45 46 /** @var IClientService */ 47 private $httpClientService; 48 49 /** @var IDiscoveryService */ 50 private $discoveryService; 51 52 /** @var IJobList */ 53 private $jobList; 54 55 /** @var ICloudFederationProviderManager */ 56 private $federationProviderManager; 57 58 /** @var ICloudFederationFactory */ 59 private $cloudFederationFactory; 60 61 /** @var IEventDispatcher */ 62 private $eventDispatcher; 63 64 /** @var ILogger */ 65 private $logger; 66 67 public function __construct( 68 AddressHandler $addressHandler, 69 IClientService $httpClientService, 70 IDiscoveryService $discoveryService, 71 ILogger $logger, 72 IJobList $jobList, 73 ICloudFederationProviderManager $federationProviderManager, 74 ICloudFederationFactory $cloudFederationFactory, 75 IEventDispatcher $eventDispatcher 76 ) { 77 $this->addressHandler = $addressHandler; 78 $this->httpClientService = $httpClientService; 79 $this->discoveryService = $discoveryService; 80 $this->jobList = $jobList; 81 $this->logger = $logger; 82 $this->federationProviderManager = $federationProviderManager; 83 $this->cloudFederationFactory = $cloudFederationFactory; 84 $this->eventDispatcher = $eventDispatcher; 85 } 86 87 /** 88 * send server-to-server share to remote server 89 * 90 * @param string $token 91 * @param string $shareWith 92 * @param string $name 93 * @param string $remoteId 94 * @param string $owner 95 * @param string $ownerFederatedId 96 * @param string $sharedBy 97 * @param string $sharedByFederatedId 98 * @param int $shareType (can be a remote user or group share) 99 * @return bool 100 * @throws \OCP\HintException 101 * @throws \OC\ServerNotAvailableException 102 */ 103 public function sendRemoteShare($token, $shareWith, $name, $remoteId, $owner, $ownerFederatedId, $sharedBy, $sharedByFederatedId, $shareType) { 104 [$user, $remote] = $this->addressHandler->splitUserRemote($shareWith); 105 106 if ($user && $remote) { 107 $local = $this->addressHandler->generateRemoteURL(); 108 109 $fields = [ 110 'shareWith' => $user, 111 'token' => $token, 112 'name' => $name, 113 'remoteId' => $remoteId, 114 'owner' => $owner, 115 'ownerFederatedId' => $ownerFederatedId, 116 'sharedBy' => $sharedBy, 117 'sharedByFederatedId' => $sharedByFederatedId, 118 'remote' => $local, 119 'shareType' => $shareType 120 ]; 121 122 $result = $this->tryHttpPostToShareEndpoint($remote, '', $fields); 123 $status = json_decode($result['result'], true); 124 125 $ocsStatus = isset($status['ocs']); 126 $ocsSuccess = $ocsStatus && ($status['ocs']['meta']['statuscode'] === 100 || $status['ocs']['meta']['statuscode'] === 200); 127 128 if ($result['success'] && (!$ocsStatus || $ocsSuccess)) { 129 $event = new FederatedShareAddedEvent($remote); 130 $this->eventDispatcher->dispatchTyped($event); 131 return true; 132 } else { 133 $this->logger->info( 134 "failed sharing $name with $shareWith", 135 ['app' => 'federatedfilesharing'] 136 ); 137 } 138 } else { 139 $this->logger->info( 140 "could not share $name, invalid contact $shareWith", 141 ['app' => 'federatedfilesharing'] 142 ); 143 } 144 145 return false; 146 } 147 148 /** 149 * ask owner to re-share the file with the given user 150 * 151 * @param string $token 152 * @param string $id remote Id 153 * @param string $shareId internal share Id 154 * @param string $remote remote address of the owner 155 * @param string $shareWith 156 * @param int $permission 157 * @param string $filename 158 * @return array|false 159 * @throws \OCP\HintException 160 * @throws \OC\ServerNotAvailableException 161 */ 162 public function requestReShare($token, $id, $shareId, $remote, $shareWith, $permission, $filename) { 163 $fields = [ 164 'shareWith' => $shareWith, 165 'token' => $token, 166 'permission' => $permission, 167 'remoteId' => $shareId, 168 ]; 169 170 $ocmFields = $fields; 171 $ocmFields['remoteId'] = (string)$id; 172 $ocmFields['localId'] = $shareId; 173 $ocmFields['name'] = $filename; 174 175 $ocmResult = $this->tryOCMEndPoint($remote, $ocmFields, 'reshare'); 176 if (is_array($ocmResult) && isset($ocmResult['token']) && isset($ocmResult['providerId'])) { 177 return [$ocmResult['token'], $ocmResult['providerId']]; 178 } 179 180 $result = $this->tryLegacyEndPoint(rtrim($remote, '/'), '/' . $id . '/reshare', $fields); 181 $status = json_decode($result['result'], true); 182 183 $httpRequestSuccessful = $result['success']; 184 $ocsCallSuccessful = $status['ocs']['meta']['statuscode'] === 100 || $status['ocs']['meta']['statuscode'] === 200; 185 $validToken = isset($status['ocs']['data']['token']) && is_string($status['ocs']['data']['token']); 186 $validRemoteId = isset($status['ocs']['data']['remoteId']); 187 188 if ($httpRequestSuccessful && $ocsCallSuccessful && $validToken && $validRemoteId) { 189 return [ 190 $status['ocs']['data']['token'], 191 $status['ocs']['data']['remoteId'] 192 ]; 193 } elseif (!$validToken) { 194 $this->logger->info( 195 "invalid or missing token requesting re-share for $filename to $remote", 196 ['app' => 'federatedfilesharing'] 197 ); 198 } elseif (!$validRemoteId) { 199 $this->logger->info( 200 "missing remote id requesting re-share for $filename to $remote", 201 ['app' => 'federatedfilesharing'] 202 ); 203 } else { 204 $this->logger->info( 205 "failed requesting re-share for $filename to $remote", 206 ['app' => 'federatedfilesharing'] 207 ); 208 } 209 210 return false; 211 } 212 213 /** 214 * send server-to-server unshare to remote server 215 * 216 * @param string $remote url 217 * @param string $id share id 218 * @param string $token 219 * @return bool 220 */ 221 public function sendRemoteUnShare($remote, $id, $token) { 222 $this->sendUpdateToRemote($remote, $id, $token, 'unshare'); 223 } 224 225 /** 226 * send server-to-server unshare to remote server 227 * 228 * @param string $remote url 229 * @param string $id share id 230 * @param string $token 231 * @return bool 232 */ 233 public function sendRevokeShare($remote, $id, $token) { 234 $this->sendUpdateToRemote($remote, $id, $token, 'reshare_undo'); 235 } 236 237 /** 238 * send notification to remote server if the permissions was changed 239 * 240 * @param string $remote 241 * @param string $remoteId 242 * @param string $token 243 * @param int $permissions 244 * @return bool 245 */ 246 public function sendPermissionChange($remote, $remoteId, $token, $permissions) { 247 $this->sendUpdateToRemote($remote, $remoteId, $token, 'permissions', ['permissions' => $permissions]); 248 } 249 250 /** 251 * forward accept reShare to remote server 252 * 253 * @param string $remote 254 * @param string $remoteId 255 * @param string $token 256 */ 257 public function sendAcceptShare($remote, $remoteId, $token) { 258 $this->sendUpdateToRemote($remote, $remoteId, $token, 'accept'); 259 } 260 261 /** 262 * forward decline reShare to remote server 263 * 264 * @param string $remote 265 * @param string $remoteId 266 * @param string $token 267 */ 268 public function sendDeclineShare($remote, $remoteId, $token) { 269 $this->sendUpdateToRemote($remote, $remoteId, $token, 'decline'); 270 } 271 272 /** 273 * inform remote server whether server-to-server share was accepted/declined 274 * 275 * @param string $remote 276 * @param string $token 277 * @param string $remoteId Share id on the remote host 278 * @param string $action possible actions: accept, decline, unshare, revoke, permissions 279 * @param array $data 280 * @param int $try 281 * @return boolean 282 */ 283 public function sendUpdateToRemote($remote, $remoteId, $token, $action, $data = [], $try = 0) { 284 $fields = [ 285 'token' => $token, 286 'remoteId' => $remoteId 287 ]; 288 foreach ($data as $key => $value) { 289 $fields[$key] = $value; 290 } 291 292 $result = $this->tryHttpPostToShareEndpoint(rtrim($remote, '/'), '/' . $remoteId . '/' . $action, $fields, $action); 293 $status = json_decode($result['result'], true); 294 295 if ($result['success'] && 296 ($status['ocs']['meta']['statuscode'] === 100 || 297 $status['ocs']['meta']['statuscode'] === 200 298 ) 299 ) { 300 return true; 301 } elseif ($try === 0) { 302 // only add new job on first try 303 $this->jobList->add('OCA\FederatedFileSharing\BackgroundJob\RetryJob', 304 [ 305 'remote' => $remote, 306 'remoteId' => $remoteId, 307 'token' => $token, 308 'action' => $action, 309 'data' => json_encode($data), 310 'try' => $try, 311 'lastRun' => $this->getTimestamp() 312 ] 313 ); 314 } 315 316 return false; 317 } 318 319 320 /** 321 * return current timestamp 322 * 323 * @return int 324 */ 325 protected function getTimestamp() { 326 return time(); 327 } 328 329 /** 330 * try http post with the given protocol, if no protocol is given we pick 331 * the secure one (https) 332 * 333 * @param string $remoteDomain 334 * @param string $urlSuffix 335 * @param array $fields post parameters 336 * @param string $action define the action (possible values: share, reshare, accept, decline, unshare, revoke, permissions) 337 * @return array 338 * @throws \Exception 339 */ 340 protected function tryHttpPostToShareEndpoint($remoteDomain, $urlSuffix, array $fields, $action = "share") { 341 if ($this->addressHandler->urlContainProtocol($remoteDomain) === false) { 342 $remoteDomain = 'https://' . $remoteDomain; 343 } 344 345 $result = [ 346 'success' => false, 347 'result' => '', 348 ]; 349 350 // if possible we use the new OCM API 351 $ocmResult = $this->tryOCMEndPoint($remoteDomain, $fields, $action); 352 if (is_array($ocmResult)) { 353 $result['success'] = true; 354 $result['result'] = json_encode([ 355 'ocs' => ['meta' => ['statuscode' => 200]]]); 356 return $result; 357 } 358 359 return $this->tryLegacyEndPoint($remoteDomain, $urlSuffix, $fields); 360 } 361 362 /** 363 * try old federated sharing API if the OCM api doesn't work 364 * 365 * @param $remoteDomain 366 * @param $urlSuffix 367 * @param array $fields 368 * @return mixed 369 * @throws \Exception 370 */ 371 protected function tryLegacyEndPoint($remoteDomain, $urlSuffix, array $fields) { 372 $result = [ 373 'success' => false, 374 'result' => '', 375 ]; 376 377 // Fall back to old API 378 $client = $this->httpClientService->newClient(); 379 $federationEndpoints = $this->discoveryService->discover($remoteDomain, 'FEDERATED_SHARING'); 380 $endpoint = isset($federationEndpoints['share']) ? $federationEndpoints['share'] : '/ocs/v2.php/cloud/shares'; 381 try { 382 $response = $client->post($remoteDomain . $endpoint . $urlSuffix . '?format=' . self::RESPONSE_FORMAT, [ 383 'body' => $fields, 384 'timeout' => 10, 385 'connect_timeout' => 10, 386 ]); 387 $result['result'] = $response->getBody(); 388 $result['success'] = true; 389 } catch (\Exception $e) { 390 // if flat re-sharing is not supported by the remote server 391 // we re-throw the exception and fall back to the old behaviour. 392 // (flat re-shares has been introduced in Nextcloud 9.1) 393 if ($e->getCode() === Http::STATUS_INTERNAL_SERVER_ERROR) { 394 throw $e; 395 } 396 } 397 398 return $result; 399 } 400 401 /** 402 * send action regarding federated sharing to the remote server using the OCM API 403 * 404 * @param $remoteDomain 405 * @param $fields 406 * @param $action 407 * 408 * @return array|false 409 */ 410 protected function tryOCMEndPoint($remoteDomain, $fields, $action) { 411 switch ($action) { 412 case 'share': 413 $share = $this->cloudFederationFactory->getCloudFederationShare( 414 $fields['shareWith'] . '@' . $remoteDomain, 415 $fields['name'], 416 '', 417 $fields['remoteId'], 418 $fields['ownerFederatedId'], 419 $fields['owner'], 420 $fields['sharedByFederatedId'], 421 $fields['sharedBy'], 422 $fields['token'], 423 $fields['shareType'], 424 'file' 425 ); 426 return $this->federationProviderManager->sendShare($share); 427 case 'reshare': 428 // ask owner to reshare a file 429 $notification = $this->cloudFederationFactory->getCloudFederationNotification(); 430 $notification->setMessage('REQUEST_RESHARE', 431 'file', 432 $fields['remoteId'], 433 [ 434 'sharedSecret' => $fields['token'], 435 'shareWith' => $fields['shareWith'], 436 'senderId' => $fields['localId'], 437 'shareType' => $fields['shareType'], 438 'message' => 'Ask owner to reshare the file' 439 ] 440 ); 441 return $this->federationProviderManager->sendNotification($remoteDomain, $notification); 442 case 'unshare': 443 //owner unshares the file from the recipient again 444 $notification = $this->cloudFederationFactory->getCloudFederationNotification(); 445 $notification->setMessage('SHARE_UNSHARED', 446 'file', 447 $fields['remoteId'], 448 [ 449 'sharedSecret' => $fields['token'], 450 'messgage' => 'file is no longer shared with you' 451 ] 452 ); 453 return $this->federationProviderManager->sendNotification($remoteDomain, $notification); 454 case 'reshare_undo': 455 // if a reshare was unshared we send the information to the initiator/owner 456 $notification = $this->cloudFederationFactory->getCloudFederationNotification(); 457 $notification->setMessage('RESHARE_UNDO', 458 'file', 459 $fields['remoteId'], 460 [ 461 'sharedSecret' => $fields['token'], 462 'message' => 'reshare was revoked' 463 ] 464 ); 465 return $this->federationProviderManager->sendNotification($remoteDomain, $notification); 466 } 467 468 return false; 469 } 470} 471