migrate therinaldos.com data
Build & Deploy to DigitalOcean Space / build (push) Failing after 2m38s

This commit is contained in:
2024-05-05 15:50:45 -04:00
commit ef1ff240d4
23182 changed files with 3801898 additions and 0 deletions
@@ -0,0 +1,186 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Joas Schilling <coding@schilljs.com>
* @author John Molakvoæ <skjnldsv@protonmail.com>
* @author Thomas Citharel <nextcloud@tcit.fr>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
namespace OCA\Activity\AppInfo;
use OC\DB\ConnectionAdapter;
use OC\Files\View;
use OC\SystemConfig;
use OCA\Activity\Capabilities;
use OCA\Activity\Consumer;
use OCA\Activity\Dashboard\ActivityWidget;
use OCA\Activity\Data;
use OCA\Activity\FilesHooksStatic;
use OCA\Activity\Listener\LoadSidebarScripts;
use OCA\Activity\Listener\SetUserDefaults;
use OCA\Activity\Listener\ShareEventListener;
use OCA\Activity\Listener\UserDeleted;
use OCA\Activity\MailQueueHandler;
use OCA\Activity\NotificationGenerator;
use OCA\Files\Event\LoadSidebar;
use OCP\Activity\IManager;
use OCP\AppFramework\App;
use OCP\AppFramework\Bootstrap\IBootContext;
use OCP\AppFramework\Bootstrap\IBootstrap;
use OCP\AppFramework\Bootstrap\IRegistrationContext;
use OCP\IConfig;
use OCP\IDateTimeFormatter;
use OCP\IDBConnection;
use OCP\IURLGenerator;
use OCP\IUserManager;
use OCP\L10N\IFactory;
use OCP\Mail\IMailer;
use OCP\RichObjectStrings\IValidator;
use OCP\Share\Events\BeforeShareDeletedEvent;
use OCP\Share\Events\ShareDeletedFromSelfEvent;
use OCP\User\Events\PostLoginEvent;
use OCP\User\Events\UserDeletedEvent;
use OCP\Util;
use Psr\Container\ContainerInterface;
use Psr\Log\LoggerInterface;
class Application extends App implements IBootstrap {
public const APP_ID = 'activity';
public function __construct(array $params = []) {
parent::__construct(self::APP_ID, $params);
}
/**
* @psalm-suppress UndefinedClass
*/
public function register(IRegistrationContext $context): void {
$context->registerService('ActivityDBConnection', function (ContainerInterface $c) {
$systemConfig = $c->get(SystemConfig::class);
$factory = new \OC\DB\ConnectionFactory($systemConfig);
$type = $systemConfig->getValue('dbtype', 'sqlite');
if (!$factory->isValidType($type)) {
/** @psalm-suppress InvalidThrow */
throw new \OC\DatabaseException('Invalid database type');
}
$connectionParams = $factory->createConnectionParams('activity_');
$connection = $factory->getConnection($type, $connectionParams);
/** @psalm-suppress MissingDependency */
$connection->getConfiguration()->setSQLLogger($c->get(\OCP\Diagnostics\IQueryLogger::class));
return $connection;
});
/**
* @psalm-suppress UndefinedClass
*/
$context->registerService('ActivityConnectionAdapter', function (ContainerInterface $c) {
$systemConfig = $c->get(SystemConfig::class);
$configPrefix = 'activity_';
if ($systemConfig->getValue($configPrefix . 'dbuser', null) === null &&
$systemConfig->getValue($configPrefix . 'dbpassword', null) === null &&
$systemConfig->getValue($configPrefix . 'dbname', null) === null &&
$systemConfig->getValue($configPrefix . 'dbhost', null) === null &&
$systemConfig->getValue($configPrefix . 'dbport', null) === null &&
$systemConfig->getValue($configPrefix . 'dbdriveroptions', null) === null) {
return $c->get(IDBConnection::class);
}
return new ConnectionAdapter(
$c->get('ActivityDBConnection')
);
});
$context->registerService(Data::class, function (ContainerInterface $c) {
return new Data(
$c->get(IManager::class),
$c->get('ActivityConnectionAdapter'),
$c->get(LoggerInterface::class),
);
});
$context->registerService(MailQueueHandler::class, function (ContainerInterface $c) {
return new MailQueueHandler(
$c->get(IDateTimeFormatter::class),
$c->get('ActivityConnectionAdapter'),
$c->get(IMailer::class),
$c->get(IURLGenerator::class),
$c->get(IUserManager::class),
$c->get(IFactory::class),
$c->get(IManager::class),
$c->get(IValidator::class),
$c->get(IConfig::class),
$c->get(LoggerInterface::class),
);
});
// Allow automatic DI for the View, until we migrated to Nodes API
$context->registerService(View::class, function () {
return new View('');
}, false);
$context->registerCapability(Capabilities::class);
$context->registerEventListener(LoadSidebar::class, LoadSidebarScripts::class);
$context->registerEventListener(UserDeletedEvent::class, UserDeleted::class);
$context->registerEventListener(PostLoginEvent::class, SetUserDefaults::class);
$context->registerDashboardWidget(ActivityWidget::class);
$this->registerFilesActivity($context);
}
public function boot(IBootContext $context): void {
$this->registerActivityConsumer();
$this->registerNotifier();
}
/**
* Registers the consumer to the Activity Manager
*/
private function registerActivityConsumer() {
$c = $this->getContainer();
/** @var \OCP\IServerContainer $server */
$server = $c->getServer();
$server->getActivityManager()->registerConsumer(function () use ($c) {
return $c->query(Consumer::class);
});
}
public function registerNotifier() {
$server = $this->getContainer()->getServer();
$server->getNotificationManager()->registerNotifierService(NotificationGenerator::class);
}
/**
* Register the hooks for filesystem operations
*/
private function registerFilesActivity(IRegistrationContext $context) {
// All other events from other apps have to be send via the Consumer
Util::connectHook('OC_Filesystem', 'post_create', FilesHooksStatic::class, 'fileCreate');
Util::connectHook('OC_Filesystem', 'post_update', FilesHooksStatic::class, 'fileUpdate');
Util::connectHook('OC_Filesystem', 'delete', FilesHooksStatic::class, 'fileDelete');
Util::connectHook('OC_Filesystem', 'rename', FilesHooksStatic::class, 'fileMove');
Util::connectHook('OC_Filesystem', 'post_rename', FilesHooksStatic::class, 'fileMovePost');
Util::connectHook('\OCA\Files_Trashbin\Trashbin', 'post_restore', FilesHooksStatic::class, 'fileRestore');
Util::connectHook('OCP\Share', 'post_shared', FilesHooksStatic::class, 'share');
$context->registerEventListener(BeforeShareDeletedEvent::class, ShareEventListener::class);
$context->registerEventListener(ShareDeletedFromSelfEvent::class, ShareEventListener::class);
}
}
@@ -0,0 +1,47 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2020 Robin Appelman <robin@icewind.nl>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\Activity\BackgroundJob;
use OC\BackgroundJob\TimedJob;
use OCA\Activity\DigestSender;
use OCP\AppFramework\Utility\ITimeFactory;
class DigestMail extends TimedJob {
/** @var DigestSender */
protected $digestSender;
/** @var ITimeFactory */
protected $timeFactory;
public function __construct(DigestSender $digestSender, ITimeFactory $timeFactory) {
// run hourly
$this->setInterval(60 * 60);
$this->digestSender = $digestSender;
$this->timeFactory = $timeFactory;
}
protected function run($argument) {
$this->digestSender->sendDigests($this->timeFactory->getTime());
}
}
@@ -0,0 +1,67 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Joas Schilling <coding@schilljs.com>
* @author Morris Jobke <hey@morrisjobke.de>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
namespace OCA\Activity\BackgroundJob;
use OC\BackgroundJob\TimedJob;
use OCA\Activity\MailQueueHandler;
/**
* Class EmailNotification
*
* @package OCA\Activity\BackgroundJob
*/
class EmailNotification extends TimedJob {
/** @var MailQueueHandler */
protected $queueHandler;
/** @var bool */
protected $isCLI;
public function __construct(MailQueueHandler $mailQueueHandler,
bool $isCLI) {
// Run everytime cron is executed, so the batching doesn't delay too much
$this->setInterval(1);
$this->queueHandler = $mailQueueHandler;
$this->isCLI = $isCLI;
}
protected function run($argument) {
// We don't use time() but "time() - 1" here, so we don't run into
// runtime issues later and delete emails, which were created in the
// same second, but were not collected for the emails.
$sendTime = time() - 1;
if ($this->isCLI) {
do {
// If we are in CLI mode, we keep sending emails
// until we are done.
$emails_sent = $this->queueHandler->sendEmails(MailQueueHandler::CLI_EMAIL_BATCH_SIZE, $sendTime);
} while ($emails_sent === MailQueueHandler::CLI_EMAIL_BATCH_SIZE);
} else {
// Only send 25 Emails in one go for web cron
$this->queueHandler->sendEmails(MailQueueHandler::WEB_EMAIL_BATCH_SIZE, $sendTime);
}
}
}
@@ -0,0 +1,56 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Joas Schilling <coding@schilljs.com>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
namespace OCA\Activity\BackgroundJob;
use OCA\Activity\Data;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\BackgroundJob\TimedJob;
use OCP\IConfig;
class ExpireActivities extends TimedJob {
/** @var Data */
protected $data;
/** @var IConfig */
protected $config;
public function __construct(ITimeFactory $time,
Data $data,
IConfig $config) {
parent::__construct($time);
// Run once per day
$this->setInterval(60 * 60 * 24);
$this->setTimeSensitivity(self::TIME_INSENSITIVE);
$this->data = $data;
$this->config = $config;
}
protected function run($argument): void {
// Remove activities that are older then one year
$expireDays = $this->config->getSystemValue('activity_expire_days', 365);
$this->data->expire($expireDays);
}
}
@@ -0,0 +1,129 @@
<?php
/**
* @copyright Copyright (c) 2017 Joas Schilling <coding@schilljs.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\Activity\BackgroundJob;
use GuzzleHttp\Exception\ClientException;
use OC\BackgroundJob\QueuedJob;
use OCA\Activity\Extension\Files;
use OCP\Federation\ICloudId;
use OCP\Federation\ICloudIdManager;
use OCP\Http\Client\IClientService;
class RemoteActivity extends QueuedJob {
/** @var IClientService */
protected $clientService;
/** @var ICloudIdManager */
protected $cloudIdManager;
public function __construct(IClientService $clientService, ICloudIdManager $cloudIdManager) {
$this->clientService = $clientService;
$this->cloudIdManager = $cloudIdManager;
}
protected function run($arguments) {
call_user_func_array([$this, 'sendActivity'], $arguments);
}
protected function sendActivity($target, $token, $path, $internalType, $time, $actor, $secondPath = '') {
$client = $this->clientService->newClient();
$cloudId = $this->cloudIdManager->resolveCloudId($target);
$type = $this->translateType($internalType, $secondPath);
$fields = [
'@context' => 'https://www.w3.org/ns/activitystreams',
'to' => [
'type' => 'Person',
'name' => $cloudId->getUser(),
],
'actor' => [
'type' => 'Person',
'name' => $actor,
],
'type' => $type,
'updated' => date(\DateTime::W3C, $time),
];
if ($type === 'Move') {
$fields['target'] = [
'type' => 'Document',
'name' => $path,
];
$fields['origin'] = [
'type' => 'Document',
'name' => $secondPath,
];
} else {
$fields['object'] = [
'type' => 'Document',
'name' => $path,
];
}
try {
$client->post(
$this->getServerURL($cloudId, $token), [
'body' => $fields,
'timeout' => 10,
'connect_timeout' => 10,
]
);
} catch (ClientException $e) {
}
}
/**
* @param ICloudId $cloudId
* @param string $token
* @return string
*/
protected function getServerURL(ICloudId $cloudId, $token) {
$remote = $cloudId->getRemote();
if (strpos($remote, 'http') !== 0) {
$remote = 'https://' . $remote;
}
return rtrim($remote, '/') . '/ocs/v2.php/apps/activity/api/v2/remote/' . $token;
}
/**
* @param string $internalType
* @param string $secondPath
* @return string
*/
protected function translateType($internalType, $secondPath) {
switch ($internalType) {
case Files::TYPE_SHARE_CREATED:
case Files::TYPE_SHARE_RESTORED:
return 'Create';
case Files::TYPE_FILE_CHANGED:
if ($secondPath !== '') {
return 'Move';
}
return 'Update';
case Files::TYPE_SHARE_DELETED:
return 'Delete';
}
return '';
}
}
@@ -0,0 +1,49 @@
<?php
/**
* @copyright Copyright (c) 2017 Joas Schilling <coding@schilljs.com>
*
* @author Joas Schilling <coding@schilljs.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\Activity;
use OCP\Capabilities\ICapability;
/**
* Class Capabilities
*
* @package OCA\Activity
*/
class Capabilities implements ICapability {
/**
* Return this classes capabilities
*/
public function getCapabilities() {
return [
'activity' => [
'apiv2' => [
'filters',
'filters-api',
'previews',
'rich-strings',
],
],
];
}
}
@@ -0,0 +1,121 @@
<?php
/**
* @copyright Copyright (c) 2017 Joas Schilling <coding@schilljs.com>
*
* @author Joas Schilling <coding@schilljs.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\Activity\Command;
use OC\Core\Command\Base;
use OCA\Activity\MailQueueHandler;
use OCA\Activity\UserSettings;
use OCP\IConfig;
use Psr\Log\LoggerInterface;
use Stecman\Component\Symfony\Console\BashCompletion\CompletionContext;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
class SendEmails extends Base {
/**
* @param MailQueueHandler $queueHandler
* @param IConfig $config
* @param LoggerInterface $logger
*/
public function __construct(protected MailQueueHandler $queueHandler,
protected IConfig $config,
protected LoggerInterface $logger) {
parent::__construct();
$this->queueHandler = $queueHandler;
$this->config = $config;
$this->logger = $logger;
}
protected function configure() {
$this
->setName('activity:send-mails')
->setDescription('Sends the activity notification mails')
->addArgument(
'restrict-batching',
InputArgument::OPTIONAL,
'Only sends the emails for users which have configured the mails: "hourly", "daily" or "weekly"',
'all'
)
->addOption(
'limit',
'l',
InputOption::VALUE_REQUIRED,
'Only sends this amount of emails to give the email server some time to relax',
'unlimited'
)
;
}
/**
* @param InputInterface $input
* @param OutputInterface $output
* @return int
*/
protected function execute(InputInterface $input, OutputInterface $output): int {
// We don't use time() but "time() - 1" here, so we don't run into
// runtime issues later and delete emails, which were created in the
// same second, but were not collected for the emails.
$sendTime = time() - 1;
$restrictBatching = $input->getArgument('restrict-batching');
if ($restrictBatching === 'hourly') {
$restrictEmails = UserSettings::EMAIL_SEND_HOURLY;
} elseif ($restrictBatching === 'daily') {
$restrictEmails = UserSettings::EMAIL_SEND_DAILY;
} elseif ($restrictBatching === 'weekly') {
$restrictEmails = UserSettings::EMAIL_SEND_WEEKLY;
} elseif ($restrictBatching === 'asap') {
$restrictEmails = UserSettings::EMAIL_SEND_ASAP;
} else {
$restrictEmails = null;
}
$limit = $input->getOption('limit');
if ($limit === 'unlimited') {
do {
$emails_sent = $this->queueHandler->sendEmails(MailQueueHandler::CLI_EMAIL_BATCH_SIZE, $sendTime, true, $restrictEmails);
} while ($emails_sent === MailQueueHandler::CLI_EMAIL_BATCH_SIZE);
} else {
$this->queueHandler->sendEmails($limit, $sendTime, true, $restrictEmails);
}
return 0;
}
/**
* @param string $argumentName
* @param CompletionContext $context
* @return string[]
*/
public function completeArgumentValues($argumentName, CompletionContext $context) {
if ($argumentName === 'restrict-batching') {
return ['asap', 'hourly', 'daily', 'weekly'];
}
return [];
}
}
@@ -0,0 +1,64 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Joas Schilling <coding@schilljs.com>
* @author Thomas Müller <thomas.mueller@tmit.eu>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
namespace OCA\Activity;
use OCP\Activity\IConsumer;
use OCP\Activity\IEvent;
use OCP\Activity\IManager;
class Consumer implements IConsumer {
public function __construct(
protected Data $data,
protected IManager $manager,
protected UserSettings $userSettings,
protected NotificationGenerator $notificationGenerator) {
}
/**
* Send an event to the notifications of a user
*
* @param IEvent $event
*
* @return void
*/
public function receive(IEvent $event) {
$selfAction = $event->getAffectedUser() === $event->getAuthor();
$notificationSetting = $this->userSettings->getUserSetting($event->getAffectedUser(), 'notification', $event->getType());
$emailSetting = $this->userSettings->getUserSetting($event->getAffectedUser(), 'email', $event->getType());
$emailSetting = ($emailSetting) ? $this->userSettings->getUserSetting($event->getAffectedUser(), 'setting', 'batchtime') : false;
$activityId = $this->data->send($event);
if (!$selfAction && $notificationSetting && $activityId) {
$this->notificationGenerator->sendNotificationForEvent($event, $activityId);
}
// Add activity to mail queue and user is not the author
if ($emailSetting !== false && !$selfAction) {
$latestSend = $event->getTimestamp() + $emailSetting;
$this->data->storeMail($event, $latestSend);
}
}
}
@@ -0,0 +1,106 @@
<?php
/**
* @copyright Copyright (c) 2016 Joas Schilling <coding@schilljs.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\Activity\Controller;
use OCA\Activity\CurrentUser;
use OCA\Activity\Data;
use OCA\Activity\GroupHelper;
use OCA\Activity\UserSettings;
use OCP\AppFramework\Http\DataResponse;
use OCP\AppFramework\OCSController;
use OCP\IRequest;
class APIv1Controller extends OCSController {
/**
* @param string $appName
* @param IRequest $request
* @param Data $data
* @param GroupHelper $groupHelper
* @param UserSettings $userSettings
* @param CurrentUser $currentUser
*/
public function __construct($appName,
IRequest $request,
protected Data $data,
protected GroupHelper $groupHelper,
protected UserSettings $userSettings,
protected CurrentUser $currentUser) {
parent::__construct($appName, $request);
}
/**
* @NoAdminRequired
*
* @param int $start
* @param int $count
* @return DataResponse
*/
public function get($start = 0, $count = 30) {
if ($start !== 0) {
$start = $this->getSinceFromOffset($start);
}
$activities = $this->data->get(
$this->groupHelper,
$this->userSettings,
$this->currentUser->getUID(), $start, $count, 'desc', 'all'
);
$entries = [];
foreach ($activities['data'] as $entry) {
$entries[] = [
'id' => $entry['activity_id'],
'subject' => $entry['subject'],
'message' => $entry['message'],
'file' => $entry['object_name'],
'link' => $entry['link'],
'date' => date('c', $entry['timestamp']),
];
}
return new DataResponse($entries);
}
/**
* @param int $offset
* @return int
*/
protected function getSinceFromOffset($offset) {
$query = \OC::$server->getDatabaseConnection()->getQueryBuilder();
$query->select('activity_id')
->from('activity')
->where($query->expr()->eq('affecteduser', $query->createNamedParameter($this->currentUser->getUID())))
->orderBy('activity_id', 'desc')
->setFirstResult($offset - 1)
->setMaxResults(1);
$result = $query->execute();
$row = $result->fetch();
$result->closeCursor();
if ($row) {
return (int) $row['activity_id'];
}
return 0;
}
}
@@ -0,0 +1,366 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Joas Schilling <coding@schilljs.com>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
namespace OCA\Activity\Controller;
use OCA\Activity\Data;
use OCA\Activity\Exception\InvalidFilterException;
use OCA\Activity\GroupHelper;
use OCA\Activity\UserSettings;
use OCA\Activity\ViewInfoCache;
use OCP\Activity\IFilter;
use OCP\Activity\IManager;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\DataResponse;
use OCP\AppFramework\OCSController;
use OCP\Files\FileInfo;
use OCP\Files\IMimeTypeDetector;
use OCP\IPreview;
use OCP\IRequest;
use OCP\IURLGenerator;
use OCP\IUser;
use OCP\IUserSession;
class APIv2Controller extends OCSController {
/** @var string */
protected $filter;
/** @var int */
protected $since;
/** @var int */
protected $limit;
/** @var string */
protected $sort;
/** @var string */
protected $objectType;
/** @var int */
protected $objectId;
/** @var string */
protected $user;
/** @var bool */
protected $loadPreviews;
public function __construct($appName,
IRequest $request,
protected IManager $activityManager,
protected Data $data,
protected GroupHelper $helper,
protected UserSettings $settings,
protected IURLGenerator $urlGenerator,
protected IUserSession $userSession,
protected IPreview $preview,
protected IMimeTypeDetector $mimeTypeDetector,
protected ViewInfoCache $infoCache,
) {
parent::__construct($appName, $request);
$this->activityManager = $activityManager;
}
/**
* @param string $filter
* @param int $since
* @param int $limit
* @param bool $previews
* @param string $objectType
* @param int $objectId
* @param string $sort
* @throws InvalidFilterException when the filter is invalid
* @throws \OutOfBoundsException when no user is given
*/
protected function validateParameters($filter, $since, $limit, $previews, $objectType, $objectId, $sort) {
$this->filter = \is_string($filter) ? $filter : 'all';
if ($this->filter !== $this->data->validateFilter($this->filter)) {
throw new InvalidFilterException('Invalid filter');
}
$this->since = (int) $since;
$this->limit = (int) $limit;
$this->loadPreviews = (bool) $previews;
$this->objectType = (string) $objectType;
$this->objectId = (int) $objectId;
$this->sort = \in_array($sort, ['asc', 'desc'], true) ? $sort : 'desc';
if (($this->objectType !== '' && $this->objectId === 0) || ($this->objectType === '' && $this->objectId !== 0)) {
// Only allowed together
$this->objectType = '';
$this->objectId = 0;
}
$user = $this->userSession->getUser();
if ($user instanceof IUser) {
$this->user = $user->getUID();
} else {
// No user logged in
throw new \OutOfBoundsException('Not logged in');
}
}
/**
* @NoAdminRequired
*
* @param int $since
* @param int $limit
* @param bool $previews
* @param string $object_type
* @param int $object_id
* @param string $sort
* @return DataResponse
*/
public function getDefault($since = 0, $limit = 50, $previews = false, $object_type = '', $object_id = 0, $sort = 'desc'): DataResponse {
return $this->get('all', $since, $limit, $previews, $object_type, $object_id, $sort);
}
/**
* @NoAdminRequired
*
* @param string $filter
* @param int $since
* @param int $limit
* @param bool $previews
* @param string $object_type
* @param int $object_id
* @param string $sort
* @return DataResponse
*/
public function getFilter($filter, $since = 0, $limit = 50, $previews = false, $object_type = '', $object_id = 0, $sort = 'desc'): DataResponse {
return $this->get($filter, $since, $limit, $previews, $object_type, $object_id, $sort);
}
/**
* @NoAdminRequired
*
* @return DataResponse
*/
public function listFilters(): DataResponse {
$filters = $this->activityManager->getFilters();
$filters = array_map(function (IFilter $filter) {
return [
'id' => $filter->getIdentifier(),
'name' => $filter->getName(),
'icon' => $filter->getIcon(),
'priority' => $filter->getPriority(),
];
}, $filters);
// php 5.6 has problems with usort and objects
usort($filters, static function (array $a, array $b) {
if ($a['priority'] === $b['priority']) {
return ($a['id'] > $b['id']) ? 1 : -1;
}
return $a['priority'] - $b['priority'];
});
return new DataResponse($filters);
}
/**
* @param string $filter
* @param int $since
* @param int $limit
* @param bool $previews
* @param string $filterObjectType
* @param int $filterObjectId
* @param string $sort
* @return DataResponse
*/
protected function get($filter, $since, $limit, $previews, $filterObjectType, $filterObjectId, $sort): DataResponse {
try {
$this->validateParameters($filter, $since, $limit, $previews, $filterObjectType, $filterObjectId, $sort);
} catch (InvalidFilterException $e) {
return new DataResponse([], Http::STATUS_NOT_FOUND);
} catch (\OutOfBoundsException $e) {
return new DataResponse([], Http::STATUS_FORBIDDEN);
}
$this->activityManager->setRequirePNG($this->request->isUserAgent([IRequest::USER_AGENT_CLIENT_IOS]));
try {
$response = $this->data->get(
$this->helper,
$this->settings,
$this->user,
$this->since,
$this->limit,
$this->sort,
$this->filter,
$this->objectType,
$this->objectId
);
} catch (\OutOfBoundsException $e) {
// Invalid since argument
return new DataResponse([], Http::STATUS_FORBIDDEN);
} catch (\BadMethodCallException $e) {
// No activity settings enabled
return new DataResponse([], Http::STATUS_NO_CONTENT);
}
$this->activityManager->setRequirePNG(false);
$headers = $this->generateHeaders($response['headers'], $response['has_more'], $response['data']);
if (empty($response['data']) || $this->request->getHeader('If-None-Match') === $headers['ETag']) {
return new DataResponse([], Http::STATUS_NOT_MODIFIED, $headers);
}
$preparedActivities = [];
foreach ($response['data'] as $activity) {
$activity['datetime'] = date(\DateTime::ATOM, $activity['timestamp']);
unset($activity['timestamp']);
if ($this->loadPreviews) {
$activity['previews'] = [];
if ($activity['object_type'] === 'files') {
if (!empty($activity['objects']) && \is_array($activity['objects'])) {
foreach ($activity['objects'] as $objectId => $objectName) {
if (((int) $objectId) === 0 || $objectName === '') {
// No file, no preview
continue;
}
$activity['previews'][] = $this->getPreview($activity['affecteduser'], (int) $objectId, $objectName);
}
} elseif ($activity['object_id']) {
$activity['previews'][] = $this->getPreview($activity['affecteduser'], (int) $activity['object_id'], $activity['object_name']);
}
}
}
unset($activity['affecteduser']);
$preparedActivities[] = $activity;
}
return new DataResponse($preparedActivities, Http::STATUS_OK, $headers);
}
protected function generateHeaders(array $headers, bool $hasMoreActivities, array $data): array {
if ($hasMoreActivities && isset($headers['X-Activity-Last-Given'])) {
// Set the "Link" header for the next page
$nextPageParameters = [
'since' => $headers['X-Activity-Last-Given'],
'limit' => $this->limit,
'sort' => $this->sort,
];
if ($this->objectType && $this->objectId) {
$nextPageParameters['object_type'] = $this->objectType;
$nextPageParameters['object_id'] = $this->objectId;
}
if ($this->request->getParam('format') !== null) {
$nextPageParameters['format'] = $this->request->getParam('format');
}
$nextPage = $this->request->getServerProtocol(); # http
$nextPage .= '://' . $this->request->getServerHost(); # localhost
$nextPage .= $this->request->getScriptName(); # /ocs/v2.php
$nextPage .= $this->request->getPathInfo(); # /apps/activity/api/v2/activity
$nextPage .= '?' . http_build_query($nextPageParameters);
$headers['Link'] = '<' . $nextPage . '>; rel="next"';
}
$ids = [];
foreach ($data as $activity) {
$ids[] = $activity['activity_id'];
}
$headers['ETag'] = md5(json_encode($ids));
return $headers;
}
protected function getPreview(string $owner, int $fileId, string $filePath): array {
$info = $this->infoCache->getInfoById($owner, $fileId, $filePath);
if (!$info['exists'] || $info['view'] !== '') {
return $this->getPreviewFromPath($fileId, $filePath, $info);
}
$preview = [
'link' => $this->urlGenerator->linkToRouteAbsolute('files.viewcontroller.showFile', ['fileid' => $fileId]),
'source' => '',
'mimeType' => 'application/octet-stream',
'isMimeTypeIcon' => true,
'fileId' => $fileId,
'view' => 'files',
'filename' => basename($filePath),
];
// show a preview image if the file still exists
if ($info['is_dir']) {
$preview['source'] = $this->getPreviewPathFromMimeType('dir');
$preview['mimeType'] = 'dir';
} else {
$fileInfo = $info['node'] ?? null;
if (!($fileInfo instanceof FileInfo)) {
return $this->getPreviewFromPath($fileId, $filePath, $info);
}
$preview['filePath'] = $fileInfo->getPath();
if ($this->preview->isAvailable($fileInfo)) {
$params = [
'forceIcon' => 0,
'a' => 0,
'x' => 250,
'y' => 250,
'fileId' => $fileId,
'c' => $fileInfo->getEtag(),
];
$preview['source'] = $this->urlGenerator->linkToRouteAbsolute('core.Preview.getPreviewByFileId', $params);
$preview['mimeType'] = $fileInfo->getMimetype() ?: 'application/octet-stream';
$preview['isMimeTypeIcon'] = false;
} else {
$preview['mimeType'] = $fileInfo->getMimetype() ?: 'application/octet-stream';
$preview['source'] = $this->getPreviewPathFromMimeType($preview['mimeType']);
}
}
return $preview;
}
protected function getPreviewFromPath(int $fileId, string $filePath, array $info): array {
$mimeType = $info['is_dir'] ? 'dir' : $this->mimeTypeDetector->detectPath($filePath);
return [
'link' => $this->urlGenerator->linkToRouteAbsolute('files.viewcontroller.showFile', ['fileid' => $fileId]),
'source' => $this->getPreviewPathFromMimeType($mimeType),
'mimeType' => $mimeType,
'isMimeTypeIcon' => true,
'fileId' => $fileId,
'view' => $info['view'] ?: 'files',
'filename' => basename($filePath),
];
}
protected function getPreviewPathFromMimeType(string $mimeType): string {
$mimeTypeIcon = $this->mimeTypeDetector->mimeTypeIcon($mimeType);
if (substr($mimeTypeIcon, -4) === '.png') {
$mimeTypeIcon = substr($mimeTypeIcon, 0, -4) . '.svg';
}
return $this->urlGenerator->getAbsoluteURL($mimeTypeIcon);
}
}
@@ -0,0 +1,147 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Joas Schilling <coding@schilljs.com>
* @author Ferdinand Thiessen <opensource@fthiessen.de>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
namespace OCA\Activity\Controller;
use OCA\Activity\Data;
use OCA\Activity\Event\LoadAdditionalScriptsEvent;
use OCA\Viewer\Event\LoadViewer;
use OCP\Activity\IFilter;
use OCP\Activity\IManager;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http\TemplateResponse;
use OCP\AppFramework\Services\IInitialState;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\IConfig;
use OCP\IL10N;
use OCP\IRequest;
use OCP\IURLGenerator;
class ActivitiesController extends Controller {
public function __construct(
string $appName,
IRequest $request,
private ?string $userId,
private IConfig $config,
private Data $data,
private IL10N $l10n,
private IEventDispatcher $eventDispatcher,
private IInitialState $initialState,
private IURLGenerator $urlGenerator,
private IManager $activityManager,
) {
parent::__construct($appName, $request);
}
/**
* @NoAdminRequired
* @NoCSRFRequired
*
* @param string $filter
* @return TemplateResponse
*/
public function index(): TemplateResponse {
return $this->showList('all');
}
/**
* @NoAdminRequired
* @NoCSRFRequired
*
* @param string $filter
* @return TemplateResponse
*/
public function showList(string $filter = 'all'): TemplateResponse {
$filter = $this->data->validateFilter($filter);
$event = new LoadAdditionalScriptsEvent($filter);
$this->eventDispatcher->dispatchTyped($event);
$this->eventDispatcher->dispatch(LoadAdditionalScriptsEvent::EVENT_ENTITY, $event);
// Load the viewer
if (class_exists(LoadViewer::class)) {
$this->eventDispatcher->dispatchTyped(new LoadViewer());
}
$this->initialState->provideInitialState('settings', [
'enableAvatars' => $this->config->getSystemValue('enable_avatars', true),
'personalSettingsLink' => $this->getPersonalSettingsLink(),
'rssLink' => $this->getRSSLink(),
]);
$this->initialState->provideInitialState('filter', $filter);
$this->initialState->provideInitialState('navigationList', $this->getLinkList());
\OCP\Util::addScript($this->appName, 'activity-app');
\OCP\Util::addStyle($this->appName, 'style');
return new TemplateResponse($this->appName, 'app-main');
}
/**
* Get link for personal settings
*/
protected function getPersonalSettingsLink(): string {
return $this->urlGenerator->linkToRouteAbsolute('settings.PersonalSettings.index', ['section' => 'notifications']);
}
/**
* Link to RSS feed if there is a RSS token, empty string otherwise
*/
protected function getRSSLink(): string {
$rssToken = $this->config->getUserValue($this->userId, 'activity', 'rsstoken');
if ($rssToken) {
return $this->urlGenerator->linkToRouteAbsolute('activity.Feed.show', ['token' => $rssToken]);
} else {
return '';
}
}
/**
* Get all items for the users we want to send an email to
*
* @return array Notification data (user => array of rows from the table)
*/
protected function getLinkList(): array {
$filters = $this->activityManager->getFilters();
usort($filters, static function (IFilter $a, IFilter $b) {
if ($a->getPriority() === $b->getPriority()) {
return (int) ($a->getIdentifier() > $b->getIdentifier());
}
return (int) ($a->getPriority() > $b->getPriority());
});
$entries = [];
foreach ($filters as $filter) {
$entries[] = [
'id' => $filter->getIdentifier(),
'icon' => $filter->getIcon(),
'name' => $filter->getName(),
'url' => $this->urlGenerator->linkToRoute('activity.Activities.showList', ['filter' => $filter->getIdentifier()]),
];
}
return $entries;
}
}
@@ -0,0 +1,109 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Joas Schilling <coding@schilljs.com>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
namespace OCA\Activity\Controller;
use OCA\Activity\Data;
use OCA\Activity\GroupHelper;
use OCA\Activity\UserSettings;
use OCA\Theming\ThemingDefaults;
use OCP\Activity\IManager;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http\TemplateResponse;
use OCP\IConfig;
use OCP\IL10N;
use OCP\IRequest;
use OCP\IURLGenerator;
use OCP\L10N\IFactory;
class FeedController extends Controller {
public const DEFAULT_PAGE_SIZE = 30;
protected IL10N $l;
public function __construct(
string $appName,
IRequest $request,
protected Data $data,
protected GroupHelper $helper,
protected UserSettings $settings,
protected IURLGenerator $urlGenerator,
protected IManager $activityManager,
protected IFactory $l10nFactory,
protected IConfig $config,
protected ThemingDefaults $themingDefaults,
) {
parent::__construct($appName, $request);
}
/**
* @PublicPage
* @NoCSRFRequired
*
* @return TemplateResponse
*/
public function show() {
try {
$user = $this->activityManager->getCurrentUserId();
$userLang = $this->config->getUserValue($user, 'core', 'lang');
// Overwrite user and language in the helper
$this->l = $this->l10nFactory->get('activity', $userLang);
$this->helper->setL10n($this->l);
$description = $this->l->t('Personal activity feed for %s', $user);
$response = $this->data->get($this->helper, $this->settings, $user, 0, self::DEFAULT_PAGE_SIZE, 'desc', 'all');
$activities = $response['data'];
} catch (\UnexpectedValueException $e) {
$this->l = $this->l10nFactory->get('activity');
$description = $this->l->t('Your feed URL is invalid');
$activities = [
[
'activity_id' => -1,
'timestamp' => time(),
'subject' => true,
'subject_prepared' => $description,
]
];
}
$title = $this->themingDefaults->getTitle();
$response = new TemplateResponse('activity', 'rss', [
'rssLang' => $this->l->getLanguageCode(),
'rssLink' => $this->urlGenerator->linkToRouteAbsolute('activity.Feed.show'),
'rssPubDate' => date('r'),
'description' => $description,
'title' => $title !== '' ? $this->l->t('Activity feed for %1$s', [$title]) : $this->l->t('Activity feed'),
'activities' => $activities,
], '');
if (stristr($this->request->getHeader('accept'), 'application/rss+xml')) {
$response->addHeader('Content-Type', 'application/rss+xml');
} else {
$response->addHeader('Content-Type', 'text/xml; charset=UTF-8');
}
return $response;
}
}
@@ -0,0 +1,218 @@
<?php
/**
* @copyright Copyright (c) 2017 Joas Schilling <coding@schilljs.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\Activity\Controller;
use OCA\Activity\Extension\Files;
use OCP\Activity\IManager as IActivityManager;
use OCP\App\IAppManager;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\DataResponse;
use OCP\AppFramework\OCSController;
use OCP\Files\InvalidPathException;
use OCP\Files\IRootFolder;
use OCP\Files\NotFoundException;
use OCP\IDBConnection;
use OCP\IRequest;
use OCP\IUser;
use OCP\IUserManager;
class RemoteActivityController extends OCSController {
public function __construct($appName,
IRequest $request,
protected IDBConnection $db,
protected IUserManager $userManager,
protected IAppManager $appManager,
protected IRootFolder $rootFolder,
protected IActivityManager $activityManager) {
parent::__construct($appName, $request);
}
/**
* @PublicPage
* @NoCSRFRequired
*
* @param string $token
* @param string[] $to
* @param string[] $actor
* @param string $type
* @param string $updated
* @param string[] $object
* @param string[] $target
* @param string[] $origin
* @return DataResponse
*/
public function receiveActivity($token, array $to, array $actor, $type, $updated, array $object = [], array $target = [], array $origin = []) {
$date = \DateTime::createFromFormat(\DateTime::W3C, $updated);
if ($date === false) {
return new DataResponse([], Http::STATUS_BAD_REQUEST);
}
$time = $date->getTimestamp();
if (!isset($to['type'], $to['name']) || $to['type'] !== 'Person') {
return new DataResponse([], Http::STATUS_BAD_REQUEST);
}
$user = $this->userManager->get($to['name']);
if (!$user instanceof IUser) {
return new DataResponse([], Http::STATUS_NOT_FOUND);
}
if (!isset($actor['type'], $actor['name']) || $actor['type'] !== 'Person') {
return new DataResponse([], Http::STATUS_BAD_REQUEST);
}
if ($user->getCloudId() === $actor['name']) {
return new DataResponse([], Http::STATUS_BAD_REQUEST);
}
if (!$this->appManager->isInstalled('federatedfilesharing')) {
return new DataResponse([], Http::STATUS_NOT_FOUND);
}
$query = $this->db->getQueryBuilder();
$query->select('*')
->from('share_external')
->where($query->expr()->eq('share_token', $query->createNamedParameter($token)))
->andWhere($query->expr()->eq('user', $query->createNamedParameter($user->getUID())));
$result = $query->execute();
$share = $result->fetch();
$result->closeCursor();
if (!is_array($share) || strpos($share['mountpoint'], '{{TemporaryMountPointName#') === 0) {
return new DataResponse([], Http::STATUS_NOT_FOUND);
}
$internalType = $this->translateType($type);
if ($internalType === '') {
return new DataResponse([], Http::STATUS_BAD_REQUEST);
}
$path2 = null;
if ($type === 'Move') {
if (!isset($target['type'], $target['name']) || $target['type'] !== 'Document') {
return new DataResponse([], Http::STATUS_BAD_REQUEST);
}
if (!isset($origin['type'], $origin['name']) || $origin['type'] !== 'Document') {
return new DataResponse([], Http::STATUS_BAD_REQUEST);
}
$path = $share['mountpoint'] . $target['name'];
$path2 = $share['mountpoint'] . $origin['name'];
} else {
if (!isset($object['type'], $object['name']) || $object['type'] !== 'Document') {
return new DataResponse([], Http::STATUS_BAD_REQUEST);
}
$path = $share['mountpoint'] . $object['name'];
}
$subject = $this->getSubject($type, $path, $path2);
if ($subject === '') {
return new DataResponse([], Http::STATUS_BAD_REQUEST);
}
$userFolder = $this->rootFolder->getUserFolder($user->getUID());
try {
$node = $userFolder->get($path);
$fileId = $node->getId();
} catch (NotFoundException $e) {
return new DataResponse([], Http::STATUS_NOT_FOUND);
} catch (InvalidPathException $e) {
return new DataResponse([], Http::STATUS_NOT_FOUND);
}
if ($path2 !== null) {
$secondPath = [$fileId => $path2];
if ($subject === 'moved_by') {
try {
$parent = $node->getParent();
$secondPath = [$parent->getId() => dirname($path2)];
} catch (NotFoundException $e) {
} catch (InvalidPathException $e) {
}
}
$subjectParams = [$secondPath, $actor['name'], [$fileId => $path]];
} else {
$subjectParams = [[$fileId => $path], $actor['name']];
}
$event = $this->activityManager->generateEvent();
try {
$event->setAffectedUser($user->getUID())
->setApp('files')
->setType($internalType)
->setAuthor($actor['name'])
->setObject('files', $fileId, $path)
->setSubject($subject, $subjectParams)
->setTimestamp($time);
$this->activityManager->publish($event);
} catch (\InvalidArgumentException $e) {
return new DataResponse(['activity'], Http::STATUS_BAD_REQUEST);
} catch (\BadMethodCallException $e) {
return new DataResponse(['sending'], Http::STATUS_BAD_REQUEST);
}
return new DataResponse();
}
/**
* @param null|string $path2
*/
protected function getSubject(string $type, string $path, string|null $path2) {
switch ($type) {
case 'Create':
return 'created_by';
case 'Move':
if ($path2 === null) {
return '';
}
if (basename($path) === basename($path2)) {
return 'moved_by';
}
return 'renamed_by';
case 'Update':
return 'changed_by';
case 'Delete':
return 'deleted_by';
}
return '';
}
/**
* @param string $type
* @return string
*/
protected function translateType($type) {
switch ($type) {
case 'Create':
return Files::TYPE_SHARE_CREATED;
case 'Move':
case 'Update':
return Files::TYPE_FILE_CHANGED;
case 'Delete':
return Files::TYPE_SHARE_DELETED;
}
return '';
}
}
@@ -0,0 +1,213 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Joas Schilling <coding@schilljs.com>
* @author Lukas Reschke <lukas@statuscode.ch>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
namespace OCA\Activity\Controller;
use OCA\Activity\CurrentUser;
use OCA\Activity\UserSettings;
use OCP\Activity\IManager;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http\DataResponse;
use OCP\IConfig;
use OCP\IL10N;
use OCP\IRequest;
use OCP\IURLGenerator;
use OCP\Security\ISecureRandom;
class SettingsController extends Controller {
protected string $user;
public function __construct(
string $appName,
IRequest $request,
protected IConfig $config,
protected ISecureRandom $random,
protected IURLGenerator $urlGenerator,
protected IManager $manager,
protected UserSettings $userSettings,
protected IL10N $l10n,
CurrentUser $currentUser) {
parent::__construct($appName, $request);
$this->user = (string) $currentUser->getUID();
}
/**
* @NoAdminRequired
*
* @param int $notify_setting_batchtime
* @param bool $notify_setting_self
* @param bool $notify_setting_selfemail
* @param bool $activity_digest
* @return DataResponse
*/
public function personal(
$notify_setting_batchtime = UserSettings::EMAIL_SEND_HOURLY,
$notify_setting_self = false,
$notify_setting_selfemail = false,
$activity_digest = false
) {
$settings = $this->manager->getSettings();
foreach ($settings as $setting) {
$this->config->setUserValue(
$this->user, 'activity',
'notify_notification_' . $setting->getIdentifier(),
(string)(int) $this->request->getParam($setting->getIdentifier() . '_notification', false)
);
if ($setting->canChangeMail()) {
$this->config->setUserValue(
$this->user, 'activity',
'notify_email_' . $setting->getIdentifier(),
(string)(int) $this->request->getParam($setting->getIdentifier() . '_email', false)
);
}
}
$email_batch_time = 3600;
if ($notify_setting_batchtime === UserSettings::EMAIL_SEND_DAILY) {
$email_batch_time = 3600 * 24;
} elseif ($notify_setting_batchtime === UserSettings::EMAIL_SEND_WEEKLY) {
$email_batch_time = 3600 * 24 * 7;
} elseif ($notify_setting_batchtime === UserSettings::EMAIL_SEND_ASAP) {
$email_batch_time = 0;
}
$this->config->setUserValue(
$this->user, 'activity',
'notify_setting_batchtime',
(string)$email_batch_time
);
$this->config->setUserValue(
$this->user, 'activity',
'notify_setting_self',
(string)(int) $notify_setting_self
);
$this->config->setUserValue(
$this->user, 'activity',
'notify_setting_selfemail',
(string)(int) $notify_setting_selfemail
);
$this->config->setUserValue(
$this->user, 'activity',
'notify_setting_activity_digest',
(string)(int) $activity_digest
);
return new DataResponse([
'data' => [
'message' => $this->l10n->t('Your settings have been updated.'),
],
]);
}
/**
* @param int $notify_setting_batchtime
* @param bool $notify_setting_self
* @param bool $notify_setting_selfemail
* @return DataResponse
*/
public function admin(
$notify_setting_batchtime = UserSettings::EMAIL_SEND_HOURLY,
$notify_setting_self = false,
$notify_setting_selfemail = false) {
$settings = $this->manager->getSettings();
foreach ($settings as $setting) {
$this->config->setAppValue(
'activity',
'notify_notification_' . $setting->getIdentifier(),
(string)(int)$this->request->getParam($setting->getIdentifier() . '_notification', false)
);
if ($setting->canChangeMail()) {
$this->config->setAppValue(
'activity',
'notify_email_' . $setting->getIdentifier(),
(string)(int) $this->request->getParam($setting->getIdentifier() . '_email', false)
);
}
}
$email_batch_time = 3600;
if ($notify_setting_batchtime === UserSettings::EMAIL_SEND_DAILY) {
$email_batch_time = 3600 * 24;
} elseif ($notify_setting_batchtime === UserSettings::EMAIL_SEND_WEEKLY) {
$email_batch_time = 3600 * 24 * 7;
} elseif ($notify_setting_batchtime === UserSettings::EMAIL_SEND_ASAP) {
$email_batch_time = 0;
}
$this->config->setAppValue(
'activity',
'notify_setting_batchtime',
(string)$email_batch_time
);
$this->config->setAppValue(
'activity',
'notify_setting_self',
(string)(int) $notify_setting_self
);
$this->config->setAppValue(
'activity',
'notify_setting_selfemail',
(string)(int) $notify_setting_selfemail
);
return new DataResponse([
'data' => [
'message' => $this->l10n->t('Settings have been updated.'),
],
]);
}
/**
* @NoAdminRequired
*
* @param bool $enable true if the feed is enabled
* @return DataResponse
*/
public function feed(bool $enable) {
$token = $tokenUrl = '';
if ($enable === true) {
$conflicts = true;
// Check for collisions
while (!empty($conflicts)) {
$token = $this->random->generate(30, ISecureRandom::CHAR_UPPER . ISecureRandom::CHAR_LOWER . ISecureRandom::CHAR_DIGITS);
$conflicts = $this->config->getUsersForUserValue('activity', 'rsstoken', $token);
}
$tokenUrl = $this->urlGenerator->linkToRouteAbsolute('activity.Feed.show', ['token' => $token]);
}
$this->config->setUserValue($this->user, 'activity', 'rsstoken', $token);
return new DataResponse([
'data' => [
'message' => $this->l10n->t('Your settings have been updated.'),
'rsslink' => trim($tokenUrl),
],
]);
}
}
@@ -0,0 +1,135 @@
<?php
/**
* @copyright Copyright (c) 2016 Joas Schilling <coding@schilljs.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\Activity;
use OCP\IRequest;
use OCP\IUser;
use OCP\IUserSession;
use OCP\Share\Exceptions\ShareNotFound;
use OCP\Share\IManager;
use OCP\Share\IShare;
class CurrentUser {
/** @var string */
protected $identifier;
/** @var string|null */
protected $cloudId;
/** @var string|false|null */
protected $sessionUser;
/**
* @param IUserSession $userSession
* @param IRequest $request
* @param IManager $shareManager
*/
public function __construct(
protected IUserSession $userSession,
protected IRequest $request,
protected IManager $shareManager) {
$this->cloudId = false;
$this->sessionUser = false;
}
public function getUser(): ?IUser {
return $this->userSession->getUser();
}
/**
* Get an identifier for the user, session or token
* @return string
*/
public function getUserIdentifier() {
if ($this->identifier === null) {
$this->identifier = $this->getUID();
if ($this->identifier === null) {
$this->identifier = $this->getCloudIDFromToken();
if ($this->identifier === null) {
// Nothing worked, fallback to empty string
$this->identifier = '';
}
}
}
return $this->identifier;
}
/**
* Get the current user id from the session
* @return string|null
*/
public function getUID() {
if ($this->sessionUser === false) {
$user = $this->userSession->getUser();
if ($user instanceof IUser) {
$this->sessionUser = (string) $user->getUID();
} else {
$this->sessionUser = null;
}
}
return $this->sessionUser;
}
/**
* Get the current user cloud id from the session
* @return string|null
*/
public function getCloudId() {
if ($this->cloudId === false) {
$user = $this->userSession->getUser();
if ($user instanceof IUser) {
$this->cloudId = (string) $user->getCloudId();
} else {
$this->cloudId = $this->getCloudIDFromToken();
}
}
return $this->cloudId;
}
/**
* Get the cloud ID from the sharing token
* @return string|null
*/
protected function getCloudIDFromToken() {
if (!empty($this->request->server['PHP_AUTH_USER'])) {
$token = $this->request->server['PHP_AUTH_USER'];
/**
* Until https://github.com/nextcloud/server/pull/26681 is merged
* @psalm-suppress InvalidCatch
*/
try {
$share = $this->shareManager->getShareByToken($token);
if ($share->getShareType() === IShare::TYPE_REMOTE) {
return $share->getSharedWith();
}
} catch (ShareNotFound $e) {
// No share, use the fallback
}
}
return null;
}
}
@@ -0,0 +1,210 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2021 Jakob Röhrl <jakob.roehrl@web.de>
*
* @author Jakob Röhrl <jakob.roehrl@web.de>
* @author Richard Steinmetz <richard@steinmetz.cloud>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\Activity\Dashboard;
use OCA\Activity\AppInfo\Application;
use OCA\Activity\Data;
use OCA\Activity\GroupHelper;
use OCA\Activity\UserSettings;
use OCP\Dashboard\IAPIWidget;
use OCP\Dashboard\IButtonWidget;
use OCP\Dashboard\IIconWidget;
use OCP\Dashboard\IReloadableWidget;
use OCP\Dashboard\Model\WidgetButton;
use OCP\Dashboard\Model\WidgetItem;
use OCP\Dashboard\Model\WidgetItems;
use OCP\IDateTimeFormatter;
use OCP\IL10N;
use OCP\IURLGenerator;
class ActivityWidget implements IAPIWidget, IButtonWidget, IIconWidget, IReloadableWidget {
private Data $data;
private IL10N $l10n;
private GroupHelper $helper;
private UserSettings $settings;
private IDateTimeFormatter $dateTimeFormatter;
private IURLGenerator $urlGenerator;
public function __construct(IL10N $l10n,
Data $data,
GroupHelper $helper,
UserSettings $settings,
IURLGenerator $urlGenerator,
IDateTimeFormatter $dateTimeFormatter) {
$this->data = $data;
$this->l10n = $l10n;
$this->helper = $helper;
$this->settings = $settings;
$this->dateTimeFormatter = $dateTimeFormatter;
$this->urlGenerator = $urlGenerator;
}
/**
* @inheritDoc
*/
public function getId(): string {
return Application::APP_ID;
}
/**
* @inheritDoc
*/
public function getTitle(): string {
return $this->l10n->t('Recent activity');
}
/**
* @inheritDoc
*/
public function getOrder(): int {
return 20;
}
/**
* @inheritDoc
*/
public function getIconClass(): string {
return 'icon-activity';
}
/**
* @inheritDoc
*/
public function getIconUrl(): string {
return $this->urlGenerator->getAbsoluteURL(
$this->urlGenerator->imagePath(Application::APP_ID, 'activity-dark.svg')
);
}
/**
* @inheritDoc
*/
public function getUrl(): ?string {
return $this->urlGenerator->getAbsoluteURL(
$this->urlGenerator->linkToRoute(Application::APP_ID . '.Activities.index')
);
}
/**
* @inheritDoc
*/
public function load(): void {
}
/**
* @inheritDoc
*/
public function getItems(string $userId, ?string $since = null, int $limit = 7): array {
// we set the limit to 50 here because data->get might return less activity entries
// in the end we take the first 7 of'em
$activities = $this->data->get(
$this->helper,
$this->settings,
$userId,
$since ? (int) $since : 0,
50,
'desc',
'by',
'',
0
);
return array_map(function (array $activity) {
return new WidgetItem(
$activity['subject'],
$this->dateTimeFormatter->formatTimeSpan($activity['timestamp']),
$activity['link'],
$activity['icon'],
(string) $activity['activity_id']
);
}, array_slice($activities['data'], 0, $limit));
}
/**
* @inheritDoc
*/
public function getItemsV2(string $userId, ?string $since = null, int $limit = 7): WidgetItems {
// we set the limit to 50 here because data->get might return less activity entries
// in the end we take the first 7 of'em
$activities = $this->data->get(
$this->helper,
$this->settings,
$userId,
$since ? (int) $since : 0,
50,
'desc',
'by',
'',
0
);
$items = array_map(function (array $activity) {
$userAvatarUrl = '';
if ($activity['user'] !== '') {
$userAvatarUrl = $this->urlGenerator->getAbsoluteURL(
$this->urlGenerator->linkToRoute('core.avatar.getAvatar', [
'userId' => $activity['user'],
'size' => 512,
])
);
}
return new WidgetItem(
$activity['subject'],
$this->dateTimeFormatter->formatTimeSpan($activity['timestamp']),
$activity['link'],
$userAvatarUrl,
(string) $activity['activity_id'],
$activity['icon'],
);
}, array_slice($activities['data'], 0, $limit));
return new WidgetItems(
$items,
empty($items) ? $this->l10n->t('No activities') : '',
);
}
/**
* @inheritDoc
*/
public function getWidgetButtons(string $userId): array {
return [
new WidgetButton(
WidgetButton::TYPE_MORE,
$this->urlGenerator->getAbsoluteURL(
$this->urlGenerator->linkToRoute(Application::APP_ID . '.Activities.index')
),
$this->l10n->t('More activities')
),
];
}
/**
* @inheritDoc
*/
public function getReloadInterval(): int {
return 30;
}
}
+505
View File
@@ -0,0 +1,505 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Frank Karlitschek <frank@karlitschek.de>
* @author Joas Schilling <coding@schilljs.com>
* @author Thomas Müller <thomas.mueller@tmit.eu>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
namespace OCA\Activity;
use Doctrine\DBAL\Platforms\MySQLPlatform;
use OCA\Activity\Filter\AllFilter;
use OCP\Activity\IEvent;
use OCP\Activity\IExtension;
use OCP\Activity\IFilter;
use OCP\Activity\IManager;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\IDBConnection;
use Psr\Log\LoggerInterface;
/**
* @brief Class for managing the data in the activities
*/
class Data {
/** @var */
protected ?IQueryBuilder $insertActivity = null;
protected ?IQueryBuilder $insertMail = null;
public function __construct(
protected IManager $activityManager,
protected IDBConnection $connection,
protected LoggerInterface $logger) {
}
/**
* Send an event into the activity stream
*
* @param IEvent $event
* @return int
*/
public function send(IEvent $event): int {
if ($event->getAffectedUser() === '') {
return 0;
}
if ($this->insertActivity === null) {
$this->insertActivity = $this->connection->getQueryBuilder();
$this->insertActivity->insert('activity')
->values([
'app' => $this->insertActivity->createParameter('app'),
'subject' => $this->insertActivity->createParameter('subject'),
'subjectparams' => $this->insertActivity->createParameter('subjectparams'),
'message' => $this->insertActivity->createParameter('message'),
'messageparams' => $this->insertActivity->createParameter('messageparams'),
'file' => $this->insertActivity->createParameter('object_name'),
'link' => $this->insertActivity->createParameter('link'),
'user' => $this->insertActivity->createParameter('user'),
'affecteduser' => $this->insertActivity->createParameter('affecteduser'),
'timestamp' => $this->insertActivity->createParameter('timestamp'),
'priority' => $this->insertActivity->createParameter('priority'),
'type' => $this->insertActivity->createParameter('type'),
'object_type' => $this->insertActivity->createParameter('object_type'),
'object_id' => $this->insertActivity->createParameter('object_id'),
]);
}
// store in DB
$this->insertActivity->setParameters([
'app' => $event->getApp(),
'type' => $event->getType(),
'affecteduser' => $event->getAffectedUser(),
'user' => $event->getAuthor(),
'timestamp' => $event->getTimestamp(),
'subject' => $event->getSubject(),
'subjectparams' => json_encode($event->getSubjectParameters()),
'message' => $event->getMessage(),
'messageparams' => json_encode($event->getMessageParameters()),
'priority' => IExtension::PRIORITY_MEDIUM,
'object_type' => $event->getObjectType(),
'object_id' => $event->getObjectId(),
'object_name' => $event->getObjectName(),
'link' => $event->getLink(),
]);
$this->insertActivity->executeStatement();
return $this->insertActivity->getLastInsertId();
}
/**
* Send an event as email
*
* @param IEvent $event
* @param int $latestSendTime Activity $timestamp + batch setting of $affectedUser
* @return bool
*/
public function storeMail(IEvent $event, int $latestSendTime): bool {
$affectedUser = $event->getAffectedUser();
if ($affectedUser === '') {
return false;
}
if ($this->insertMail === null) {
$this->insertMail = $this->connection->getQueryBuilder();
$this->insertMail->insert('activity_mq')
->values([
'amq_appid' => $this->insertMail->createParameter('amq_appid'),
'amq_subject' => $this->insertMail->createParameter('amq_subject'),
'amq_subjectparams' => $this->insertMail->createParameter('amq_subjectparams'),
'amq_affecteduser' => $this->insertMail->createParameter('amq_affecteduser'),
'amq_timestamp' => $this->insertMail->createParameter('amq_timestamp'),
'amq_type' => $this->insertMail->createParameter('amq_type'),
'amq_latest_send' => $this->insertMail->createParameter('amq_latest_send'),
'object_type' => $this->insertMail->createParameter('object_type'),
'object_id' => $this->insertMail->createParameter('object_id'),
]);
}
$this->insertMail->setParameters([
'amq_appid' => $event->getApp(),
'amq_subject' => $event->getSubject(),
'amq_subjectparams' => json_encode($event->getSubjectParameters()),
'amq_affecteduser' => $affectedUser,
'amq_timestamp' => $event->getTimestamp(),
'amq_type' => $event->getType(),
'amq_latest_send' => $latestSendTime,
'object_type' => $event->getObjectType(),
'object_id' => $event->getObjectId(),
]);
$this->insertMail->executeStatement();
return true;
}
/**
* Read a list of events from the activity stream
*
* @param GroupHelper $groupHelper Allows activities to be grouped
* @param UserSettings $userSettings Gets the settings of the user
* @param string $user User for whom we display the stream
*
* @param int $since The integer ID of the last activity that has been seen.
* @param int $limit How many activities should be returned
* @param string $sort Should activities be given ascending or descending
*
* @param string $filter Filter the activities
* @param string $objectType Allows to filter the activities to a given object. May only appear together with $objectId
* @param int $objectId Allows to filter the activities to a given object. May only appear together with $objectType
*
* @param bool $returnEvents return only the events
* @return array
*
*/
public function get(GroupHelper $groupHelper, UserSettings $userSettings, $user, $since, $limit, $sort, $filter, $objectType = '', $objectId = 0, bool $returnEvents = false) {
// get current user
if ($user === '') {
throw new \OutOfBoundsException('Invalid user', 1);
}
$limit = min(200, $limit);
$activeFilter = null;
try {
$activeFilter = $this->activityManager->getFilterById($filter);
} catch (\InvalidArgumentException $e) {
// Unknown filter => ignore and show all activities
}
$query = $this->connection->getQueryBuilder();
$query->select('*')
->from('activity');
$query->where($query->expr()->eq('affecteduser', $query->createNamedParameter($user)));
if ($activeFilter instanceof IFilter && !($activeFilter instanceof AllFilter)) {
$notificationTypes = $userSettings->getNotificationTypes();
$notificationTypes = $activeFilter->filterTypes($notificationTypes);
$notificationTypes = array_unique($notificationTypes);
$query->andWhere($query->expr()->in('type', $query->createNamedParameter($notificationTypes, IQueryBuilder::PARAM_STR_ARRAY)));
}
if ($filter === 'self') {
$query->andWhere($query->expr()->eq('user', $query->createNamedParameter($user)));
} elseif ($filter === 'by') {
$query->andWhere($query->expr()->neq('user', $query->createNamedParameter($user)));
} elseif ($filter === 'filter') {
$query->andWhere($query->expr()->eq('object_type', $query->createNamedParameter($objectType)));
$query->andWhere($query->expr()->eq('object_id', $query->createNamedParameter($objectId)));
}
if ($activeFilter instanceof IFilter) {
$apps = $activeFilter->allowedApps();
if (!empty($apps)) {
$query->andWhere($query->expr()->in('app', $query->createNamedParameter($apps, IQueryBuilder::PARAM_STR_ARRAY)));
}
}
if (
$filter === 'files_favorites' ||
(in_array($filter, ['all', 'by', 'self']) && $userSettings->getUserSetting($user, 'stream', 'files_favorites'))
) {
try {
$favoriteFilter = $this->activityManager->getFilterById('files_favorites');
/** @var \OCA\Files\Activity\Filter\Favorites $favoriteFilter */
$favoriteFilter->filterFavorites($query);
} catch (\InvalidArgumentException $e) {
}
}
/**
* Order and specify the offset
*/
$sqlSort = ($sort === 'asc') ? 'ASC' : 'DESC';
$headers = $this->setOffsetFromSince($query, $user, $since, $sqlSort);
$query->orderBy('timestamp', $sqlSort)
->addOrderBy('activity_id', $sqlSort);
$query->setMaxResults($limit + 1);
$result = $query->execute();
$hasMore = false;
while ($row = $result->fetch()) {
if ($limit === 0) {
$hasMore = true;
break;
}
$headers['X-Activity-Last-Given'] = (int)$row['activity_id'];
$groupHelper->addActivity($row);
$limit--;
}
$result->closeCursor();
if ($returnEvents) {
return $groupHelper->getEvents();
} else {
return ['data' => $groupHelper->getActivities(), 'has_more' => $hasMore, 'headers' => $headers];
}
}
/**
* @param IQueryBuilder $query
* @param string $user
* @param int $since
* @param string $sort
*
* @return array Headers that should be set on the response
*
* @throws \OutOfBoundsException If $since is not owned by $user
*/
protected function setOffsetFromSince(IQueryBuilder $query, $user, $since, $sort) {
if ($since) {
$queryBuilder = $this->connection->getQueryBuilder();
$queryBuilder->select(['affecteduser', 'timestamp'])
->from('activity')
->where($queryBuilder->expr()->eq('activity_id', $queryBuilder->createNamedParameter((int)$since)));
$result = $queryBuilder->execute();
$activity = $result->fetch();
$result->closeCursor();
if ($activity) {
if ($activity['affecteduser'] !== $user) {
throw new \OutOfBoundsException('Invalid since', 2);
}
$timestamp = (int)$activity['timestamp'];
if ($sort === 'DESC') {
$query->andWhere($query->expr()->lte('timestamp', $query->createNamedParameter($timestamp)));
$query->andWhere($query->expr()->lt('activity_id', $query->createNamedParameter($since)));
} else {
$query->andWhere($query->expr()->gte('timestamp', $query->createNamedParameter($timestamp)));
$query->andWhere($query->expr()->gt('activity_id', $query->createNamedParameter($since)));
}
return [];
}
}
/**
* Couldn't find the since, so find the oldest one and set the header
*/
$fetchQuery = $this->connection->getQueryBuilder();
$fetchQuery->select('activity_id')
->from('activity')
->where($fetchQuery->expr()->eq('affecteduser', $fetchQuery->createNamedParameter($user)))
->orderBy('timestamp', $sort)
->setMaxResults(1);
$result = $fetchQuery->execute();
$activity = $result->fetch();
$result->closeCursor();
if ($activity !== false) {
return [
'X-Activity-First-Known' => (int)$activity['activity_id'],
];
}
return [];
}
/**
* Verify that the filter is valid
*
* @param string $filterValue
* @return string
*/
public function validateFilter($filterValue) {
if (!isset($filterValue)) {
return 'all';
}
switch ($filterValue) {
case 'filter':
return $filterValue;
default:
try {
$this->activityManager->getFilterById($filterValue);
return $filterValue;
} catch (\InvalidArgumentException $e) {
return 'all';
}
}
}
/**
* Delete old events
*
* @param int $expireDays Minimum 1 day
*/
public function expire($expireDays = 365) {
$ttl = (60 * 60 * 24 * max(1, $expireDays));
$timelimit = time() - $ttl;
$this->deleteActivities([
'timestamp' => [$timelimit, '<'],
]);
}
/**
* Delete activities that match certain conditions
*
* @param array $conditions Array with conditions that have to be met
* 'field' => 'value' => `field` = 'value'
* 'field' => array('value', 'operator') => `field` operator 'value'
*/
public function deleteActivities($conditions): void {
$platform = $this->connection->getDatabasePlatform();
if($platform instanceof MySQLPlatform) {
$this->logger->debug('Choosing chunked activity delete for MySQL/MariaDB', ['app' => 'activity']);
$this->deleteActivitiesForMySQL($conditions);
return;
}
$this->logger->debug('Choosing regular activity delete', ['app' => 'activity']);
$deleteQuery = $this->connection->getQueryBuilder();
$deleteQuery->delete('activity');
foreach ($conditions as $column => $comparison) {
if (is_array($comparison)) {
$operation = $comparison[1] ?? '=';
$value = $comparison[0];
} else {
$operation = '=';
$value = $comparison;
}
$deleteQuery->andWhere($deleteQuery->expr()->comparison($column, $operation, $deleteQuery->createNamedParameter($value)));
}
// Dont use chunked delete - let the DB handle the large row count natively
$deleteQuery->executeStatement();
}
public function getById(int $activityId): ?IEvent {
$query = $this->connection->getQueryBuilder();
$query->select('*')
->from('activity')
->where($query->expr()->eq('activity_id', $query->createNamedParameter($activityId)));
$result = $query->execute();
if ($row = $result->fetch()) {
$event = $this->activityManager->generateEvent();
$event->setApp((string)$row['app'])
->setType((string)$row['type'])
->setAffectedUser((string)$row['affecteduser'])
->setAuthor((string)$row['user'])
->setTimestamp((int)$row['timestamp'])
->setSubject((string)$row['subject'], (array)json_decode($row['subjectparams'], true))
->setMessage((string)$row['message'], (array)json_decode($row['messageparams'], true))
->setObject((string)$row['object_type'], (int)$row['object_id'], (string)$row['file'])
->setLink((string)$row['link']);
return $event;
}
return null;
}
/**
* Get the id of the first activity in the stream since a specified time
*
* @param string $user
* @param int $timestamp
* @return int
*/
public function getFirstActivitySince(string $user, int $timestamp): int {
$query = $this->connection->getQueryBuilder();
$query->select('activity_id')
->from('activity')
->where($query->expr()->eq('affecteduser', $query->createNamedParameter($user)))
->andWhere($query->expr()->gt('timestamp', $query->createNamedParameter($timestamp, IQueryBuilder::PARAM_INT)))
->orderBy('timestamp', 'ASC')
->setMaxResults(1);
$res = $query->execute()->fetch(\PDO::FETCH_COLUMN);
return (int)$res;
}
/**
* Get the number of activity items and the latest activity id since the specified activity
*
* @param string $user
* @param int $since
* @param bool $byOthers
* @return array
*/
public function getActivitySince(string $user, int $since, bool $byOthers) {
$query = $this->connection->getQueryBuilder();
$nameParam = $query->createNamedParameter($user);
$query->select($query->func()->count('activity_id', 'count'))
->selectAlias($query->func()->max('activity_id'), 'max')
->from('activity')
->where($query->expr()->eq('affecteduser', $nameParam))
->andWhere($query->expr()->gt('activity_id', $query->createNamedParameter($since, IQueryBuilder::PARAM_INT)));
if ($byOthers) {
$query->andWhere($query->expr()->neq('user', $nameParam));
}
return $query->execute()->fetch();
}
/**
* Add galera safe delete chunking if using mysql
* Stops us hitting wsrep_max_ws_rows when large row counts are deleted
*
* @param array $conditions
* @return void
*/
private function deleteActivitiesForMySQL(array $conditions): void {
$query = $this->connection->getQueryBuilder();
$query->select('activity_id')
->from('activity');
foreach ($conditions as $column => $comparison) {
if (is_array($comparison)) {
$operation = $comparison[1] ?? '=';
$value = $comparison[0];
} else {
$operation = '=';
$value = $comparison;
}
$query->where($query->expr()->comparison($column, $operation, $query->createNamedParameter($value)));
}
$query->setMaxResults(50000);
$result = $query->executeQuery();
$count = $result->rowCount();
if($count === 0) {
return;
}
$ids = array_map(static function (array $id) {
return (int)$id[0];
}, $result->fetchAll(\PDO::FETCH_NUM));
$result->closeCursor();
$queryResult = 0;
$deleteQuery = $this->connection->getQueryBuilder();
$deleteQuery->delete('activity');
$deleteQuery->where($deleteQuery->expr()->in('activity_id', $deleteQuery->createParameter('ids'), IQueryBuilder::PARAM_INT_ARRAY));
foreach(array_chunk($ids, 1000) as $chunk) {
$deleteQuery->setParameter('ids', $chunk, IQueryBuilder::PARAM_INT_ARRAY);
$queryResult += $deleteQuery->executeStatement();
}
if($queryResult === 50000) {
$this->deleteActivitiesForMySQL($conditions);
}
}
}
@@ -0,0 +1,223 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2020 Robin Appelman <robin@icewind.nl>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\Activity;
use OCP\Activity\IEvent;
use OCP\Activity\IManager;
use OCP\Defaults;
use OCP\IConfig;
use OCP\IDateTimeFormatter;
use OCP\IURLGenerator;
use OCP\IUserManager;
use OCP\L10N\IFactory;
use OCP\Mail\IMailer;
use OCP\Util;
use Psr\Log\LoggerInterface;
class DigestSender {
public const ACTIVITY_LIMIT = 20;
public function __construct(
private IConfig $config,
private Data $data,
private UserSettings $userSettings,
private GroupHelper $groupHelper,
private IMailer $mailer,
private IManager $activityManager,
private IUserManager $userManager,
private IURLGenerator $urlGenerator,
private Defaults $defaults,
private IFactory $l10nFactory,
private IDateTimeFormatter $dateTimeFormatter,
private LoggerInterface $logger
) {
}
public function sendDigests(int $now): void {
$users = $this->getDigestUsers();
$userLanguages = $this->config->getUserValueForUsers('core', 'lang', $users);
$userTimezones = $this->config->getUserValueForUsers('core', 'timezone', $users);
$digestDate = $this->config->getUserValueForUsers('activity', 'digest', $users);
$defaultLanguage = $this->config->getSystemValue('default_language', 'en');
$defaultTimeZone = date_default_timezone_get();
$timezoneDigestDay = [];
$this->activityManager->setRequirePNG(true);
foreach ($users as $user) {
$language = (!empty($userLanguages[$user])) ? $userLanguages[$user] : $defaultLanguage;
$timezone = (!empty($userTimezones[$user])) ? $userTimezones[$user] : $defaultTimeZone;
// Check if the user's timezone is after 6am already
if (!isset($timezoneDigestDay[$timezone])) {
$timezoneDate = new \DateTime('now', new \DateTimeZone($timezone));
if ($timezoneDate->format('H') < 6) {
// Still before 6am, so dont send yet.
$timezoneDate->sub(new \DateInterval('P1D'));
}
$timezoneDigestDay[$timezone] = $timezoneDate->format('Y.m.d');
}
$userDigestDate = $digestDate[$user] ?? '';
if ($userDigestDate === $timezoneDigestDay[$timezone]) {
// User got todays digest already
continue;
}
try {
$this->sendDigestForUser($user, $now, $timezone, $language);
} catch (\Throwable $e) {
$this->logger->error('Exception occurred while sending user digest email', [
'exception' => $e,
]);
}
// We still update the digest time after an failed email,
// so it hopefully works tomorrow
$this->config->setUserValue($user, 'activity', 'digest', $timezoneDigestDay[$timezone]);
}
$this->activityManager->setRequirePNG(false);
}
/**
* get all users who have activity digest enabled
*
* @return string[]
*/
private function getDigestUsers(): array {
return $this->config->getUsersForUserValue('activity', 'notify_setting_activity_digest', '1');
}
private function getLastSendActivity(string $user, int $now): int {
$lastSend = (int)$this->config->getUserValue($user, 'activity', 'activity_digest_last_send', 0);
if ($lastSend > 0) {
return $lastSend;
}
// Don't flood on first email with old news, just consider the last 24h
return $this->data->getFirstActivitySince($user, $now - (24 * 60 * 60));
}
public function sendDigestForUser(string $uid, int $now, string $timezone, string $language) {
$l10n = $this->l10nFactory->get('activity', $language);
$this->groupHelper->setL10n($l10n);
$lastSend = $this->getLastSendActivity($uid, $now);
$user = $this->userManager->get($uid);
if ($lastSend === 0) {
return;
}
$this->activityManager->setCurrentUserId($uid);
['count' => $count, 'max' => $lastActivityId] = $this->data->getActivitySince($uid, $lastSend, true);
$count = (int) $count;
$lastActivityId = (int) $lastActivityId;
if ($count === 0) {
return;
}
/** @var IEvent[] $activities */
$activities = $this->data->get(
$this->groupHelper,
$this->userSettings,
$uid,
$lastSend,
self::ACTIVITY_LIMIT,
'asc',
'by',
'',
0,
true
);
$skippedCount = max(0, $count - self::ACTIVITY_LIMIT);
$template = $this->mailer->createEMailTemplate('activity.Notification', [
'displayname' => $user->getDisplayName(),
'url' => $this->urlGenerator->getAbsoluteURL('/'),
'activityEvents' => $activities,
'skippedCount' => $skippedCount,
]);
$template->setSubject($l10n->t('Daily activity summary for %s', $this->defaults->getName()));
$template->addHeader();
foreach ($activities as $event) {
$relativeDateTime = $this->dateTimeFormatter->formatDateTimeRelativeDay(
$event->getTimestamp(),
'long',
'short',
new \DateTimeZone($timezone),
$l10n
);
$template->addBodyListItem($this->getHTMLSubject($event), $relativeDateTime, $event->getIcon(), $event->getParsedSubject());
}
if ($skippedCount) {
$template->addBodyListItem($l10n->n('and %n more ', 'and %n more ', $skippedCount));
}
$template->addFooter('', $language);
$message = $this->mailer->createMessage();
$message->setTo([$user->getEMailAddress() => $user->getDisplayName()]);
$message->useTemplate($template);
$message->setFrom([Util::getDefaultEmailAddress('no-reply') => $this->defaults->getName()]);
$this->activityManager->setCurrentUserId(null);
try {
$this->mailer->send($message);
$this->config->setUserValue($user->getUID(), 'activity', 'activity_digest_last_send', (string) $lastActivityId);
} catch (\Exception $e) {
$this->logger->error($e->getMessage());
return;
}
}
/**
* @param IEvent $event
* @return string
*/
protected function getHTMLSubject(IEvent $event): string {
if ($event->getRichSubject() === '') {
return htmlspecialchars($event->getParsedSubject());
}
$placeholders = $replacements = [];
foreach ($event->getRichSubjectParameters() as $placeholder => $parameter) {
$placeholders[] = '{' . $placeholder . '}';
if ($parameter['type'] === 'file') {
$replacement = (string) $parameter['path'];
} else {
$replacement = (string) $parameter['name'];
}
if (isset($parameter['link'])) {
$replacements[] = '<a href="' . $parameter['link'] . '">' . htmlspecialchars($replacement) . '</a>';
} else {
$replacements[] = '<strong>' . htmlspecialchars($replacement) . '</strong>';
}
}
return str_replace($placeholders, $replacements, $event->getRichSubject());
}
}
@@ -0,0 +1,38 @@
<?php
/**
* @copyright Copyright (c) 2023, Louis Chmn <louis@chmn.me>
*
* @author Louis Chmn <louis@chmn.me>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
namespace OCA\Activity\Event;
use OCP\EventDispatcher\Event;
/**
* @since 28.0.0 Dispatched as a typed event
*/
class LoadAdditionalScriptsEvent extends Event {
/**
* @deprecated 28.0.0 - Listen to the typed event instead.
*/
public const EVENT_ENTITY = 'OCA\Activity::loadAdditionalScripts';
public function __construct(public string $filter) {
parent::__construct();
}
}
@@ -0,0 +1,26 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Joas Schilling <coding@schilljs.com>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
namespace OCA\Activity\Exception;
class InvalidFilterException extends \InvalidArgumentException {
}
@@ -0,0 +1,31 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Joas Schilling <coding@schilljs.com>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
namespace OCA\Activity\Extension;
class Files {
public const TYPE_SHARE_CREATED = 'file_created';
public const TYPE_FILE_CHANGED = 'file_changed';
public const TYPE_FAVORITE_CHANGED = 'file_favorite_changed';
public const TYPE_SHARE_DELETED = 'file_deleted';
public const TYPE_SHARE_RESTORED = 'file_restored';
}
@@ -0,0 +1,27 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Joas Schilling <coding@schilljs.com>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
namespace OCA\Activity\Extension;
class Files_Sharing {
public const TYPE_SHARED = 'shared';
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,93 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
* @copyright Copyright (c) 2017, Joas Schilling <coding@schilljs.com>
*
* @author Joas Schilling <coding@schilljs.com>
* @author Vincent Petry <pvince81@owncloud.com>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
namespace OCA\Activity;
/**
* The class to handle the filesystem hooks
*/
class FilesHooksStatic {
/**
* @return FilesHooks
*/
protected static function getHooks() {
return \OC::$server->query(FilesHooks::class);
}
/**
* Store the create hook events
* @param array $params The hook params
*/
public static function fileCreate($params) {
self::getHooks()->fileCreate($params['path']);
}
/**
* Store the update hook events
* @param array $params The hook params
*/
public static function fileUpdate($params) {
self::getHooks()->fileUpdate($params['path']);
}
/**
* Store the delete hook events
* @param array $params The hook params
*/
public static function fileDelete($params) {
self::getHooks()->fileDelete($params['path']);
}
/**
* Store the rename hook events
* @param array $params The hook params
*/
public static function fileMove($params) {
self::getHooks()->fileMove($params['oldpath'], $params['newpath']);
}
/**
* Store the rename hook events
* @param array $params The hook params
*/
public static function fileMovePost($params) {
self::getHooks()->fileMovePost($params['oldpath'], $params['newpath']);
}
/**
* Store the restore hook events
* @param array $params The hook params
*/
public static function fileRestore($params) {
self::getHooks()->fileRestore($params['filePath']);
}
/**
* Manage sharing events
* @param array $params The hook params
*/
public static function share($params) {
self::getHooks()->share($params);
}
}
@@ -0,0 +1,92 @@
<?php
/**
* @copyright Copyright (c) 2016 Joas Schilling <coding@schilljs.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\Activity\Filter;
use OCP\Activity\IFilter;
use OCP\IL10N;
use OCP\IURLGenerator;
class AllFilter implements IFilter {
/** @var IL10N */
protected $l;
/** @var IURLGenerator */
protected $url;
/**
* @param IL10N $l
* @param IURLGenerator $url
*/
public function __construct(IL10N $l, IURLGenerator $url) {
$this->l = $l;
$this->url = $url;
}
/**
* @return string Lowercase a-z only identifier
* @since 9.2.0
*/
public function getIdentifier() {
return 'all';
}
/**
* @return string A translated string
* @since 9.2.0
*/
public function getName() {
return $this->l->t('All activities');
}
/**
* @return int
* @since 9.2.0
*/
public function getPriority() {
return 0;
}
/**
* @return string Full URL to an icon, empty string when none is given
* @since 9.2.0
*/
public function getIcon() {
return $this->url->getAbsoluteURL($this->url->imagePath('activity', 'activity-dark.svg'));
}
/**
* @param string[] $types
* @return string[] An array of allowed apps from which activities should be displayed
* @since 9.2.0
*/
public function filterTypes(array $types) {
return $types;
}
/**
* @return string[] An array of allowed apps from which activities should be displayed
* @since 9.2.0
*/
public function allowedApps() {
return [];
}
}
@@ -0,0 +1,92 @@
<?php
/**
* @copyright Copyright (c) 2016 Joas Schilling <coding@schilljs.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\Activity\Filter;
use OCP\Activity\IFilter;
use OCP\IL10N;
use OCP\IURLGenerator;
class ByFilter implements IFilter {
/** @var IL10N */
protected $l;
/** @var IURLGenerator */
protected $url;
/**
* @param IL10N $l
* @param IURLGenerator $url
*/
public function __construct(IL10N $l, IURLGenerator $url) {
$this->l = $l;
$this->url = $url;
}
/**
* @return string Lowercase a-z only identifier
* @since 9.2.0
*/
public function getIdentifier() {
return 'by';
}
/**
* @return string A translated string
* @since 9.2.0
*/
public function getName() {
return $this->l->t('By others');
}
/**
* @return int
* @since 9.2.0
*/
public function getPriority() {
return 2;
}
/**
* @return string Full URL to an icon, empty string when none is given
* @since 9.2.0
*/
public function getIcon() {
return $this->url->getAbsoluteURL($this->url->imagePath('core', 'places/contacts.svg'));
}
/**
* @param string[] $types
* @return string[] An array of allowed apps from which activities should be displayed
* @since 9.2.0
*/
public function filterTypes(array $types) {
return $types;
}
/**
* @return string[] An array of allowed apps from which activities should be displayed
* @since 9.2.0
*/
public function allowedApps() {
return [];
}
}
@@ -0,0 +1,92 @@
<?php
/**
* @copyright Copyright (c) 2016 Joas Schilling <coding@schilljs.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\Activity\Filter;
use OCP\Activity\IFilter;
use OCP\IL10N;
use OCP\IURLGenerator;
class SelfFilter implements IFilter {
/** @var IL10N */
protected $l;
/** @var IURLGenerator */
protected $url;
/**
* @param IL10N $l
* @param IURLGenerator $url
*/
public function __construct(IL10N $l, IURLGenerator $url) {
$this->l = $l;
$this->url = $url;
}
/**
* @return string Lowercase a-z only identifier
* @since 9.2.0
*/
public function getIdentifier() {
return 'self';
}
/**
* @return string A translated string
* @since 9.2.0
*/
public function getName() {
return $this->l->t('By you');
}
/**
* @return int
* @since 9.2.0
*/
public function getPriority() {
return 1;
}
/**
* @return string Full URL to an icon, empty string when none is given
* @since 9.2.0
*/
public function getIcon() {
return $this->url->getAbsoluteURL($this->url->imagePath('core', 'actions/user.svg'));
}
/**
* @param string[] $types
* @return string[] An array of allowed apps from which activities should be displayed
* @since 9.2.0
*/
public function filterTypes(array $types) {
return $types;
}
/**
* @return string[] An array of allowed apps from which activities should be displayed
* @since 9.2.0
*/
public function allowedApps() {
return [];
}
}
@@ -0,0 +1,216 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Joas Schilling <coding@schilljs.com>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
namespace OCA\Activity;
use OCP\Activity\IEvent;
use OCP\Activity\IManager;
use OCP\IL10N;
use OCP\RichObjectStrings\InvalidObjectExeption;
use OCP\RichObjectStrings\IValidator;
use Psr\Log\LoggerInterface;
class GroupHelper {
/** @var IEvent[] */
protected $event = [];
/** @var int */
protected $lastEvent = 0;
/** @var bool */
protected $allowGrouping;
public function __construct(
protected IL10N $l,
protected IManager $activityManager,
protected IValidator $richObjectValidator,
protected LoggerInterface $logger) {
$this->allowGrouping = true;
}
/**
* @param IL10N $l
*/
public function setL10n(IL10N $l) {
$this->l = $l;
}
/**
* Add an activity to the internal array
*
* @param array $activity
*/
public function addActivity($activity) {
$id = (int) $activity['activity_id'];
$event = $this->arrayToEvent($activity);
$language = $this->l->getLanguageCode();
foreach ($this->activityManager->getProviders() as $provider) {
try {
$this->activityManager->setFormattingObject($event->getObjectType(), $event->getObjectId());
if ($this->allowGrouping && $this->lastEvent !== 0 && isset($this->event[$this->lastEvent])) {
$event = $provider->parse($language, $event, $this->event[$this->lastEvent]);
} else {
$event = $provider->parse($language, $event);
}
try {
$this->richObjectValidator->validate($event->getRichSubject(), $event->getRichSubjectParameters());
} catch (InvalidObjectExeption $e) {
$this->logger->error(
$e->getMessage(),
[
'app' => 'activity',
'exception' => $e
],
);
$event->setRichSubject('Rich subject or a parameter for "' . $event->getRichSubject() . '" is malformed', []);
$event->setParsedSubject('Rich subject or a parameter for "' . $event->getRichSubject() . '" is malformed');
}
if ($event->getRichMessage()) {
try {
$this->richObjectValidator->validate($event->getRichMessage(), $event->getRichMessageParameters());
} catch (InvalidObjectExeption $e) {
$this->logger->error(
$e->getMessage(),
[
'app' => 'activity',
'exception' => $e
],
);
$event->setRichMessage('Rich message or a parameter is malformed', []);
$event->setParsedMessage('Rich message or a parameter is malformed');
}
}
$this->activityManager->setFormattingObject('', 0);
$child = $event->getChildEvent();
if ($child instanceof IEvent) {
unset($this->event[$this->lastEvent]);
}
} catch (\InvalidArgumentException $e) {
}
}
if (!$event->getParsedSubject()) {
$this->logger->debug('Activity "' . $event->getRichSubject() . '" was not parsed by any provider');
return;
}
$this->event[$id] = $event;
$this->lastEvent = $id;
}
/**
* Get the prepared activities
*
* @return array translated activities ready for use
*/
public function getActivities() {
$return = [];
foreach ($this->event as $id => $event) {
$return[] = $this->eventToArray($event, $id);
}
$this->event = [];
return $return;
}
/**
* @return IEvent[]
*/
public function getEvents(): array {
$return = $this->event;
$this->event = [];
return $return;
}
/**
* @param array $row
* @return IEvent
*/
protected function arrayToEvent(array $row) {
$event = $this->activityManager->generateEvent();
$event->setApp((string) $row['app'])
->setType((string) $row['type'])
->setAffectedUser((string) $row['affecteduser'])
->setAuthor((string) $row['user'])
->setTimestamp((int) $row['timestamp'])
->setSubject((string) $row['subject'], (array) json_decode($row['subjectparams'], true))
->setMessage((string) $row['message'], (array) json_decode($row['messageparams'], true))
->setObject((string) $row['object_type'], (int) $row['object_id'], (string) $row['file'])
->setLink((string) $row['link']);
return $event;
}
/**
* @param IEvent $event
* @param (int|string) $id
*
* @return array
*
* @psalm-param array-key $id
*/
protected function eventToArray(IEvent $event, $id) {
return [
'activity_id' => $id,
'app' => $event->getApp(),
'type' => $event->getType(),
'affecteduser' => $event->getAffectedUser(),
'user' => $event->getAuthor(),
'timestamp' => $event->getTimestamp(),
'subject' => $event->getParsedSubject(),
'subject_rich' => [
$event->getRichSubject(),
$event->getRichSubjectParameters(),
],
'message' => $event->getParsedMessage(),
'message_rich' => [
$event->getRichMessage(),
$event->getRichMessageParameters(),
],
'object_type' => $event->getObjectType(),
'object_id' => $event->getObjectId(),
'object_name' => $event->getObjectName(),
'objects' => $this->getObjectsFromChildren($event),
'link' => $event->getLink(),
'icon' => $event->getIcon(),
];
}
/**
* @param IEvent $event
* @return array
*/
protected function getObjectsFromChildren(IEvent $event): array {
$child = $event->getChildEvent();
$objects = [];
if ($child instanceof IEvent) {
$objects = $this->getObjectsFromChildren($child);
}
if ($event->getObjectId() !== 0 || $event->getObjectName() !== '') {
$objects[$event->getObjectId()] = $event->getObjectName();
}
return $objects;
}
}
@@ -0,0 +1,40 @@
<?php
/**
* @copyright Copyright (c) 2016 Joas Schilling <coding@schilljs.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\Activity;
use OCP\Activity\IManager;
use OCP\IL10N;
use OCP\RichObjectStrings\IValidator;
use Psr\Log\LoggerInterface;
class GroupHelperDisabled extends GroupHelper {
public function __construct(IL10N $l,
IManager $activityManager,
IValidator $richObjectValidator,
LoggerInterface $logger) {
parent::__construct($l,
$activityManager,
$richObjectValidator,
$logger);
$this->allowGrouping = false;
}
}
@@ -0,0 +1,50 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2019, Roeland Jago Douma <roeland@famdouma.nl>
*
* @author Roeland Jago Douma <roeland@famdouma.nl>
* @author John Molakvoæ <skjnldsv@protonmail.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\Activity\Listener;
use OCA\Activity\AppInfo\Application;
use OCA\Files\Event\LoadSidebar;
use OCP\EventDispatcher\Event;
use OCP\EventDispatcher\IEventListener;
use OCP\Util;
/**
* @template-implements IEventListener<Event>
*/
class LoadSidebarScripts implements IEventListener {
public function handle(Event $event): void {
if (!($event instanceof LoadSidebar)) {
return;
}
// TODO: make sure to only include the sidebar script when
// we properly split it between files list and sidebar
Util::addStyle(Application::APP_ID, 'style');
Util::addScript(Application::APP_ID, 'activity-sidebar');
Util::addInitScript(Application::APP_ID, 'activity-api');
}
}
@@ -0,0 +1,81 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2021, Thomas Citharel <nextcloud@tcit.fr>
*
* @author Thomas Citharel <nextcloud@tcit.fr>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\Activity\Listener;
use OCP\EventDispatcher\Event;
use OCP\EventDispatcher\IEventListener;
use OCP\IConfig;
use OCP\IUser;
use OCP\User\Events\PostLoginEvent;
/**
* @template-implements IEventListener<Event>
*/
class SetUserDefaults implements IEventListener {
/** @var IConfig */
private $config;
public function __construct(IConfig $config) {
$this->config = $config;
}
public function handle(Event $event): void {
if (!($event instanceof PostLoginEvent)) {
return;
}
$user = $event->getUser();
$this->setDefaultsForUser($user);
}
private function setDefaultsForUser(IUser $user): void {
if ($this->config->getUserValue($user->getUID(), 'activity', 'configured', 'no') === 'yes') {
// Already has settings
return;
}
foreach ($this->config->getAppKeys('activity') as $key) {
if (strpos($key, 'notify_') !== 0) {
continue;
}
if ($this->config->getUserValue($user->getUID(), 'activity', $key, null) !== null) {
// Already has this setting
continue;
}
$this->config->setUserValue(
$user->getUID(),
'activity',
$key,
$this->config->getAppValue('activity', $key)
);
}
// Mark settings as configured
$this->config->setUserValue($user->getUID(), 'activity', 'configured', 'yes');
}
}
@@ -0,0 +1,67 @@
<?php
/**
* @copyright Copyright (c) 2023, Louis Chmn <louis@chmn.me>
*
* @author Louis Chmn <louis@chmn.me>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
namespace OCA\Activity\Listener;
use OCA\Activity\FilesHooks;
use OCP\EventDispatcher\Event;
use OCP\EventDispatcher\IEventListener;
use OCP\Share\Events\BeforeShareDeletedEvent;
use OCP\Share\Events\ShareDeletedFromSelfEvent;
/**
* The class to handle the share events
* @template-implements IEventListener<Event>
*/
class ShareEventListener implements IEventListener {
public function __construct(
private FilesHooks $fileHooks,
) {
}
public function handle(Event $event): void {
if ($event instanceof BeforeShareDeletedEvent) {
$this->unShare($event);
}
if ($event instanceof ShareDeletedFromSelfEvent) {
$this->unShareSelf($event);
}
}
/**
* Unsharing event
*/
public function unShare(BeforeShareDeletedEvent $event): void {
$share = $event->getShare();
$this->fileHooks->unShare($share);
}
/**
* "Unsharing a share from self only" event
*/
public function unShareSelf(ShareDeletedFromSelfEvent $event): void {
$share = $event->getShare();
$this->fileHooks->unShareSelf($share);
}
}
@@ -0,0 +1,68 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2021, Thomas Citharel <nextcloud@tcit.fr>
*
* @author Thomas Citharel <nextcloud@tcit.fr>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\Activity\Listener;
use OCA\Activity\Data;
use OCA\Activity\MailQueueHandler;
use OCP\EventDispatcher\Event;
use OCP\EventDispatcher\IEventListener;
use OCP\IUser;
use OCP\User\Events\UserDeletedEvent;
/**
* @template-implements IEventListener<Event>
*/
class UserDeleted implements IEventListener {
/** @var Data */
private $data;
/**
* @var MailQueueHandler
*/
private $mailQueueHandler;
public function __construct(Data $data, MailQueueHandler $mailQueueHandler) {
$this->data = $data;
$this->mailQueueHandler = $mailQueueHandler;
}
public function handle(Event $event): void {
if (!($event instanceof UserDeletedEvent)) {
return;
}
$user = $event->getUser();
$this->deleteUserStream($user);
$this->deleteUserMailQueue($user);
}
private function deleteUserStream(IUser $user): void {
$this->data->deleteActivities(['affecteduser' => $user->getUID()]);
}
private function deleteUserMailQueue(IUser $user): void {
$this->mailQueueHandler->purgeItemsForUser($user->getUID());
}
}
@@ -0,0 +1,497 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Joas Schilling <coding@schilljs.com>
* @author Lukas Reschke <lukas@statuscode.ch>
* @author Thomas Citharel <nextcloud@tcit.fr>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
namespace OCA\Activity;
use OCP\Activity\IEvent;
use OCP\Activity\IManager;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\Defaults;
use OCP\IConfig;
use OCP\IDateTimeFormatter;
use OCP\IDBConnection;
use OCP\IURLGenerator;
use OCP\IUser;
use OCP\IUserManager;
use OCP\L10N\IFactory;
use OCP\Mail\Headers\AutoSubmitted;
use OCP\Mail\IMailer;
use OCP\RichObjectStrings\InvalidObjectExeption;
use OCP\RichObjectStrings\IValidator;
use OCP\Util;
use Psr\Log\LoggerInterface;
/**
* Class MailQueueHandler
* Gets the users from the database and
*
* @package OCA\Activity
*/
class MailQueueHandler {
public const CLI_EMAIL_BATCH_SIZE = 500;
public const WEB_EMAIL_BATCH_SIZE = 25;
/** Number of entries we want to list in the email */
public const ENTRY_LIMIT = 200;
/** @var array */
protected $languages;
/** @var string */
protected $senderAddress;
/** @var string */
protected $senderName;
/** @var IDateTimeFormatter */
protected $dateFormatter;
public function __construct(IDateTimeFormatter $dateFormatter,
protected IDBConnection $connection,
protected IMailer $mailer,
protected IURLGenerator $urlGenerator,
protected IUserManager $userManager,
protected IFactory $lFactory,
protected IManager $activityManager,
protected IValidator $richObjectValidator,
protected IConfig $config,
protected LoggerInterface $logger) {
$this->dateFormatter = $dateFormatter;
}
/**
* Send an email to {$limit} users
*
* @param int $limit Number of users we want to send an email to
* @param int $sendTime The latest send time
* @param bool $forceSending Ignores latest send and just sends all emails
* @param null|int $restrictEmails null or one of UserSettings::EMAIL_SEND_*
* @return int Number of users we sent an email to
*/
public function sendEmails($limit, $sendTime, $forceSending = false, $restrictEmails = null) {
// Get all users which should receive an email
$affectedUsers = $this->getAffectedUsers($limit, $sendTime, $forceSending, $restrictEmails);
if (empty($affectedUsers)) {
// No users found to notify, mission abort
return 0;
}
$userLanguages = $this->config->getUserValueForUsers('core', 'lang', $affectedUsers);
$userTimezones = $this->config->getUserValueForUsers('core', 'timezone', $affectedUsers);
$userEnabled = $this->config->getUserValueForUsers('core', 'enabled', $affectedUsers);
// Send Email
$default_lang = $this->config->getSystemValue('default_language', 'en');
$defaultTimeZone = date_default_timezone_get();
$deleteItemsForUsers = [];
$this->activityManager->setRequirePNG(true);
foreach ($affectedUsers as $user) {
if (isset($userEnabled[$user]) && $userEnabled[$user] === 'false') {
$deleteItemsForUsers[] = $user;
continue;
}
$userObject = $this->userManager->get($user);
$email = $userObject ? $userObject->getEMailAddress() : '';
if (empty($email)) {
// The user did not setup an email address
// So we will not send an email :(
$this->logger->debug("Couldn't send notification email to user '{user}' (email address isn't set for that user)", ['user' => $user, 'app' => 'activity']);
$deleteItemsForUsers[] = $user;
continue;
}
$language = (!empty($userLanguages[$user])) ? $userLanguages[$user] : $default_lang;
$timezone = (!empty($userTimezones[$user])) ? $userTimezones[$user] : $defaultTimeZone;
try {
if ($this->sendEmailToUser($user, $email, $language, $timezone, $sendTime)) {
$deleteItemsForUsers[] = $user;
} else {
$this->logger->warning("Failed sending activity email to user '{user}'.", ['user' => $user, 'app' => 'activity']);
}
} catch (\Exception $e) {
$this->logger->error('Failed creating activity email for user "{user}"', [
'exception' => $e,
'user' => $user,
'app' => 'activity',
]);
// continue;
}
}
$this->activityManager->setRequirePNG(false);
// Delete all entries we dealt with
$this->deleteSentItems($deleteItemsForUsers, $sendTime);
return count($affectedUsers);
}
/**
* Get the users we want to send an email to
*
* @param int|null $limit
* @param int $latestSend
* @param bool $forceSending
* @param int|null $restrictEmails
* @return array
*/
protected function getAffectedUsers($limit, $latestSend, $forceSending, $restrictEmails) {
$query = $this->connection->getQueryBuilder();
$query->select('amq_affecteduser')
->selectAlias($query->createFunction('MIN(' . $query->getColumnName('amq_latest_send') . ')'), 'amq_trigger_time')
->from('activity_mq')
->groupBy('amq_affecteduser')
->orderBy('amq_trigger_time', 'ASC');
if ($limit > 0) {
$query->setMaxResults($limit);
}
if ($forceSending) {
$query->where($query->expr()->lt('amq_timestamp', $query->createNamedParameter($latestSend)));
} else {
$query->where($query->expr()->lt('amq_latest_send', $query->createNamedParameter($latestSend)));
}
if ($restrictEmails !== null) {
if ($restrictEmails === UserSettings::EMAIL_SEND_HOURLY) {
$query->where($query->expr()->eq('amq_timestamp', $query->func()->subtract('amq_latest_send', $query->expr()->literal(3600))));
} elseif ($restrictEmails === UserSettings::EMAIL_SEND_DAILY) {
$query->where($query->expr()->eq('amq_timestamp', $query->func()->subtract('amq_latest_send', $query->expr()->literal(3600 * 24))));
} elseif ($restrictEmails === UserSettings::EMAIL_SEND_WEEKLY) {
$query->where($query->expr()->eq('amq_timestamp', $query->func()->subtract('amq_latest_send', $query->expr()->literal(3600 * 24 * 7))));
} elseif ($restrictEmails === UserSettings::EMAIL_SEND_ASAP) {
$query->where($query->expr()->eq('amq_timestamp', 'amq_latest_send'));
}
}
$result = $query->execute();
$affectedUsers = [];
while ($row = $result->fetch()) {
$affectedUsers[] = $row['amq_affecteduser'];
}
$result->closeCursor();
return $affectedUsers;
}
/**
* Get all items for the user we want to send an email to
*
* @param string $affectedUser
* @param int $maxTime
* @param int $maxNumItems
* @return array [data of the first max. 200 entries, total number of entries]
*/
protected function getItemsForUser($affectedUser, $maxTime, $maxNumItems = self::ENTRY_LIMIT) {
$query = $this->connection->getQueryBuilder();
$query->select('*')
->from('activity_mq')
->where($query->expr()->lte('amq_timestamp', $query->createNamedParameter($maxTime)))
->andWhere($query->expr()->eq('amq_affecteduser', $query->createNamedParameter($affectedUser)))
->orderBy('amq_timestamp', 'ASC')
->setMaxResults($maxNumItems);
$result = $query->execute();
$activities = [];
while ($row = $result->fetch()) {
$activities[] = $row;
}
$result->closeCursor();
if (isset($activities[$maxNumItems - 1])) {
// Reached the limit, run a query to get the actual count.
$query = $this->connection->getQueryBuilder();
$query->selectAlias($query->func()->count('*'), 'actual_count')
->from('activity_mq')
->where($query->expr()->lte('amq_timestamp', $query->createNamedParameter($maxTime)))
->andWhere($query->expr()->eq('amq_affecteduser', $query->createNamedParameter($affectedUser)));
$result = $query->execute();
$row = $result->fetch();
$result->closeCursor();
return [$activities, $row['actual_count'] - $maxNumItems];
}
return [$activities, 0];
}
public function purgeItemsForUser(string $affectedUser): void {
$queryBuilder = $this->connection->getQueryBuilder();
$queryBuilder->delete('activity_mq')
->where($queryBuilder->expr()->eq('amq_affecteduser', $queryBuilder->createNamedParameter($affectedUser)));
$queryBuilder->executeStatement();
}
/**
* Get a language object for a specific language
*
* @param string $lang Language identifier
* @return \OCP\IL10N Language object of $lang
*/
protected function getLanguage($lang) {
if (!isset($this->languages[$lang])) {
$this->languages[$lang] = $this->lFactory->get('activity', $lang);
}
return $this->languages[$lang];
}
/**
* Get the sender data
* @param string $setting Either `email` or `name`
* @return string
*/
protected function getSenderData($setting) {
if (empty($this->senderAddress)) {
$this->senderAddress = Util::getDefaultEmailAddress('no-reply');
}
if (empty($this->senderName)) {
$defaults = new Defaults();
$this->senderName = $defaults->getName();
}
if ($setting === 'email') {
return $this->senderAddress;
}
return $this->senderName;
}
/**
* Send a notification to one user
*
* @param string $userName Username of the recipient
* @param string $email Email address of the recipient
* @param string $lang Selected language of the recipient
* @param string $timezone Selected timezone of the recipient
* @param int $maxTime
* @return bool True if the entries should be removed, false otherwise
* @throws \UnexpectedValueException
*/
protected function sendEmailToUser($userName, $email, $lang, $timezone, $maxTime) {
$user = $this->userManager->get($userName);
if (!$user instanceof IUser) {
return true;
}
if (!$this->mailer->validateMailAddress($email)) {
$this->logger->warning('Notification for user "{user}" not sent because the email address "{email}" is invalid.', ['user' => $userName, 'email' => $email]);
return true;
}
[$mailData, $skippedCount] = $this->getItemsForUser($userName, $maxTime);
$l = $this->getLanguage($lang);
$this->activityManager->setCurrentUserId($userName);
$activityEvents = [];
foreach ($mailData as $activity) {
$event = $this->activityManager->generateEvent();
try {
$event->setApp((string) $activity['amq_appid'])
->setType((string) $activity['amq_type'])
->setAffectedUser((string) $activity['amq_affecteduser'])
->setTimestamp((int) $activity['amq_timestamp'])
->setSubject((string) $activity['amq_subject'], (array) json_decode($activity['amq_subjectparams'], true))
->setObject((string) $activity['object_type'], (int) $activity['object_id']);
} catch (\InvalidArgumentException $e) {
continue;
}
$relativeDateTime = $this->dateFormatter->formatDateTimeRelativeDay(
(int) $activity['amq_timestamp'],
'long', 'short',
new \DateTimeZone($timezone), $l
);
try {
$event = $this->parseEvent($lang, $event);
} catch (\InvalidArgumentException $e) {
continue;
}
$activityEvents[] = [
'event' => $event,
'relativeDateTime' => $relativeDateTime
];
}
$template = $this->mailer->createEMailTemplate('activity.Notification', [
'displayname' => $user->getDisplayName(),
'url' => $this->urlGenerator->getAbsoluteURL('/'),
'activityEvents' => $activityEvents,
'skippedCount' => $skippedCount,
]);
$template->setSubject($l->t('Activity at %s', $this->getSenderData('name')));
$template->addHeader();
$template->addHeading($l->t('Hello %s', [$user->getDisplayName()]), $l->t('Hello %s,', [$user->getDisplayName()]));
$homeLink = '<a href="' . $this->urlGenerator->getAbsoluteURL('/') . '">' . htmlspecialchars($this->getSenderData('name')) . '</a>';
$template->addBodyText(
$l->t('There was some activity at %s', [$homeLink]),
$l->t('There was some activity at %s', [$this->urlGenerator->getAbsoluteURL('/')])
);
foreach ($activityEvents as $activity) {
/** @var IEvent $event */
$event = $activity['event'];
$relativeDateTime = $activity['relativeDateTime'];
$template->addBodyListItem($this->getHTMLSubject($event), $relativeDateTime, $event->getIcon(), $event->getParsedSubject());
}
if ($skippedCount) {
$template->addBodyListItem($l->n('and %n more ', 'and %n more ', $skippedCount));
}
$template->addFooter('', $lang);
$message = $this->mailer->createMessage();
$message->setTo([$email => $user->getDisplayName()]);
$message->useTemplate($template);
$message->setFrom([$this->getSenderData('email') => $this->getSenderData('name')]);
// We don't want auto generated responses to autogenerated activity notifications
$message->setAutoSubmitted(AutoSubmitted::VALUE_AUTO_GENERATED);
try {
$this->mailer->send($message);
} catch (\Exception $e) {
$this->logger->error('Failed sending activity email to user "{user}"', [
'exception' => $e,
'user' => $userName,
'app' => 'activity',
]);
return false;
}
$this->activityManager->setCurrentUserId(null);
return true;
}
/**
* @param IEvent $event
* @return string
*/
protected function getHTMLSubject(IEvent $event): string {
if ($event->getRichSubject() === '') {
return htmlspecialchars($event->getParsedSubject());
}
$placeholders = $replacements = [];
foreach ($event->getRichSubjectParameters() as $placeholder => $parameter) {
$placeholders[] = '{' . $placeholder . '}';
if ($parameter['type'] === 'file') {
$replacement = (string) $parameter['path'];
} else {
$replacement = (string) $parameter['name'];
}
if (isset($parameter['link'])) {
$replacements[] = '<a href="' . $parameter['link'] . '">' . htmlspecialchars($replacement) . '</a>';
} else {
$replacements[] = '<strong>' . htmlspecialchars($replacement) . '</strong>';
}
}
return str_replace($placeholders, $replacements, $event->getRichSubject());
}
/**
* @param string $lang
* @param IEvent $event
* @return IEvent
* @throws \InvalidArgumentException when the event could not be parsed
*/
protected function parseEvent($lang, IEvent $event) {
$this->activityManager->setFormattingObject($event->getObjectType(), $event->getObjectId());
foreach ($this->activityManager->getProviders() as $provider) {
try {
$event = $provider->parse($lang, $event);
} catch (\InvalidArgumentException $e) {
}
}
$this->activityManager->setFormattingObject('', 0);
try {
$this->richObjectValidator->validate($event->getRichSubject(), $event->getRichSubjectParameters());
} catch (InvalidObjectExeption $e) {
$this->logger->error(
$e->getMessage(),
[
'app' => 'activity',
'exception' => $e
],
);
$event->setRichSubject('Rich subject or a parameter for "' . $event->getRichSubject() . '" is malformed', []);
$event->setParsedSubject('Rich subject or a parameter for "' . $event->getRichSubject() . '" is malformed');
}
if ($event->getRichMessage()) {
try {
$this->richObjectValidator->validate($event->getRichMessage(), $event->getRichMessageParameters());
} catch (InvalidObjectExeption $e) {
$this->logger->error(
$e->getMessage(),
[
'app' => 'activity',
'exception' => $e
],
);
$event->setRichMessage('Rich message or a parameter is malformed', []);
$event->setParsedMessage('Rich message or a parameter is malformed');
}
}
if (!$event->getParsedSubject()) {
$this->logger->debug('Activity "' . $event->getRichSubject() . '" was not parsed by any provider');
throw new \InvalidArgumentException('Activity "' . $event->getRichSubject() . '" was not parsed by any provider');
}
return $event;
}
/**
* Delete all entries we dealt with
*
* @param array $affectedUsers
* @param int $maxTime
*/
protected function deleteSentItems(array $affectedUsers, $maxTime) {
if (empty($affectedUsers)) {
return;
}
$query = $this->connection->getQueryBuilder();
$query->delete('activity_mq')
->where($query->expr()->lte('amq_timestamp', $query->createNamedParameter($maxTime, IQueryBuilder::PARAM_INT)))
->andWhere($query->expr()->in('amq_affecteduser', $query->createNamedParameter($affectedUsers, IQueryBuilder::PARAM_STR_ARRAY), IQueryBuilder::PARAM_STR));
$query->execute();
}
}
@@ -0,0 +1,158 @@
<?php
/**
* @copyright Copyright (c) 2017 Joas Schilling <coding@schilljs.com>
*
* @author Joas Schilling <coding@schilljs.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\Activity\Migration;
use Doctrine\DBAL\Types\Types;
use OCP\DB\ISchemaWrapper;
use OCP\Migration\IOutput;
use OCP\Migration\SimpleMigrationStep;
class Version2006Date20170808154933 extends SimpleMigrationStep {
/**
* @param IOutput $output
* @param \Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
* @param array $options
* @return null|ISchemaWrapper
* @since 13.0.0
*/
public function changeSchema(IOutput $output, \Closure $schemaClosure, array $options) {
/** @var ISchemaWrapper $schema */
$schema = $schemaClosure();
if (!$schema->hasTable('activity')) {
$table = $schema->createTable('activity');
$table->addColumn('activity_id', Types::BIGINT, [
'autoincrement' => true,
'notnull' => true,
'length' => 20,
]);
$table->addColumn('timestamp', 'integer', [
'notnull' => true,
'length' => 4,
'default' => 0,
]);
$table->addColumn('priority', 'integer', [
'notnull' => true,
'length' => 4,
'default' => 0,
]);
$table->addColumn('type', 'string', [
'notnull' => false,
'length' => 255,
]);
$table->addColumn('user', 'string', [
'notnull' => false,
'length' => 64,
]);
$table->addColumn('affecteduser', 'string', [
'notnull' => true,
'length' => 64,
]);
$table->addColumn('app', 'string', [
'notnull' => true,
'length' => 32,
]);
$table->addColumn('subject', 'string', [
'notnull' => true,
'length' => 255,
]);
$table->addColumn('subjectparams', 'text', [
'notnull' => true,
]);
$table->addColumn('message', 'string', [
'notnull' => false,
'length' => 255,
]);
$table->addColumn('messageparams', 'text', [
'notnull' => false,
]);
$table->addColumn('file', 'string', [
'notnull' => false,
'length' => 4000,
]);
$table->addColumn('link', 'string', [
'notnull' => false,
'length' => 4000,
]);
$table->addColumn('object_type', 'string', [
'notnull' => false,
'length' => 255,
]);
$table->addColumn('object_id', Types::BIGINT, [
'notnull' => true,
'length' => 20,
'default' => 0,
]);
$table->setPrimaryKey(['activity_id']);
$table->addIndex(['affecteduser', 'timestamp'], 'activity_user_time');
$table->addIndex(['affecteduser', 'user', 'timestamp'], 'activity_filter_by');
// FIXME Fixed install, see Version2006Date20170808155040: $table->addIndex(['affecteduser', 'app', 'timestamp'], 'activity_filter_app');
$table->addIndex(['affecteduser', 'type', 'app', 'timestamp'], 'activity_filter');
$table->addIndex(['object_type', 'object_id'], 'activity_object');
}
if (!$schema->hasTable('activity_mq')) {
$table = $schema->createTable('activity_mq');
$table->addColumn('mail_id', Types::BIGINT, [
'autoincrement' => true,
'notnull' => true,
'length' => 20,
]);
$table->addColumn('amq_timestamp', 'integer', [
'notnull' => true,
'length' => 4,
'default' => 0,
]);
$table->addColumn('amq_latest_send', 'integer', [
'notnull' => true,
'length' => 4,
'default' => 0,
]);
$table->addColumn('amq_type', 'string', [
'notnull' => true,
'length' => 255,
]);
$table->addColumn('amq_affecteduser', 'string', [
'notnull' => true,
'length' => 64,
]);
$table->addColumn('amq_appid', 'string', [
'notnull' => true,
'length' => 255,
]);
$table->addColumn('amq_subject', 'string', [
'notnull' => true,
'length' => 255,
]);
$table->addColumn('amq_subjectparams', 'text', [
'notnull' => true,
]);
$table->setPrimaryKey(['mail_id']);
$table->addIndex(['amq_affecteduser'], 'amp_user');
$table->addIndex(['amq_latest_send'], 'amp_latest_send_time');
$table->addIndex(['amq_timestamp'], 'amp_timestamp_time');
}
return $schema;
}
}
@@ -0,0 +1,55 @@
<?php
/**
* @copyright Copyright (c) 2017 Joas Schilling <coding@schilljs.com>
*
* @author Joas Schilling <coding@schilljs.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\Activity\Migration;
use OCP\DB\ISchemaWrapper;
use OCP\Migration\IOutput;
use OCP\Migration\SimpleMigrationStep;
class Version2006Date20170808155040 extends SimpleMigrationStep {
/**
* @param IOutput $output
* @param \Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
* @param array $options
* @return null|ISchemaWrapper
* @throws \Doctrine\DBAL\Schema\SchemaException
* @since 13.0.0
*/
public function changeSchema(IOutput $output, \Closure $schemaClosure, array $options) {
/** @var ISchemaWrapper $schema */
/**
* FIXME To prevent slowness on update we don't change the index.
* FIXME Anyone complaining can manually update it.
*
* $schema = $schemaClosure();
*
* $table = $schema->getTable('activity');
* $table->dropIndex('activity_filter_app');
* $table->addIndex(['affecteduser', 'type', 'app', 'timestamp'], 'activity_filter');
*
* return $schema;
*/
return null;
}
}
@@ -0,0 +1,59 @@
<?php
/**
* @copyright Copyright (c) 2017 Joas Schilling <coding@schilljs.com>
*
* @author Joas Schilling <coding@schilljs.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\Activity\Migration;
use OCP\DB\ISchemaWrapper;
use OCP\Migration\BigIntMigration;
use OCP\Migration\IOutput;
class Version2006Date20170919095939 extends BigIntMigration {
/**
* @return array Returns an array with the following structure
* ['table1' => ['column1', 'column2'], ...]
* @since 13.0.0
*/
protected function getColumnsByTable() {
return [
'activity' => ['activity_id', 'object_id'],
'activity_mq' => ['mail_id'],
];
}
/**
* @param IOutput $output
* @param \Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
* @param array $options
* @return null|ISchemaWrapper
* @since 13.0.0
*/
public function changeSchema(IOutput $output, \Closure $schemaClosure, array $options) {
/**
* FIXME To prevent slowness on update we don't change the schema.
* FIXME Instead it can be updated with ./occ db:convert-filecache-bigint
* parent::changeSchema($output, $schemaClosure, $options);
*/
return null;
}
}
@@ -0,0 +1,65 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2018, Joas Schilling <coding@schilljs.com>
*
* @author Joas Schilling <coding@schilljs.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\Activity\Migration;
use Closure;
use Doctrine\DBAL\Types\Types;
use OCP\DB\ISchemaWrapper;
use OCP\Migration\IOutput;
use OCP\Migration\SimpleMigrationStep;
class Version2007Date20181107114613 extends SimpleMigrationStep {
/**
* @param IOutput $output
* @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
* @param array $options
* @return null|ISchemaWrapper
*/
public function changeSchema(IOutput $output, Closure $schemaClosure, array $options) {
/** @var ISchemaWrapper $schema */
$schema = $schemaClosure();
$table = $schema->getTable('activity');
if (!$table->hasColumn('object_type')) {
$table->addColumn('object_type', Types::STRING, [
'notnull' => false,
'length' => 255,
]);
$table->addColumn('object_id', Types::BIGINT, [
'notnull' => true,
'length' => 20,
'default' => 0,
]);
}
if (!$table->hasIndex('activity_object')) {
$table->addIndex(['object_type', 'object_id'], 'activity_object');
}
return $schema;
}
}
@@ -0,0 +1,64 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2018, Joas Schilling <coding@schilljs.com>
*
* @author Joas Schilling <coding@schilljs.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\Activity\Migration;
use Closure;
use Doctrine\DBAL\Schema\SchemaException;
use Doctrine\DBAL\Types\Types;
use OCP\DB\ISchemaWrapper;
use OCP\Migration\IOutput;
use OCP\Migration\SimpleMigrationStep;
class Version2008Date20181011095117 extends SimpleMigrationStep {
/**
* @param IOutput $output
* @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
* @param array $options
* @return null|ISchemaWrapper
*/
public function changeSchema(IOutput $output, Closure $schemaClosure, array $options) {
/** @var ISchemaWrapper $schema */
$schema = $schemaClosure();
try {
$table = $schema->getTable('activity_mq');
} catch (SchemaException $e) {
return null;
}
$table->addColumn('object_type', Types::STRING, [
'notnull' => false,
'length' => 255,
]);
$table->addColumn('object_id', Types::BIGINT, [
'notnull' => true,
'length' => 20,
'default' => 0,
]);
return $schema;
}
}
@@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
namespace OCA\Activity\Migration;
use Closure;
use OCP\DB\ISchemaWrapper;
use OCP\Migration\IOutput;
use OCP\Migration\SimpleMigrationStep;
/**
* Auto-generated migration step: Please modify to your needs!
*/
class Version2010Date20190416112817 extends SimpleMigrationStep {
/**
* @param IOutput $output
* @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
* @param array $options
* @return null|ISchemaWrapper
*/
public function changeSchema(IOutput $output, Closure $schemaClosure, array $options) {
/** @var ISchemaWrapper $schema */
$schema = $schemaClosure();
$table = $schema->getTable('activity');
if ($table->hasIndex('activity_time')) {
$table->dropIndex('activity_time');
}
return $schema;
}
}
@@ -0,0 +1,88 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2020 Joas Schilling <coding@schilljs.com>
*
* @author Joas Schilling <coding@schilljs.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\Activity\Migration;
use Closure;
use Doctrine\DBAL\Types\Type;
use OCP\DB\ISchemaWrapper;
use OCP\IDBConnection;
use OCP\Migration\IOutput;
use OCP\Migration\SimpleMigrationStep;
class Version2011Date20201006132544 extends SimpleMigrationStep {
/** @var IDBConnection */
protected $connection;
public function __construct(IDBConnection $connection) {
$this->connection = $connection;
}
/**
* @param IOutput $output
* @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
* @param array $options
* @return null|ISchemaWrapper
*/
public function changeSchema(IOutput $output, Closure $schemaClosure, array $options) {
/** @var ISchemaWrapper $schema */
$schema = $schemaClosure();
$table = $schema->getTable('activity_mq');
$column = $table->getColumn('amq_appid');
$column->setType(Type::getType('string'));
$column->setNotnull(true);
$column->setLength(32);
$column = $table->getColumn('amq_subjectparams');
// Can't switch from Long to clob on Oracle, so we need an intermediate column
if ($column->getType() !== Type::getType('text')) {
$table->addColumn('amq_subjectparams2', 'text', [
'notnull' => false,
]);
}
return $schema;
}
/**
* {@inheritDoc}
*
* @since 13.0.0
*/
public function postSchemaChange(IOutput $output, \Closure $schemaClosure, array $options): void {
/** @var ISchemaWrapper $schema */
$schema = $schemaClosure();
if (!$schema->getTable('activity_mq')->hasColumn('amq_subjectparams2')) {
return;
}
$query = $this->connection->getQueryBuilder();
$query->update('activity_mq')
->set('amq_subjectparams2', 'amq_subjectparams');
$query->execute();
}
}
@@ -0,0 +1,53 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2020 Joas Schilling <coding@schilljs.com>
*
* @author Joas Schilling <coding@schilljs.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\Activity\Migration;
use Closure;
use Doctrine\DBAL\Types\Type;
use OCP\DB\ISchemaWrapper;
use OCP\Migration\IOutput;
use OCP\Migration\SimpleMigrationStep;
class Version2011Date20201006132545 extends SimpleMigrationStep {
/**
* @param IOutput $output
* @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
* @param array $options
* @return null|ISchemaWrapper
*/
public function changeSchema(IOutput $output, Closure $schemaClosure, array $options) {
/** @var ISchemaWrapper $schema */
$schema = $schemaClosure();
$table = $schema->getTable('activity_mq');
$column = $table->getColumn('amq_subjectparams');
if ($column->getType() !== Type::getType('text')) {
$table->dropColumn('amq_subjectparams');
return $schema;
}
return null;
}
}
@@ -0,0 +1,81 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2020 Joas Schilling <coding@schilljs.com>
*
* @author Joas Schilling <coding@schilljs.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\Activity\Migration;
use Closure;
use OCP\DB\ISchemaWrapper;
use OCP\IDBConnection;
use OCP\Migration\IOutput;
use OCP\Migration\SimpleMigrationStep;
class Version2011Date20201006132546 extends SimpleMigrationStep {
/** @var IDBConnection */
protected $connection;
public function __construct(IDBConnection $connection) {
$this->connection = $connection;
}
/**
* @param IOutput $output
* @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
* @param array $options
* @return null|ISchemaWrapper
*/
public function changeSchema(IOutput $output, Closure $schemaClosure, array $options) {
/** @var ISchemaWrapper $schema */
$schema = $schemaClosure();
$table = $schema->getTable('activity_mq');
if (!$table->hasColumn('amq_subjectparams')) {
$table->addColumn('amq_subjectparams', 'text', [
'notnull' => false,
]);
return $schema;
}
return null;
}
/**
* {@inheritDoc}
*
* @since 13.0.0
*/
public function postSchemaChange(IOutput $output, \Closure $schemaClosure, array $options): void {
/** @var ISchemaWrapper $schema */
$schema = $schemaClosure();
if (!$schema->getTable('activity_mq')->hasColumn('amq_subjectparams2')
|| !$schema->getTable('activity_mq')->hasColumn('amq_subjectparams')) {
return;
}
$query = $this->connection->getQueryBuilder();
$query->update('activity_mq')
->set('amq_subjectparams', 'amq_subjectparams2');
$query->execute();
}
}
@@ -0,0 +1,51 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2020 Joas Schilling <coding@schilljs.com>
*
* @author Joas Schilling <coding@schilljs.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\Activity\Migration;
use Closure;
use OCP\DB\ISchemaWrapper;
use OCP\Migration\IOutput;
use OCP\Migration\SimpleMigrationStep;
class Version2011Date20201006132547 extends SimpleMigrationStep {
/**
* @param IOutput $output
* @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
* @param array $options
* @return null|ISchemaWrapper
*/
public function changeSchema(IOutput $output, Closure $schemaClosure, array $options) {
/** @var ISchemaWrapper $schema */
$schema = $schemaClosure();
$table = $schema->getTable('activity_mq');
if ($table->hasColumn('amq_subjectparams2')) {
$table->dropColumn('amq_subjectparams2');
return $schema;
}
return null;
}
}
@@ -0,0 +1,61 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2020 Joas Schilling <coding@schilljs.com>
*
* @author Joas Schilling <coding@schilljs.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\Activity\Migration;
use Closure;
use OCP\DB\ISchemaWrapper;
use OCP\Migration\IOutput;
use OCP\Migration\SimpleMigrationStep;
class Version2011Date20201207091915 extends SimpleMigrationStep {
/**
* @param IOutput $output
* @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
* @param array $options
* @return null|ISchemaWrapper
*/
public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper {
/** @var ISchemaWrapper $schema */
$schema = $schemaClosure();
$result = $this->ensureColumnIsNullable($schema, 'activity_mq', 'amq_subjectparams');
return $result ? $schema : null;
}
protected function ensureColumnIsNullable(ISchemaWrapper $schema, string $tableName, string $columnName): bool {
$table = $schema->getTable($tableName);
$column = $table->getColumn($columnName);
if ($column->getNotnull()) {
$column->setNotnull(false);
return true;
}
return false;
}
}
@@ -0,0 +1,144 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2020 Robin Appelman <robin@icewind.nl>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\Activity;
use OCP\Activity\IEvent;
use OCP\Activity\IManager as ActivityManager;
use OCP\IL10N;
use OCP\Notification\IManager as NotificationManager;
use OCP\Notification\INotification;
use OCP\Notification\INotifier;
class NotificationGenerator implements INotifier {
public function __construct(
protected Data $data,
protected ActivityManager $activityManager,
protected NotificationManager $notificationManager,
protected UserSettings $userSettings,
protected IL10N $l10n) {
}
public function deferNotifications(): bool {
return $this->notificationManager->defer();
}
public function flushNotifications() {
$this->notificationManager->flush();
}
public function sendNotificationForEvent(IEvent $event, int $activityId) {
$selfAction = $event->getAffectedUser() === $event->getAuthor();
$notifySetting = $this->userSettings->getUserSetting($event->getAffectedUser(), 'notification', $event->getType());
if ($notifySetting && !$selfAction && $event->getGenerateNotification()) {
$this->notificationManager->notify($this->getNotificationForEvent($event, $activityId));
}
}
private function getNotificationForEvent(IEvent $event, int $activityId): INotification {
$notification = $this->notificationManager->createNotification();
$notification->setApp($event->getApp());
$notification->setUser($event->getAffectedUser());
$notification->setDateTime(\DateTime::createFromFormat('U', (string)$event->getTimestamp()));
$notification->setObject('activity_notification', (string)$activityId);
$notification->setSubject($event->getSubject(), $event->getSubjectParameters());
if ($event->getRichSubject()) {
$notification->setRichSubject($event->getRichSubject(), $event->getRichSubjectParameters());
}
if ($event->getRichMessage()) {
$notification->setRichMessage($event->getRichMessage(), $event->getRichMessageParameters());
}
if ($event->getMessage()) {
$notification->setMessage($event->getMessage(), $event->getMessageParameters());
}
if ($event->getLink()) {
$notification->setLink($event->getLink());
}
return $notification;
}
private function populateEvent(IEvent $event, string $language) {
$this->activityManager->setFormattingObject($event->getObjectType(), $event->getObjectId());
foreach ($this->activityManager->getProviders() as $provider) {
try {
$event = $provider->parse($language, $event);
} catch (\InvalidArgumentException $e) {
}
}
$this->activityManager->setFormattingObject('', 0);
return $event;
}
public function getID(): string {
return 'activity';
}
public function getName(): string {
return 'Activity';
}
public function prepare(INotification $notification, string $languageCode): INotification {
if ($notification->getObjectType() !== 'activity_notification') {
throw new \InvalidArgumentException();
}
$event = $this->data->getById((int)$notification->getObjectId());
if (!$event || $event->getAffectedUser() !== $notification->getUser()) {
throw new \InvalidArgumentException();
}
$this->activityManager->setCurrentUserId($notification->getUser());
$event = $this->populateEvent($event, $languageCode);
$this->activityManager->setCurrentUserId(null);
return $this->getDisplayNotificationForEvent($event, $event->getObjectId());
}
private function getDisplayNotificationForEvent(IEvent $event, int $activityId): INotification {
$notification = $this->getNotificationForEvent($event, $activityId);
$notification->setRichSubject($event->getRichSubject(), $event->getRichSubjectParameters());
$notification->setParsedSubject($event->getParsedSubject());
if ($event->getIcon()) {
$notification->setIcon($event->getIcon());
}
if ($event->getRichMessage()) {
$notification->setRichMessage($event->getRichMessage(), $event->getRichMessageParameters());
}
if ($event->getParsedMessage()) {
$notification->setParsedMessage($event->getParsedMessage());
}
return $notification;
}
}
@@ -0,0 +1,131 @@
<?php
/**
* @copyright Copyright (c) 2017 Joas Schilling <coding@schilljs.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\Activity\Settings;
use OCA\Activity\UserSettings;
use OCP\Activity\ActivitySettings;
use OCP\Activity\IExtension;
use OCP\Activity\IManager;
use OCP\AppFramework\Http\TemplateResponse;
use OCP\AppFramework\Services\IInitialState;
use OCP\IConfig;
use OCP\IL10N;
use OCP\Settings\ISettings;
class Admin implements ISettings {
private IConfig $config;
private IL10N $l10n;
private IManager $manager;
private UserSettings $userSettings;
private IInitialState $initialState;
public function __construct(IConfig $config, IL10N $l10n, UserSettings $userSettings, IManager $manager, IInitialState $initialState) {
$this->config = $config;
$this->l10n = $l10n;
$this->manager = $manager;
$this->userSettings = $userSettings;
$this->initialState = $initialState;
}
public function getForm(): TemplateResponse {
$settings = $this->manager->getSettings();
usort($settings, static function (ActivitySettings $a, ActivitySettings $b): int {
if ($a->getPriority() === $b->getPriority()) {
return (int) ($a->getIdentifier() > $b->getIdentifier());
}
return (int) ($a->getPriority() > $b->getPriority());
});
$activityGroups = [];
foreach ($settings as $setting) {
if (!$setting->canChangeMail() && !$setting->canChangeNotification()) {
// No setting can be changed => don't display
continue;
}
$methods = [];
if ($setting->canChangeMail()) {
$methods[] = IExtension::METHOD_MAIL;
}
if ($setting->canChangeNotification()) {
$methods[] = IExtension::METHOD_NOTIFICATION;
}
$identifier = $setting->getIdentifier();
$groupIdentifier = $setting->getGroupIdentifier();
if (!isset($activityGroups[$groupIdentifier])) {
$activityGroups[$groupIdentifier] = [
'activities' => [],
'name' => $setting->getGroupName()
];
}
$activityGroups[$groupIdentifier]['activities'][$identifier] = [
'desc' => $setting->getName(),
IExtension::METHOD_MAIL => $this->userSettings->getAdminSetting('email', $identifier),
IExtension::METHOD_NOTIFICATION => $this->userSettings->getAdminSetting('notification', $identifier),
'methods' => $methods,
];
}
if (isset($activityGroups['other'])) {
$otherActivities = $activityGroups['other'];
unset($activityGroups['other']);
$activityGroups['other'] = $otherActivities;
}
$settingBatchTime = UserSettings::EMAIL_SEND_HOURLY;
$currentSetting = (int) $this->userSettings->getAdminSetting('setting', 'batchtime');
if ($currentSetting === 3600 * 24 * 7) {
$settingBatchTime = UserSettings::EMAIL_SEND_WEEKLY;
} elseif ($currentSetting === 3600 * 24) {
$settingBatchTime = UserSettings::EMAIL_SEND_DAILY;
} elseif ($currentSetting === 0) {
$settingBatchTime = UserSettings::EMAIL_SEND_ASAP;
}
$this->initialState->provideInitialState('setting', 'admin');
$this->initialState->provideInitialState('activity_groups', $activityGroups);
$this->initialState->provideInitialState('is_email_set', true);
$this->initialState->provideInitialState('email_enabled', $this->config->getAppValue('activity', 'enable_email', 'yes') === 'yes');
$this->initialState->provideInitialState('setting_batchtime', $settingBatchTime);
$this->initialState->provideInitialState('methods', [
IExtension::METHOD_MAIL => $this->l10n->t('Mail'),
IExtension::METHOD_NOTIFICATION => $this->l10n->t('Push'),
]);
return new TemplateResponse('activity', 'settings/admin', [], 'blank');
}
public function getSection(): string {
return 'activity';
}
public function getPriority(): int {
return 55;
}
}
@@ -0,0 +1,88 @@
<?php
/**
* @copyright Copyright (c) 2017 Joas Schilling <coding@schilljs.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\Activity\Settings;
use OCP\IL10N;
use OCP\IURLGenerator;
use OCP\Settings\IIconSection;
class AdminSection implements IIconSection {
/** @var IL10N */
private $l;
/** @var IURLGenerator */
private $url;
/**
* @param IURLGenerator $url
* @param IL10N $l
*/
public function __construct(IURLGenerator $url, IL10N $l) {
$this->url = $url;
$this->l = $l;
}
/**
* returns the relative path to an 16*16 icon describing the section.
* e.g. '/core/img/places/files.svg'
*
* @returns string
* @since 12
*/
public function getIcon() {
return $this->url->imagePath('activity', 'activity-dark.svg');
}
/**
* returns the ID of the section. It is supposed to be a lower case string,
* e.g. 'ldap'
*
* @returns string
* @since 9.1
*/
public function getID() {
return 'activity';
}
/**
* returns the translated name as it should be displayed, e.g. 'LDAP / AD
* integration'. Use the L10N service to translate it.
*
* @return string
* @since 9.1
*/
public function getName() {
return $this->l->t('Activity');
}
/**
* @return int whether the form should be rather on the top or bottom of
* the settings navigation. The sections are arranged in ascending order of
* the priority values. It is required to return a value between 0 and 99.
*
* E.g.: 70
* @since 9.1
*/
public function getPriority() {
return 55;
}
}
@@ -0,0 +1,177 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Joas Schilling <coding@schilljs.com>
* @author Lukas Reschke <lukas@statuscode.ch>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
namespace OCA\Activity\Settings;
use OCA\Activity\CurrentUser;
use OCA\Activity\UserSettings;
use OCP\Activity\ActivitySettings;
use OCP\Activity\IExtension;
use OCP\Activity\IManager;
use OCP\AppFramework\Http\TemplateResponse;
use OCP\AppFramework\Services\IInitialState;
use OCP\IConfig;
use OCP\IL10N;
use OCP\IUser;
use OCP\Settings\ISettings;
class Personal implements ISettings {
private IConfig $config;
private IManager $manager;
private UserSettings $userSettings;
private IL10N $l10n;
private string $userId;
private IUser $user;
private IInitialState $initialState;
public function __construct(
IConfig $config,
IManager $manager,
UserSettings $userSettings,
IL10N $l10n,
CurrentUser $currentUser,
IInitialState $initialState
) {
$this->config = $config;
$this->manager = $manager;
$this->userSettings = $userSettings;
$this->l10n = $l10n;
$this->userId = (string) $currentUser->getUID();
$this->user = $currentUser->getUser();
$this->initialState = $initialState;
}
public function getForm(): TemplateResponse {
$settings = $this->manager->getSettings();
usort($settings, static function (ActivitySettings $a, ActivitySettings $b): int {
if ($a->getPriority() === $b->getPriority()) {
return (int) ($a->getIdentifier() > $b->getIdentifier());
}
return (int) ($a->getPriority() > $b->getPriority());
});
$activityGroups = [];
foreach ($settings as $setting) {
if (!$setting->canChangeMail() && !$setting->canChangeNotification()) {
// No setting can be changed => don't display
continue;
}
$methods = [];
if ($setting->canChangeMail()) {
$methods[] = IExtension::METHOD_MAIL;
}
if ($setting->canChangeNotification()) {
$methods[] = IExtension::METHOD_NOTIFICATION;
}
$identifier = $setting->getIdentifier();
$groupIdentifier = $setting->getGroupIdentifier();
if (!isset($activityGroups[$groupIdentifier])) {
$activityGroups[$groupIdentifier] = [
'activities' => [],
'name' => $setting->getGroupName()
];
}
$activityGroups[$groupIdentifier]['activities'][$identifier] = [
'desc' => $setting->getName(),
IExtension::METHOD_MAIL => $this->userSettings->getUserSetting($this->userId, 'email', $identifier),
IExtension::METHOD_NOTIFICATION => $this->userSettings->getUserSetting($this->userId, 'notification', $identifier),
'methods' => $methods,
];
}
if (isset($activityGroups['other'])) {
$otherActivities = $activityGroups['other'];
unset($activityGroups['other']);
$activityGroups['other'] = $otherActivities;
}
$settingBatchTime = UserSettings::EMAIL_SEND_HOURLY;
$currentSetting = (int) $this->userSettings->getUserSetting($this->userId, 'setting', 'batchtime');
if ($currentSetting === 3600 * 24 * 7) {
$settingBatchTime = UserSettings::EMAIL_SEND_WEEKLY;
} elseif ($currentSetting === 3600 * 24) {
$settingBatchTime = UserSettings::EMAIL_SEND_DAILY;
} elseif ($currentSetting === 0) {
$settingBatchTime = UserSettings::EMAIL_SEND_ASAP;
}
$emailEnabled = $this->config->getAppValue('activity', 'enable_email', 'yes') === 'yes';
if ($emailEnabled) {
$methods = [
IExtension::METHOD_MAIL => $this->l10n->t('Mail'),
];
} else {
$methods = [];
}
if ($this->config->getAppValue('activity', 'enable_notify', 'yes') === 'yes') {
$methods[IExtension::METHOD_NOTIFICATION] = $this->l10n->t('Push');
}
$this->initialState->provideInitialState('setting', 'personal');
$this->initialState->provideInitialState('activity_groups', $activityGroups);
$this->initialState->provideInitialState('is_email_set', $this->user instanceof IUser && !empty($this->user->getEMailAddress()));
$this->initialState->provideInitialState('email_enabled', $emailEnabled);
$this->initialState->provideInitialState('setting_batchtime', $settingBatchTime);
$this->initialState->provideInitialState('methods', $methods);
$this->initialState->provideInitialState('activity_digest_enabled', $this->userSettings->getUserSetting($this->userId, 'setting', 'activity_digest'));
return new TemplateResponse('activity', 'settings/personal', [
'setting' => 'personal',
'activityGroups' => $activityGroups,
'is_email_set' => $this->user instanceof IUser && !empty($this->user->getEMailAddress()),
'email_enabled' => $emailEnabled,
'setting_batchtime' => $settingBatchTime,
'methods' => $methods,
'activity_digest_enabled' => $this->userSettings->getUserSetting($this->userId, 'setting', 'activity_digest')
], 'blank');
}
/**
* @return string the section ID, e.g. 'sharing'
*/
public function getSection() {
return 'notifications';
}
/**
* @return int whether the form should be rather on the top or bottom of
* the admin section. The forms are arranged in ascending order of the
* priority values. It is required to return a value between 0 and 100.
*
* E.g.: 70
*/
public function getPriority() {
return 55;
}
}
@@ -0,0 +1,91 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2017 Joas Schilling <coding@schilljs.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\Activity\Settings;
use OCP\IL10N;
use OCP\IURLGenerator;
use OCP\Settings\IIconSection;
class PersonalSection implements IIconSection {
/** @var IL10N */
private $l;
/** @var IURLGenerator */
private $url;
/**
* @param IURLGenerator $url
* @param IL10N $l
*/
public function __construct(IURLGenerator $url, IL10N $l) {
$this->url = $url;
$this->l = $l;
}
/**
* returns the relative path to an 16*16 icon describing the section.
* e.g. '/core/img/places/files.svg'
*
* @returns string
* @since 12
*/
public function getIcon() {
return $this->url->imagePath('activity', 'notifications-dark.svg');
}
/**
* returns the ID of the section. It is supposed to be a lower case string,
* e.g. 'ldap'
*
* @returns string
* @since 9.1
*/
public function getID() {
return 'notifications';
}
/**
* returns the translated name as it should be displayed, e.g. 'LDAP / AD
* integration'. Use the L10N service to translate it.
*
* @return string
* @since 9.1
*/
public function getName() {
return $this->l->t('Notifications');
}
/**
* @return int whether the form should be rather on the top or bottom of
* the settings navigation. The sections are arranged in ascending order of
* the priority values. It is required to return a value between 0 and 99.
*
* E.g.: 70
* @since 9.1
*/
public function getPriority() {
return 10;
}
}
@@ -0,0 +1,246 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Joas Schilling <coding@schilljs.com>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
namespace OCA\Activity;
use OCP\Activity\ActivitySettings;
use OCP\Activity\IManager;
use OCP\IConfig;
/**
* Class UserSettings
*
* @package OCA\Activity
*/
class UserSettings {
protected Data $data;
public const EMAIL_SEND_HOURLY = 0;
public const EMAIL_SEND_DAILY = 1;
public const EMAIL_SEND_WEEKLY = 2;
public const EMAIL_SEND_ASAP = 3;
/**
* @param IManager $manager
* @param IConfig $config
*/
public function __construct(protected IManager $manager, protected IConfig $config) {
}
/**
* Get the user setting
* Falling back to the admin default if not set for the user
*
* Falls back to some good default values if the user does not have a preference
*
* @param string $user
* @param string $method Should be one of 'stream', 'email' or 'setting'
* @param string $type One of the activity types, 'batchtime' or 'self'
* @return bool|int
*/
public function getUserSetting($user, $method, $type) {
if ($method === 'email' && $this->config->getAppValue('activity', 'enable_email', 'yes') === 'no') {
return false;
}
$defaultSetting = $this->getAdminSetting($method, $type);
if (!$this->canModifySetting($method, $type)) {
return $defaultSetting;
}
if (is_bool($defaultSetting)) {
return (bool) $this->config->getUserValue(
$user,
'activity',
'notify_' . $method . '_' . $type,
$defaultSetting
);
}
return (int) $this->config->getUserValue(
$user,
'activity',
'notify_' . $method . '_' . $type,
$defaultSetting
);
}
/**
* Get the admin configured default for the setting
* Falling back to the implementation default if not set by the admin
*
* @param string $method
* @param string $type
* @return bool|int
*/
public function getAdminSetting($method, $type) {
$defaultSetting = $this->getDefaultSetting($method, $type);
if (is_bool($defaultSetting)) {
return (bool) $this->config->getAppValue(
'activity',
'notify_' . $method . '_' . $type,
(string) $defaultSetting
);
}
return (int) $this->config->getAppValue(
'activity',
'notify_' . $method . '_' . $type,
(string) $defaultSetting
);
}
/**
* Get default setting for a preference from the implementation
*
* @param string $method Should be one of 'stream', 'email' or 'setting'
* @param string $type One of the activity types, 'batchtime', 'self' or 'selfemail'
* @return bool|int
*/
protected function getDefaultSetting($method, $type) {
if ($method === 'setting') {
if ($type === 'batchtime') {
return 3600;
}
if ($type === 'self') {
return true;
}
if ($type === 'selfemail') {
return false;
}
return false;
}
try {
$setting = $this->manager->getSettingById($type);
switch ($method) {
case 'email':
return $setting->isDefaultEnabledMail();
case 'notification':
return $setting->isDefaultEnabledNotification();
default:
return false;
}
} catch (\InvalidArgumentException $e) {
return false;
}
}
/**
* Get a good default setting for a preference
*
* @param string $method Should be one of 'stream', 'email' or 'setting'
* @param string $type One of the activity types, 'batchtime', 'self' or 'selfemail'
* @return bool
*/
protected function canModifySetting($method, $type) {
if ($method === 'setting') {
return true;
}
try {
$setting = $this->manager->getSettingById($type);
switch ($method) {
case 'email':
return $setting->canChangeMail();
case 'notification':
return $setting->canChangeNotification();
default:
return false;
}
} catch (\InvalidArgumentException $e) {
return false;
}
}
/**
* Get a list with all notification types
*/
public function getNotificationTypes() {
$settings = $this->manager->getSettings();
$return = array_map(function (ActivitySettings $setting) {
return $setting->getIdentifier();
}, $settings);
if (array_search('file_changed', $return) !== false) {
array_push($return, 'file_created', 'file_deleted', 'file_restored');
}
return $return;
}
/**
* Filters the given user array by their notification setting
*
* @param array $users
* @param string $method
* @param string $type
* @return array Returns a "username => b:true" Map for method = notification
* Returns a "username => i:batchtime" Map for method = email
*/
public function filterUsersBySetting($users, $method, $type) {
if (empty($users)) {
return [];
}
if ($method === 'email' && $this->config->getAppValue('activity', 'enable_email', 'yes') === 'no') {
return [];
}
$filteredUsers = [];
$potentialUsers = $this->config->getUserValueForUsers('activity', 'notify_' . $method . '_' . $type, $users);
foreach ($potentialUsers as $user => $value) {
if ($value) {
$filteredUsers[$user] = true;
}
unset($users[array_search($user, $users, true)]);
}
// Get the batch time setting from the database
if ($method === 'email') {
$potentialUsers = $this->config->getUserValueForUsers('activity', 'notify_setting_batchtime', array_keys($filteredUsers));
foreach ($potentialUsers as $user => $value) {
$filteredUsers[$user] = $value;
}
}
if (empty($users)) {
return $filteredUsers;
}
// If the setting is enabled by default,
// we add all users that didn't set the preference yet.
if ($this->getDefaultSetting($method, $type)) {
foreach ($users as $user) {
if ($method === 'notification') {
$filteredUsers[$user] = true;
} else {
$filteredUsers[$user] = $this->getDefaultSetting('setting', 'batchtime');
}
}
}
return $filteredUsers;
}
}
@@ -0,0 +1,119 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Joas Schilling <coding@schilljs.com>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
namespace OCA\Activity;
use OCP\Files\Folder;
use OCP\Files\IRootFolder;
use OCP\Files\Node;
use OCP\Files\NotFoundException;
class ViewInfoCache {
/** @var array */
protected $cachePath;
/** @var array */
protected $cacheId;
public function __construct(protected IRootFolder $rootFolder) {
}
/**
* @param string $user
* @param int $fileId
* @param string $path
* @return array
*/
public function getInfoById($user, $fileId, $path) {
if (isset($this->cacheId[$user][$fileId])) {
$cache = $this->cacheId[$user][$fileId];
if ($cache['path'] === null) {
$cache['path'] = $path;
}
return $cache;
}
return $this->findInfoById($user, $fileId, $path);
}
/**
* @param string $user
* @param int $fileId
* @param string $filePath
* @return array
*/
protected function findInfoById($user, $fileId, $filePath) {
$cache = [
'path' => $filePath,
'exists' => false,
'is_dir' => false,
'view' => '',
];
$notFound = false;
try {
$userFolder = $this->rootFolder->getUserFolder($user);
$entries = $userFolder->getById($fileId);
if (empty($entries)) {
throw new NotFoundException('No entries returned');
}
/** @var Node $entry */
$entry = array_shift($entries);
$cache['path'] = $userFolder->getRelativePath($entry->getPath());
$cache['is_dir'] = $entry instanceof Folder;
$cache['exists'] = true;
$cache['node'] = $entry;
} catch (NotFoundException $e) {
// The file was not found in the normal view,
// maybe it is in the trashbin?
try {
/** @var Folder $userTrashBin */
$userTrashBin = $this->rootFolder->get('/' . $user . '/files_trashbin');
$entries = $userTrashBin->getById($fileId);
if (empty($entries)) {
throw new NotFoundException('No entries returned');
}
/** @var Node $entry */
$entry = array_shift($entries);
$cache = [
'path' => $userTrashBin->getRelativePath($entry->getPath()),
'exists' => true,
'is_dir' => $entry instanceof Folder,
'view' => 'trashbin',
'node' => $entry,
];
} catch (NotFoundException $e) {
$notFound = true;
}
}
$this->cacheId[$user][$fileId] = $cache;
if ($notFound) {
$this->cacheId[$user][$fileId]['path'] = null;
}
return $cache;
}
}