setInterval(60 * 60); $this->folderMimeTypeId = $mimeTypeLoader->getId(ICacheEntry::DIRECTORY_MIMETYPE); } /** * 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 { foreach ($this->getShares() as $share) { $reminderInfo = $this->prepareReminder($share); if ($reminderInfo !== null) { $this->sendReminder($reminderInfo); } } } /** * Finds all shares of empty folders, for which the user has write permissions. * The returned shares are of type user or email only, have expiration dates within the specified time frame * and have not yet received a reminder. * * @return array|\Iterator * @throws Exception if a database error occurs */ private function getShares(): array|\Iterator { if ($this->db->getShardDefinition('filecache')) { $sharesResult = $this->getSharesDataSharded(); } else { $sharesResult = $this->getSharesData(); } foreach ($sharesResult as $share) { if ($share['share_type'] === IShare::TYPE_EMAIL) { $id = "ocMailShare:$share[id]"; } else { $id = "ocinternal:$share[id]"; } try { yield $this->shareManager->getShareById($id); } catch (ShareNotFound) { $this->logger->error("Share with ID $id not found."); } } } /** * @return list */ private function getSharesData(): array { $minDate = new \DateTime(); $maxDate = new \DateTime(); $maxDate->setTimestamp($maxDate->getTimestamp() + self::SECONDS_BEFORE_REMINDER); $qb = $this->db->getQueryBuilder(); $qb->select('s.id', 's.share_type') ->from('share', 's') ->leftJoin('s', 'filecache', 'f', $qb->expr()->eq('f.parent', 's.file_source')) ->where( $qb->expr()->andX( $qb->expr()->orX( $qb->expr()->eq('s.share_type', $qb->expr()->literal(IShare::TYPE_USER)), $qb->expr()->eq('s.share_type', $qb->expr()->literal(IShare::TYPE_EMAIL)) ), $qb->expr()->eq('s.item_type', $qb->expr()->literal('folder')), $qb->expr()->gte('s.expiration', $qb->createNamedParameter($minDate, IQueryBuilder::PARAM_DATE)), $qb->expr()->lte('s.expiration', $qb->createNamedParameter($maxDate, IQueryBuilder::PARAM_DATE)), $qb->expr()->eq('s.reminder_sent', $qb->createNamedParameter( false, IQueryBuilder::PARAM_BOOL )), $qb->expr()->eq( $qb->expr()->bitwiseAnd('s.permissions', Constants::PERMISSION_CREATE), $qb->createNamedParameter(Constants::PERMISSION_CREATE, IQueryBuilder::PARAM_INT) ), $qb->expr()->isNull('f.fileid') ) ) ->setMaxResults(SharesReminderJob::CHUNK_SIZE); $shares = $qb->executeQuery()->fetchAll(); return array_values(array_map(fn ($share): array => [ 'id' => (int)$share['id'], 'share_type' => (int)$share['share_type'], ], $shares)); } /** * Sharding compatible version of getSharesData * * @return list */ private function getSharesDataSharded(): array|\Iterator { $minDate = new \DateTime(); $maxDate = new \DateTime(); $maxDate->setTimestamp($maxDate->getTimestamp() + self::SECONDS_BEFORE_REMINDER); $qb = $this->db->getQueryBuilder(); $qb->select('s.id', 's.share_type', 's.file_source') ->from('share', 's') ->where( $qb->expr()->andX( $qb->expr()->orX( $qb->expr()->eq('s.share_type', $qb->expr()->literal(IShare::TYPE_USER)), $qb->expr()->eq('s.share_type', $qb->expr()->literal(IShare::TYPE_EMAIL)) ), $qb->expr()->eq('s.item_type', $qb->expr()->literal('folder')), $qb->expr()->gte('s.expiration', $qb->createNamedParameter($minDate, IQueryBuilder::PARAM_DATE)), $qb->expr()->lte('s.expiration', $qb->createNamedParameter($maxDate, IQueryBuilder::PARAM_DATE)), $qb->expr()->eq('s.reminder_sent', $qb->createNamedParameter( false, IQueryBuilder::PARAM_BOOL )), $qb->expr()->eq( $qb->expr()->bitwiseAnd('s.permissions', Constants::PERMISSION_CREATE), $qb->createNamedParameter(Constants::PERMISSION_CREATE, IQueryBuilder::PARAM_INT) ), ) ); $shares = $qb->executeQuery()->fetchAll(); $shares = array_values(array_map(fn ($share): array => [ 'id' => (int)$share['id'], 'share_type' => (int)$share['share_type'], 'file_source' => (int)$share['file_source'], ], $shares)); return $this->filterSharesWithEmptyFolders($shares, self::CHUNK_SIZE); } /** * Check which of the supplied file ids is an empty folder until there are `$maxResults` folders * @param list $shares * @return list */ private function filterSharesWithEmptyFolders(array $shares, int $maxResults): array { $query = $this->db->getQueryBuilder(); $query->select('fileid') ->from('filecache') ->where($query->expr()->eq('size', $query->createNamedParameter(0), IQueryBuilder::PARAM_INT_ARRAY)) ->andWhere($query->expr()->eq('mimetype', $query->createNamedParameter($this->folderMimeTypeId, IQueryBuilder::PARAM_INT))) ->andWhere($query->expr()->in('fileid', $query->createParameter('fileids'))); $chunks = array_chunk($shares, SharesReminderJob::CHUNK_SIZE); $results = []; foreach ($chunks as $chunk) { $chunkFileIds = array_map(fn ($share): int => $share['file_source'], $chunk); $chunkByFileId = array_combine($chunkFileIds, $chunk); $query->setParameter('fileids', $chunkFileIds, IQueryBuilder::PARAM_INT_ARRAY); $chunkResults = $query->executeQuery()->fetchAll(\PDO::FETCH_COLUMN); foreach ($chunkResults as $folderId) { $results[] = $chunkByFileId[$folderId]; } if (count($results) >= $maxResults) { break; } } return $results; } /** * Retrieves and returns all the necessary data before sending a reminder. * It also updates the reminder sent flag for the affected shares (to avoid multiple reminders). * * @param IShare $share Share that was obtained with {@link getShares} * @return array|null Info needed to send a reminder */ private function prepareReminder(IShare $share): ?array { $sharedWith = $share->getSharedWith(); $reminderInfo = []; if ($share->getShareType() == IShare::TYPE_USER) { $user = $this->userManager->get($sharedWith); if ($user === null) { return null; } $reminderInfo['email'] = $user->getEMailAddress(); $reminderInfo['userLang'] = $this->l10nFactory->getUserLanguage($user); $reminderInfo['folderLink'] = $this->urlGenerator->linkToRouteAbsolute('files.view.index', [ 'dir' => $share->getTarget() ]); } else { $reminderInfo['email'] = $sharedWith; $reminderInfo['folderLink'] = $this->urlGenerator->linkToRouteAbsolute('files_sharing.sharecontroller.showShare', [ 'token' => $share->getToken() ]); } if (empty($reminderInfo['email'])) { return null; } try { $reminderInfo['folderName'] = $share->getNode()->getName(); } catch (NotFoundException) { $id = $share->getFullId(); $this->logger->error("File by share ID $id not found."); } $share->setReminderSent(true); $this->shareManager->updateShare($share); return $reminderInfo; } /** * This method accepts data obtained by {@link prepareReminder} and sends reminder email. * * @param array $reminderInfo * @return void */ private function sendReminder(array $reminderInfo): void { $instanceName = $this->defaults->getName(); $from = [Util::getDefaultEmailAddress($instanceName) => $instanceName]; $l = $this->l10nFactory->get('files_sharing', $reminderInfo['userLang'] ?? null); $emailTemplate = $this->generateEMailTemplate($l, [ 'link' => $reminderInfo['folderLink'], 'name' => $reminderInfo['folderName'] ]); $message = $this->mailer->createMessage(); $message->setFrom($from); $message->setTo([$reminderInfo['email']]); $message->useTemplate($emailTemplate); $errorText = "Sending email with share reminder to $reminderInfo[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 $folder Folder the user should be reminded of * @return IEMailTemplate */ private function generateEMailTemplate(IL10N $l, array $folder): IEMailTemplate { $emailTemplate = $this->mailer->createEMailTemplate('files_sharing.SharesReminder', [ 'folder' => $folder, ]); $emailTemplate->addHeader(); $emailTemplate->setSubject( $l->t('Remember to upload the files to %s', [$folder['name']]) ); $emailTemplate->addBodyText($l->t( 'We would like to kindly remind you that you have not yet uploaded any files to the shared folder.' )); $emailTemplate->addBodyButton( $l->t('Open "%s"', [$folder['name']]), $folder['link'] ); $emailTemplate->addFooter(); return $emailTemplate; } }