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