aboutsummaryrefslogtreecommitdiffstats
path: root/apps/files_sharing/lib
diff options
context:
space:
mode:
authorStefan Cherniakov <luka-sama@pm.me>2024-08-22 06:43:43 +0200
committerJohn Molakvoæ <skjnldsv@users.noreply.github.com>2024-09-03 13:37:17 +0200
commit5e4a166365a601bf0133a97fda968a85744dfef5 (patch)
tree837eb9bf42cde512caff9103c7c7faae34addcea /apps/files_sharing/lib
parent6b85a3ae0eec4f6516459d223159a8d8e301e445 (diff)
downloadnextcloud-server-5e4a166365a601bf0133a97fda968a85744dfef5.tar.gz
nextcloud-server-5e4a166365a601bf0133a97fda968a85744dfef5.zip
feat(files_sharing): reminder for link shares with expiration date
Signed-off-by: Stefan Cherniakov <luka-sama@pm.me>
Diffstat (limited to 'apps/files_sharing/lib')
-rw-r--r--apps/files_sharing/lib/Migration/Version31000Date20240821142813.php36
-rw-r--r--apps/files_sharing/lib/SharesReminderJob.php261
2 files changed, 297 insertions, 0 deletions
diff --git a/apps/files_sharing/lib/Migration/Version31000Date20240821142813.php b/apps/files_sharing/lib/Migration/Version31000Date20240821142813.php
new file mode 100644
index 00000000000..b063902a380
--- /dev/null
+++ b/apps/files_sharing/lib/Migration/Version31000Date20240821142813.php
@@ -0,0 +1,36 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\Files_Sharing\Migration;
+
+use Closure;
+use OCP\DB\ISchemaWrapper;
+use OCP\DB\Types;
+use OCP\Migration\IOutput;
+use OCP\Migration\SimpleMigrationStep;
+
+class Version31000Date20240821142813 extends SimpleMigrationStep {
+
+ /**
+ * @param IOutput $output
+ * @param Closure(): ISchemaWrapper $schemaClosure
+ * @param array $options
+ * @return null|ISchemaWrapper
+ */
+ public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper {
+ $schema = $schemaClosure();
+ $table = $schema->getTable('share');
+ $table->addColumn('reminder_sent', Types::BOOLEAN, [
+ 'notnull' => false,
+ 'default' => false,
+ ]);
+ return $schema;
+ }
+
+}
diff --git a/apps/files_sharing/lib/SharesReminderJob.php b/apps/files_sharing/lib/SharesReminderJob.php
new file mode 100644
index 00000000000..214e555f173
--- /dev/null
+++ b/apps/files_sharing/lib/SharesReminderJob.php
@@ -0,0 +1,261 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\Files_Sharing;
+
+use OCP\AppFramework\Utility\ITimeFactory;
+use OCP\BackgroundJob\TimedJob;
+use OCP\Constants;
+use OCP\DB\Exception;
+use OCP\DB\QueryBuilder\IQueryBuilder;
+use OCP\Defaults;
+use OCP\Files\NotFoundException;
+use OCP\IDBConnection;
+use OCP\IL10N;
+use OCP\IURLGenerator;
+use OCP\IUserManager;
+use OCP\L10N\IFactory;
+use OCP\Mail\IEMailTemplate;
+use OCP\Mail\IMailer;
+use OCP\Share\Exceptions\ShareNotFound;
+use OCP\Share\IManager;
+use OCP\Share\IShare;
+use OCP\Util;
+use Psr\Log\LoggerInterface;
+
+/**
+ * Send a reminder via email to the sharee(s) if the folder is still empty a predefined time before the expiration date
+ */
+class SharesReminderJob extends TimedJob {
+ private const SECONDS_BEFORE_REMINDER = 86400;
+
+ public function __construct(
+ ITimeFactory $time,
+ private readonly IDBConnection $db,
+ private readonly IManager $shareManager,
+ private readonly IUserManager $userManager,
+ private readonly LoggerInterface $logger,
+ private readonly IURLGenerator $urlGenerator,
+ private readonly IFactory $l10nFactory,
+ private readonly IMailer $mailer,
+ private readonly Defaults $defaults,
+ ) {
+ parent::__construct($time);
+ $this->setInterval(3600);
+ }
+
+
+ /**
+ * Makes the background job do its work
+ *
+ * @param array $argument unused argument
+ * @throws Exception if a database error occurs
+ */
+ public function run(mixed $argument): void {
+ $shares = $this->getShares();
+ [$foldersByEmail, $langByEmail] = $this->prepareReminders($shares);
+ $this->sendReminders($foldersByEmail, $langByEmail);
+ }
+
+ /**
+ * Finds all folder shares of type user or email with expiration dates within the specified timeframe.
+ * This method returns only those shares that have not yet received the reminder.
+ *
+ * @return array<IShare>
+ * @throws Exception if a database error occurs
+ */
+ private function getShares(): array {
+ $minDate = new \DateTime();
+ $maxDate = new \DateTime();
+ $maxDate->setTimestamp($maxDate->getTimestamp() + self::SECONDS_BEFORE_REMINDER);
+
+ $qb = $this->db->getQueryBuilder();
+ $qb->select('id', 'share_type')
+ ->from('share')
+ ->where(
+ $qb->expr()->andX(
+ $qb->expr()->orX(
+ $qb->expr()->eq('share_type', $qb->expr()->literal(IShare::TYPE_USER)),
+ $qb->expr()->eq('share_type', $qb->expr()->literal(IShare::TYPE_EMAIL))
+ ),
+ $qb->expr()->eq('item_type', $qb->expr()->literal('folder')),
+ $qb->expr()->gte('expiration', $qb->createNamedParameter($minDate->format('Y-m-d H:i:s'))),
+ $qb->expr()->lt('expiration', $qb->createNamedParameter($maxDate->format('Y-m-d H:i:s'))),
+ $qb->expr()->eq('reminder_sent', $qb->createNamedParameter(
+ false, IQueryBuilder::PARAM_BOOL
+ ))
+ )
+ );
+
+ $sharesResult = $qb->executeQuery();
+ $shares = [];
+ while ($share = $sharesResult->fetch()) {
+ if ((int)$share['share_type'] === IShare::TYPE_EMAIL) {
+ $id = "ocMailShare:$share[id]";
+ } else {
+ $id = "ocinternal:$share[id]";
+ }
+
+ try {
+ $shares[] = $this->shareManager->getShareById($id);
+ } catch (ShareNotFound) {
+ $this->logger->error("Share with ID $id not found.");
+ }
+ }
+ $sharesResult->closeCursor();
+ return $shares;
+ }
+
+ /**
+ * Checks if the user should be reminded about this share.
+ * If so, it will retrieve and return all the necessary data for this.
+ * It also updates the reminder sent flag for the affected shares (to avoid multiple reminders).
+ *
+ * @param array<IShare> $shares Shares that were obtained with {@link getShares}
+ * @return array<array> A tuple consisting of two dictionaries: folders and languages by email
+ * @throws Exception if the reminder sent flag could not be saved
+ */
+ private function prepareReminders(array $shares): array {
+ // This dictionary stores email addresses as keys and folder lists as values.
+ // It is used to ensure that each user receives no more than one email notification.
+ // The email will include the names and links of the folders that the user should be reminded of.
+ $foldersByEmail = [];
+ // Similar to the previous one, this variable stores the language for each email (if provided)
+ $langByEmail = [];
+
+ /** @var IShare $share */
+ foreach ($shares as $share) {
+ if (!$this->shouldRemindOfThisShare($share)) {
+ continue;
+ }
+
+ $sharedWith = $share->getSharedWith();
+ if ($share->getShareType() == IShare::TYPE_USER) {
+ $user = $this->userManager->get($sharedWith);
+ $mailTo = $user->getEMailAddress();
+ $lang = $this->l10nFactory->getUserLanguage($user);
+ $link = $this->urlGenerator->linkToRouteAbsolute('files.view.index', [
+ 'dir' => $share->getTarget()
+ ]);
+ } else {
+ $mailTo = $sharedWith;
+ $lang = '';
+ $link = $this->urlGenerator->linkToRouteAbsolute('files_sharing.sharecontroller.showShare', [
+ 'token' => $share->getToken()
+ ]);
+ }
+ if (empty($mailTo)) {
+ continue;
+ }
+
+ if (!empty($lang)) {
+ $langByEmail[$mailTo] ??= $lang;
+ }
+ if (!isset($foldersByEmail[$mailTo])) {
+ $foldersByEmail[$mailTo] = [];
+ }
+ $foldersByEmail[$mailTo][] = ['name' => $share->getNode()->getName(), 'link' => $link];
+
+ $share->setReminderSent(true);
+ $qb = $this->db->getQueryBuilder();
+ $qb->update('share')
+ ->where($qb->expr()->eq('id', $qb->createNamedParameter($share->getId())))
+ ->set('reminder_sent', $qb->createNamedParameter($share->getReminderSent()))
+ ->execute();
+ }
+
+ return [$foldersByEmail, $langByEmail];
+ }
+
+ /**
+ * Checks if user has write permission and folder is empty
+ *
+ * @param IShare $share Share to check
+ * @return bool
+ */
+ private function shouldRemindOfThisShare(IShare $share): bool {
+ try {
+ $folder = $share->getNode();
+ $fileCount = count($folder->getDirectoryListing());
+ } catch (NotFoundException) {
+ $id = $share->getFullId();
+ $this->logger->debug("File by share ID $id not found.");
+ return false;
+ }
+ $permissions = $share->getPermissions();
+ $hasCreatePermission = ($permissions & Constants::PERMISSION_CREATE) === Constants::PERMISSION_CREATE;
+ return ($fileCount == 0 && $hasCreatePermission);
+ }
+
+ /**
+ * This method accepts data obtained by {@link prepareReminders} and sends reminder emails.
+ *
+ * @param array $foldersByEmail
+ * @param array $langByEmail
+ * @return void
+ */
+ private function sendReminders(array $foldersByEmail, array $langByEmail): void {
+ $instanceName = $this->defaults->getName();
+ $from = [Util::getDefaultEmailAddress($instanceName) => $instanceName];
+ foreach ($foldersByEmail as $email => $folders) {
+ $l = $this->l10nFactory->get('files_sharing', $langByEmail[$email] ?? null);
+ $emailTemplate = $this->generateEMailTemplate($l, $folders);
+ $message = $this->mailer->createMessage();
+ $message->setFrom($from);
+ $message->setTo([$email]);
+ $message->useTemplate($emailTemplate);
+ $errorText = "Sending email with share reminder to $email failed.";
+ try {
+ $failedRecipients = $this->mailer->send($message);
+ if (count($failedRecipients) > 0) {
+ $this->logger->error($errorText);
+ }
+ } catch (\Exception) {
+ $this->logger->error($errorText);
+ }
+ }
+ }
+
+ /**
+ * Returns the reminder email template
+ *
+ * @param IL10N $l
+ * @param array<array> $folders Folders the user should be reminded of
+ * @return IEMailTemplate
+ */
+ private function generateEMailTemplate(IL10N $l, array $folders): IEMailTemplate {
+ $emailTemplate = $this->mailer->createEMailTemplate('files_sharing.SharesReminder', [
+ 'folders' => $folders,
+ ]);
+
+ $emailTemplate->addHeader();
+ if (count($folders) == 1) {
+ $emailTemplate->setSubject(
+ $l->t('Remember to upload the files to %s', [$folders[0]['name']])
+ );
+ $emailTemplate->addBodyText($l->t(
+ 'We would like to kindly remind you that you have not yet uploaded any files to the shared folder.'
+ ));
+ } else {
+ $emailTemplate->setSubject(
+ $l->t('Remember to upload the files to shared folders')
+ );
+ $emailTemplate->addBodyText($l->t(
+ 'We would like to kindly remind you that you have not yet uploaded any files to the shared folders.'
+ ));
+ }
+
+ foreach ($folders as $folder) {
+ $emailTemplate->addBodyButton(
+ $l->t('Open "%s"', [$folder['name']]),
+ $folder['link']
+ );
+ }
+ $emailTemplate->addFooter();
+ return $emailTemplate;
+ }
+}