1<?php 2 3declare(strict_types=1); 4 5 6/** 7 * Circles - Bring cloud-users closer together. 8 * 9 * This file is licensed under the Affero General Public License version 3 or 10 * later. See the COPYING file. 11 * 12 * @author Maxence Lange <maxence@artificial-owl.com> 13 * @copyright 2017 14 * @license GNU AGPL version 3 or any later version 15 * 16 * This program is free software: you can redistribute it and/or modify 17 * it under the terms of the GNU Affero General Public License as 18 * published by the Free Software Foundation, either version 3 of the 19 * License, or (at your option) any later version. 20 * 21 * This program is distributed in the hope that it will be useful, 22 * but WITHOUT ANY WARRANTY; without even the implied warranty of 23 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 24 * GNU Affero General Public License for more details. 25 * 26 * You should have received a copy of the GNU Affero General Public License 27 * along with this program. If not, see <http://www.gnu.org/licenses/>. 28 * 29 */ 30 31 32namespace OCA\Circles\GlobalScale; 33 34use ArtificialOwl\MySmallPhpTools\Model\SimpleDataStore; 35use Exception; 36use OC\User\NoUserException; 37use OCA\Circles\Exceptions\CircleDoesNotExistException; 38use OCA\Circles\Exceptions\CircleTypeNotValidException; 39use OCA\Circles\Exceptions\ConfigNoCircleAvailableException; 40use OCA\Circles\Exceptions\EmailAccountInvalidFormatException; 41use OCA\Circles\Exceptions\GlobalScaleDSyncException; 42use OCA\Circles\Exceptions\GlobalScaleEventException; 43use OCA\Circles\Exceptions\MemberAlreadyExistsException; 44use OCA\Circles\Exceptions\MemberCantJoinCircleException; 45use OCA\Circles\Exceptions\MemberIsNotModeratorException; 46use OCA\Circles\Exceptions\MembersLimitException; 47use OCA\Circles\Exceptions\TokenDoesNotExistException; 48use OCA\Circles\Model\DeprecatedCircle; 49use OCA\Circles\Model\GlobalScale\GSEvent; 50use OCA\Circles\Model\DeprecatedMember; 51use OCA\Circles\Model\SharesToken; 52use OCP\IUser; 53use OCP\Mail\IEMailTemplate; 54use OCP\Util; 55 56/** 57 * Class MemberAdd 58 * 59 * @package OCA\Circles\GlobalScale 60 */ 61class MemberAdd extends AGlobalScaleEvent { 62 63 64 /** 65 * @param GSEvent $event 66 * @param bool $localCheck 67 * @param bool $mustBeChecked 68 * 69 * @throws CircleDoesNotExistException 70 * @throws ConfigNoCircleAvailableException 71 * @throws EmailAccountInvalidFormatException 72 * @throws GlobalScaleDSyncException 73 * @throws GlobalScaleEventException 74 * @throws MemberAlreadyExistsException 75 * @throws MemberCantJoinCircleException 76 * @throws MembersLimitException 77 * @throws NoUserException 78 * @throws CircleTypeNotValidException 79 * @throws MemberIsNotModeratorException 80 */ 81 public function verify(GSEvent $event, bool $localCheck = false, bool $mustBeChecked = false): void { 82 parent::verify($event, $localCheck, true); 83 84 $eventMember = $event->getMember(); 85 $this->cleanMember($eventMember); 86 87 if ($eventMember->getInstance() === '') { 88 $eventMember->setInstance($event->getSource()); 89 } 90 91 $ident = $eventMember->getUserId(); 92 $this->membersService->verifyIdentBasedOnItsType( 93 $ident, $eventMember->getType(), $eventMember->getInstance() 94 ); 95 96 $circle = $event->getDeprecatedCircle(); 97 98 if (!$event->isForced()) { 99 $circle->getHigherViewer() 100 ->hasToBeModerator(); 101 } 102 103 $member = $this->membersRequest->getFreshNewMember( 104 $circle->getUniqueId(), $ident, $eventMember->getType(), $eventMember->getInstance() 105 ); 106 $member->hasToBeInviteAble(); 107 $member->setCachedName($eventMember->getCachedName()); 108 109 $this->circlesService->checkThatCircleIsNotFull($circle); 110 $this->membersService->addMemberBasedOnItsType($circle, $member); 111 112 $password = ''; 113 $sendPasswordByMail = false; 114 if ($this->configService->enforcePasswordProtection($circle)) { 115 if ($circle->getSetting('password_single_enabled') === 'true') { 116 $password = $circle->getPasswordSingle(); 117 } else { 118 $sendPasswordByMail = true; 119 $password = $this->miscService->token(15); 120 } 121 } 122 123 $event->setData( 124 new SimpleDataStore( 125 [ 126 'password' => $password, 127 'passwordByMail' => $sendPasswordByMail 128 ] 129 ) 130 ); 131 $event->setMember($member); 132 } 133 134 135 /** 136 * @param GSEvent $event 137 * 138 * @throws MemberAlreadyExistsException 139 */ 140 public function manage(GSEvent $event): void { 141 $circle = $event->getDeprecatedCircle(); 142 $member = $event->getMember(); 143 if ($member->getJoined() === '') { 144 $this->membersRequest->createMember($member); 145 } else { 146 $this->membersRequest->updateMemberLevel($member); 147 } 148 149 150 // 151 // TODO: verifiez comment se passe le cached name sur un member_add 152 // 153 $cachedName = $member->getCachedName(); 154 $password = $event->getData() 155 ->g('password'); 156 157 $shares = $this->generateUnknownSharesLinks($circle, $member, $password); 158 $result = [ 159 'unknownShares' => $shares, 160 'cachedName' => $cachedName 161 ]; 162 163 if ($member->getType() === DeprecatedMember::TYPE_CONTACT 164 && $this->configService->isLocalInstance($member->getInstance())) { 165 $result['contact'] = $this->miscService->getInfosFromContact($member); 166 } 167 168 $event->setResult(new SimpleDataStore($result)); 169 $this->eventsService->onMemberNew($circle, $member); 170 } 171 172 173 /** 174 * @param GSEvent[] $events 175 * 176 * @throws Exception 177 */ 178 public function result(array $events): void { 179 $password = $cachedName = ''; 180 $circle = $member = null; 181 $links = []; 182 $recipients = []; 183 foreach ($events as $event) { 184 $data = $event->getData(); 185 if ($data->gBool('passwordByMail') !== false) { 186 $password = $data->g('password'); 187 } 188 $circle = $event->getDeprecatedCircle(); 189 $member = $event->getMember(); 190 $result = $event->getResult(); 191 if ($result->g('cachedName') !== '') { 192 $cachedName = $result->g('cachedName'); 193 } 194 195 $links = array_merge($links, $result->gArray('unknownShares')); 196 $contact = $result->gArray('contact'); 197 if (!empty($contact)) { 198 $recipients = $contact['emails']; 199 } 200 } 201 202 if (empty($links) || $circle === null || $member === null) { 203 return; 204 } 205 206 if ($cachedName !== '') { 207 $member->setCachedName($cachedName); 208 $this->membersService->updateMember($member); 209 } 210 211 if ($member->getType() === DeprecatedMember::TYPE_MAIL 212 || $member->getType() === DeprecatedMember::TYPE_CONTACT) { 213 if ($member->getType() === DeprecatedMember::TYPE_MAIL) { 214 $recipients = [$member->getUserId()]; 215 } 216 217 foreach ($recipients as $recipient) { 218 $this->memberIsMailbox($circle, $recipient, $links, $password); 219 } 220 } 221 } 222 223 224 /** 225 * @param DeprecatedCircle $circle 226 * @param string $recipient 227 * @param array $links 228 * @param string $password 229 */ 230 private function memberIsMailbox(DeprecatedCircle $circle, string $recipient, array $links, string $password) { 231 if ($circle->getViewer() === null) { 232 $author = $circle->getOwner() 233 ->getUserId(); 234 } else { 235 $author = $circle->getViewer() 236 ->getUserId(); 237 } 238 239 try { 240 $template = $this->generateMailExitingShares($author, $circle->getName()); 241 $this->fillMailExistingShares($template, $links); 242 $this->sendMailExistingShares($template, $author, $recipient); 243 $this->sendPasswordExistingShares($author, $recipient, $password); 244 } catch (Exception $e) { 245 $this->miscService->log('Failed to send mail about existing share ' . $e->getMessage()); 246 } 247 } 248 249 250 /** 251 * @param DeprecatedCircle $circle 252 * @param DeprecatedMember $member 253 * @param string $password 254 * 255 * @return array 256 */ 257 private function generateUnknownSharesLinks(DeprecatedCircle $circle, DeprecatedMember $member, string $password): array { 258 $unknownShares = $this->getUnknownShares($member); 259 260 $data = []; 261 foreach ($unknownShares as $share) { 262 try { 263 $data[] = $this->getMailLinkFromShare($share, $member, $password); 264 } catch (TokenDoesNotExistException $e) { 265 } 266 } 267 268 return $data; 269 } 270 271 272 /** 273 * @param DeprecatedMember $member 274 * 275 * @return array 276 */ 277 private function getUnknownShares(DeprecatedMember $member): array { 278 $allShares = $this->fileSharesRequest->getSharesForCircle($member->getCircleId()); 279 $knownShares = array_map( 280 function (SharesToken $shareToken) { 281 return $shareToken->getShareId(); 282 }, 283 $this->tokensRequest->getTokensFromMember($member) 284 ); 285 286 $unknownShares = []; 287 foreach ($allShares as $share) { 288 if (!in_array($share['id'], $knownShares)) { 289 $unknownShares[] = $share; 290 } 291 } 292 293 return $unknownShares; 294 } 295 296 297 /** 298 * @param array $share 299 * @param DeprecatedMember $member 300 * @param string $password 301 * 302 * @return array 303 * @throws TokenDoesNotExistException 304 */ 305 private function getMailLinkFromShare(array $share, DeprecatedMember $member, string $password = '') { 306 $sharesToken = $this->tokensRequest->generateTokenForMember($member, (int)$share['id'], $password); 307 $link = $this->urlGenerator->linkToRouteAbsolute( 308 'files_sharing.sharecontroller.showShare', 309 ['token' => $sharesToken->getToken()] 310 ); 311 $author = $share['uid_initiator']; 312 $filename = basename($share['file_target']); 313 314 return [ 315 'author' => $author, 316 'link' => $link, 317 'filename' => $filename 318 ]; 319 } 320 321 322 /** 323 * @param string $author 324 * @param string $circleName 325 * 326 * @return IEMailTemplate 327 */ 328 private function generateMailExitingShares(string $author, string $circleName): IEMailTemplate { 329 $emailTemplate = $this->mailer->createEMailTemplate('circles.ExistingShareNotification', []); 330 $emailTemplate->addHeader(); 331 332 $text = $this->l10n->t('%s shared multiple files with "%s".', [$author, $circleName]); 333 $emailTemplate->addBodyText(htmlspecialchars($text), $text); 334 335 return $emailTemplate; 336 } 337 338 /** 339 * @param IEMailTemplate $emailTemplate 340 * @param array $links 341 */ 342 private function fillMailExistingShares(IEMailTemplate $emailTemplate, array $links) { 343 foreach ($links as $item) { 344 $emailTemplate->addBodyButton( 345 $this->l10n->t('Open »%s«', [htmlspecialchars($item['filename'])]), $item['link'] 346 ); 347 } 348 } 349 350 351 /** 352 * @param IEMailTemplate $emailTemplate 353 * @param string $author 354 * @param string $recipient 355 * 356 * @throws Exception 357 */ 358 private function sendMailExistingShares(IEMailTemplate $emailTemplate, string $author, string $recipient 359 ) { 360 $subject = $this->l10n->t('%s shared multiple files with you.', [$author]); 361 362 $instanceName = $this->defaults->getName(); 363 $senderName = $this->l10n->t('%s on %s', [$author, $instanceName]); 364 365 $message = $this->mailer->createMessage(); 366 367 $message->setFrom([Util::getDefaultEmailAddress($instanceName) => $senderName]); 368 $message->setSubject($subject); 369 $message->setPlainBody($emailTemplate->renderText()); 370 $message->setHtmlBody($emailTemplate->renderHtml()); 371 $message->setTo([$recipient]); 372 373 $this->mailer->send($message); 374 } 375 376 377 /** 378 * @param string $author 379 * @param string $email 380 * @param string $password 381 * 382 * @throws Exception 383 */ 384 protected function sendPasswordExistingShares(string $author, string $email, string $password) { 385 if ($password === '') { 386 return; 387 } 388 389 $message = $this->mailer->createMessage(); 390 391 $authorUser = $this->userManager->get($author); 392 $authorName = ($authorUser instanceof IUser) ? $authorUser->getDisplayName() : $author; 393 $authorEmail = ($authorUser instanceof IUser) ? $authorUser->getEMailAddress() : null; 394 395 $this->miscService->log("Sending password mail about existing files to '" . $email . "'", 0); 396 397 $plainBodyPart = $this->l10n->t( 398 "%1\$s shared multiple files with you.\nYou should have already received a separate email with a link to access them.\n", 399 [$authorName] 400 ); 401 $htmlBodyPart = $this->l10n->t( 402 '%1$s shared multiple files with you. You should have already received a separate email with a link to access them.', 403 [$authorName] 404 ); 405 406 $emailTemplate = $this->mailer->createEMailTemplate( 407 'sharebymail.RecipientPasswordNotification', [ 408 'password' => $password, 409 'author' => $author 410 ] 411 ); 412 413 $emailTemplate->setSubject( 414 $this->l10n->t( 415 'Password to access files shared to you by %1$s', [$authorName] 416 ) 417 ); 418 $emailTemplate->addHeader(); 419 $emailTemplate->addHeading($this->l10n->t('Password to access files'), false); 420 $emailTemplate->addBodyText(htmlspecialchars($htmlBodyPart), $plainBodyPart); 421 $emailTemplate->addBodyText($this->l10n->t('It is protected with the following password:')); 422 $emailTemplate->addBodyText($password); 423 424 // The "From" contains the sharers name 425 $instanceName = $this->defaults->getName(); 426 $senderName = $this->l10n->t( 427 '%1$s via %2$s', 428 [ 429 $authorName, 430 $instanceName 431 ] 432 ); 433 434 $message->setFrom([\OCP\Util::getDefaultEmailAddress($instanceName) => $senderName]); 435 if ($authorEmail !== null) { 436 $message->setReplyTo([$authorEmail => $authorName]); 437 $emailTemplate->addFooter($instanceName . ' - ' . $this->defaults->getSlogan()); 438 } else { 439 $emailTemplate->addFooter(); 440 } 441 442 $message->setTo([$email]); 443 $message->useTemplate($emailTemplate); 444 $this->mailer->send($message); 445 } 446} 447