From e6757e42e04e655741ee89ee4a8532d2cbf564d2 Mon Sep 17 00:00:00 2001 From: Carl Schwan Date: Thu, 8 Sep 2022 11:11:39 +0200 Subject: [PATCH 01/18] Show share expiration if it exits Signed-off-by: Carl Schwan Signed-off-by: Florent Poinsaut --- lib/Service/SharesList.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/Service/SharesList.php b/lib/Service/SharesList.php index ed61d724..fe09c089 100644 --- a/lib/Service/SharesList.php +++ b/lib/Service/SharesList.php @@ -113,7 +113,7 @@ public function get(string $userId, int $filter, string $path = null, string $to }, $shares); } - $shares = iter\filter(function (IShare $share) { + $shares = iter\filter(function (IShare $share): bool { try { $userFolder = $this->rootFolder->getUserFolder($share->getShareOwner()); } catch (NoUserException $e) { @@ -261,6 +261,10 @@ public function formatShare(IShare $share): array { $data['recipient'] = $share->getSharedWith(); } + if ($share->getExpirationDate() !== null) { + $data['expiration'] = $share->getExpirationDate()->format('Y-m-d H:i:s'); + } + return $data; } } From 49f42c7d321e43ea561640d498dd9306e64670f4 Mon Sep 17 00:00:00 2001 From: Carl Schwan Date: Thu, 8 Sep 2022 12:33:34 +0200 Subject: [PATCH 02/18] Add filtering Signed-off-by: Carl Schwan Signed-off-by: Florent Poinsaut --- lib/Command/ListShares.php | 6 +++++- lib/Service/SharesList.php | 20 +++++++++++++++++--- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/lib/Command/ListShares.php b/lib/Command/ListShares.php index 808dc5fb..a3a5970d 100644 --- a/lib/Command/ListShares.php +++ b/lib/Command/ListShares.php @@ -87,7 +87,7 @@ public function configure() { 'filter', 'f', InputOption::VALUE_OPTIONAL, - 'Filter shares, possible values: owner, initiator, recipient, token' + 'Filter shares, possible values: owner, initiator, recipient, token, has-expiration, no-expiration' ); parent::configure(); } @@ -104,6 +104,10 @@ protected function execute(InputInterface $input, OutputInterface $output): int $filter = SharesList::FILTER_INITIATOR; } else if ($filter === 'recipient') { $filter = SharesList::FILTER_RECIPIENT; + } else if ($filter === 'has-expiration') { + $filter = SharesList::FILTER_HAS_EXPIRATION; + } else if ($filter === 'no-expiration') { + $filter = SharesList::FILTER_NO_EXPIRATION; } else { $filter = SharesList::FILTER_NONE; } diff --git a/lib/Service/SharesList.php b/lib/Service/SharesList.php index fe09c089..d8de0c41 100644 --- a/lib/Service/SharesList.php +++ b/lib/Service/SharesList.php @@ -42,6 +42,8 @@ class SharesList { const FILTER_INITIATOR = 2; const FILTER_RECIPIENT = 3; const FILTER_TOKEN = 4; + const FILTER_HAS_EXPIRATION = 5; + const FILTER_NO_EXPIRATION = 6; /** @var ShareManager */ private $shareManager; @@ -93,7 +95,7 @@ public function get(string $userId, int $filter, string $path = null, string $to }, $shares); } if ($token !== null) { - $shares = [$this->shareManager->getShareByToken($token)]; + $shares = [$this->shareManager->getShareByToken($token)]; } if ($filter === self::FILTER_OWNER) { @@ -113,6 +115,18 @@ public function get(string $userId, int $filter, string $path = null, string $to }, $shares); } + if ($filter === self::FILTER_HAS_EXPIRATION) { + $shares = iter\filter(function (IShare $share) use ($userId): bool { + return $share->getExpirationDate() !== null; + }, $shares); + } + + if ($filter === self::FILTER_NO_EXPIRATION) { + $shares = iter\filter(function (IShare $share) use ($userId): bool { + return $share->getExpirationDate() === null; + }, $shares); + } + $shares = iter\filter(function (IShare $share): bool { try { $userFolder = $this->rootFolder->getUserFolder($share->getShareOwner()); @@ -190,10 +204,10 @@ public function getSub(string $userId, int $filter, string $path): \Iterator { return $shares; } - public function getFormattedShares(string $userId = '', int $filter, string $path = null, string $token = null): \Iterator { + public function getFormattedShares(string $userId = '', int $filter = self::FILTER_NONE, string $path = null, string $token = null): \Iterator { $shares = $this->get($userId, $filter, $path, $token); - $formattedShares = iter\map(function (IShare $share) { + $formattedShares = iter\map(function (IShare $share): array { return $this->formatShare($share); }, $shares); From 6e8a33819c198b247e2d62bc746ed76b3df27363 Mon Sep 17 00:00:00 2001 From: Florent Poinsaut Date: Tue, 27 Sep 2022 16:45:02 +0000 Subject: [PATCH 03/18] Send shares list by email (JSON + CSV) Signed-off-by: Florent Poinsaut --- appinfo/info.xml | 1 + composer.json | 3 +- composer.lock | 571 ++++++++++++++---------- lib/BackgroundJob/EmailNotification.php | 68 +++ lib/Command/ListShares.php | 17 +- lib/Command/SendShares.php | 147 ++++++ lib/Service/ReportSender.php | 140 ++++++ lib/Service/SharesList.php | 26 ++ 8 files changed, 723 insertions(+), 250 deletions(-) create mode 100644 lib/BackgroundJob/EmailNotification.php create mode 100644 lib/Command/SendShares.php create mode 100644 lib/Service/ReportSender.php diff --git a/appinfo/info.xml b/appinfo/info.xml index 8d200cea..ed4e8bef 100644 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -17,5 +17,6 @@ OCA\ShareListing\Command\ListShares + OCA\ShareListing\Command\SendShares diff --git a/composer.json b/composer.json index 456c72c3..45376fd8 100644 --- a/composer.json +++ b/composer.json @@ -3,7 +3,8 @@ "description": "3rdparty components for sharelisting", "license": "MIT", "require": { - "nikic/iter": "^2.2" + "nikic/iter": "^2.2", + "symfony/serializer": "^5.4" }, "require-dev": { "phpunit/phpunit": "^9", diff --git a/composer.lock b/composer.lock index 247c0554..c9c367d4 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "e962911f90e4296ff4f9b36c97b0f673", + "content-hash": "280059a3e5c545fce72e8e3f92894660", "packages": [ { "name": "nikic/iter", @@ -55,6 +55,341 @@ "source": "https://github.com/nikic/iter/tree/v2.2.0" }, "time": "2021-08-02T15:04:32+00:00" + }, + { + "name": "symfony/deprecation-contracts", + "version": "v2.5.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "e8b495ea28c1d97b5e0c121748d6f9b53d075c66" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/e8b495ea28c1d97b5e0c121748d6f9b53d075c66", + "reference": "e8b495ea28c1d97b5e0c121748d6f9b53d075c66", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.5-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "autoload": { + "files": [ + "function.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "A generic function and convention to trigger deprecation notices", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/deprecation-contracts/tree/v2.5.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-01-02T09:53:40+00:00" + }, + { + "name": "symfony/polyfill-ctype", + "version": "v1.25.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "30885182c981ab175d4d034db0f6f469898070ab" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/30885182c981ab175d4d034db0f6f469898070ab", + "reference": "30885182c981ab175d4d034db0f6f469898070ab", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "provide": { + "ext-ctype": "*" + }, + "suggest": { + "ext-ctype": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.23-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Ctype\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Gert de Pagter", + "email": "BackEndTea@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for ctype functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "ctype", + "polyfill", + "portable" + ], + "support": { + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.25.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2021-10-20T20:35:02+00:00" + }, + { + "name": "symfony/polyfill-php80", + "version": "v1.25.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php80.git", + "reference": "4407588e0d3f1f52efb65fbe92babe41f37fe50c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/4407588e0d3f1f52efb65fbe92babe41f37fe50c", + "reference": "4407588e0d3f1f52efb65fbe92babe41f37fe50c", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.23-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php80\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ion Bazan", + "email": "ion.bazan@gmail.com" + }, + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php80/tree/v1.25.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-03-04T08:16:47+00:00" + }, + { + "name": "symfony/serializer", + "version": "v5.4.12", + "source": { + "type": "git", + "url": "https://github.com/symfony/serializer.git", + "reference": "776fa3010f62b97a7119757a66596a654cd244d4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/serializer/zipball/776fa3010f62b97a7119757a66596a654cd244d4", + "reference": "776fa3010f62b97a7119757a66596a654cd244d4", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "symfony/deprecation-contracts": "^2.1|^3", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-php80": "^1.16" + }, + "conflict": { + "doctrine/annotations": "<1.12", + "phpdocumentor/reflection-docblock": "<3.2.2", + "phpdocumentor/type-resolver": "<1.4.0", + "symfony/dependency-injection": "<4.4", + "symfony/property-access": "<5.4", + "symfony/property-info": "<5.3.13", + "symfony/uid": "<5.3", + "symfony/yaml": "<4.4" + }, + "require-dev": { + "doctrine/annotations": "^1.12", + "phpdocumentor/reflection-docblock": "^3.2|^4.0|^5.0", + "symfony/cache": "^4.4|^5.0|^6.0", + "symfony/config": "^4.4|^5.0|^6.0", + "symfony/dependency-injection": "^4.4|^5.0|^6.0", + "symfony/error-handler": "^4.4|^5.0|^6.0", + "symfony/filesystem": "^4.4|^5.0|^6.0", + "symfony/form": "^4.4|^5.0|^6.0", + "symfony/http-foundation": "^4.4|^5.0|^6.0", + "symfony/http-kernel": "^4.4|^5.0|^6.0", + "symfony/mime": "^4.4|^5.0|^6.0", + "symfony/property-access": "^5.4|^6.0", + "symfony/property-info": "^5.3.13|^6.0", + "symfony/uid": "^5.3|^6.0", + "symfony/validator": "^4.4|^5.0|^6.0", + "symfony/var-dumper": "^4.4|^5.0|^6.0", + "symfony/var-exporter": "^4.4|^5.0|^6.0", + "symfony/yaml": "^4.4|^5.0|^6.0" + }, + "suggest": { + "psr/cache-implementation": "For using the metadata cache.", + "symfony/config": "For using the XML mapping loader.", + "symfony/mime": "For using a MIME type guesser within the DataUriNormalizer.", + "symfony/property-access": "For using the ObjectNormalizer.", + "symfony/property-info": "To deserialize relations.", + "symfony/var-exporter": "For using the metadata compiler.", + "symfony/yaml": "For using the default YAML mapping loader." + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Serializer\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Handles serializing and deserializing data structures, including object graphs, into array structures or other formats like XML and JSON.", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/serializer/tree/v5.4.12" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-08-26T10:32:10+00:00" } ], "packages-dev": [ @@ -3334,73 +3669,6 @@ ], "time": "2022-04-12T16:02:29+00:00" }, - { - "name": "symfony/deprecation-contracts", - "version": "v2.5.1", - "source": { - "type": "git", - "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "e8b495ea28c1d97b5e0c121748d6f9b53d075c66" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/e8b495ea28c1d97b5e0c121748d6f9b53d075c66", - "reference": "e8b495ea28c1d97b5e0c121748d6f9b53d075c66", - "shasum": "" - }, - "require": { - "php": ">=7.1" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "2.5-dev" - }, - "thanks": { - "name": "symfony/contracts", - "url": "https://github.com/symfony/contracts" - } - }, - "autoload": { - "files": [ - "function.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "A generic function and convention to trigger deprecation notices", - "homepage": "https://symfony.com", - "support": { - "source": "https://github.com/symfony/deprecation-contracts/tree/v2.5.1" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2022-01-02T09:53:40+00:00" - }, { "name": "symfony/event-dispatcher", "version": "v5.4.3", @@ -3761,88 +4029,6 @@ ], "time": "2022-01-02T09:53:40+00:00" }, - { - "name": "symfony/polyfill-ctype", - "version": "v1.25.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "30885182c981ab175d4d034db0f6f469898070ab" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/30885182c981ab175d4d034db0f6f469898070ab", - "reference": "30885182c981ab175d4d034db0f6f469898070ab", - "shasum": "" - }, - "require": { - "php": ">=7.1" - }, - "provide": { - "ext-ctype": "*" - }, - "suggest": { - "ext-ctype": "For best performance" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "1.23-dev" - }, - "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" - } - }, - "autoload": { - "files": [ - "bootstrap.php" - ], - "psr-4": { - "Symfony\\Polyfill\\Ctype\\": "" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Gert de Pagter", - "email": "BackEndTea@gmail.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony polyfill for ctype functions", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "ctype", - "polyfill", - "portable" - ], - "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.25.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2021-10-20T20:35:02+00:00" - }, { "name": "symfony/polyfill-intl-grapheme", "version": "v1.25.0", @@ -4170,89 +4356,6 @@ ], "time": "2021-06-05T21:20:04+00:00" }, - { - "name": "symfony/polyfill-php80", - "version": "v1.25.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-php80.git", - "reference": "4407588e0d3f1f52efb65fbe92babe41f37fe50c" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/4407588e0d3f1f52efb65fbe92babe41f37fe50c", - "reference": "4407588e0d3f1f52efb65fbe92babe41f37fe50c", - "shasum": "" - }, - "require": { - "php": ">=7.1" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "1.23-dev" - }, - "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" - } - }, - "autoload": { - "files": [ - "bootstrap.php" - ], - "psr-4": { - "Symfony\\Polyfill\\Php80\\": "" - }, - "classmap": [ - "Resources/stubs" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Ion Bazan", - "email": "ion.bazan@gmail.com" - }, - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "polyfill", - "portable", - "shim" - ], - "support": { - "source": "https://github.com/symfony/polyfill-php80/tree/v1.25.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2022-03-04T08:16:47+00:00" - }, { "name": "symfony/polyfill-php81", "version": "v1.25.0", @@ -4746,5 +4849,5 @@ "platform-overrides": { "php": "7.4" }, - "plugin-api-version": "2.2.0" + "plugin-api-version": "2.3.0" } diff --git a/lib/BackgroundJob/EmailNotification.php b/lib/BackgroundJob/EmailNotification.php new file mode 100644 index 00000000..5ae43060 --- /dev/null +++ b/lib/BackgroundJob/EmailNotification.php @@ -0,0 +1,68 @@ + + * @author Morris Jobke + * + * @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 + * + */ + +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); + } + } +} diff --git a/lib/Command/ListShares.php b/lib/Command/ListShares.php index a3a5970d..9922db7d 100644 --- a/lib/Command/ListShares.php +++ b/lib/Command/ListShares.php @@ -3,6 +3,7 @@ /** * @copyright Copyright (c) 2018 Roeland Jago Douma * + * @author Florent Poinsaut * @author Roeland Jago Douma * @author John Molakvoæ * @@ -96,21 +97,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $user = $input->getOption('user'); $path = $input->getOption('path'); $token = $input->getOption('token'); - $filter = $input->getOption('filter'); - - if ($filter === 'owner') { - $filter = SharesList::FILTER_OWNER; - } elseif ($filter === 'initiator') { - $filter = SharesList::FILTER_INITIATOR; - } else if ($filter === 'recipient') { - $filter = SharesList::FILTER_RECIPIENT; - } else if ($filter === 'has-expiration') { - $filter = SharesList::FILTER_HAS_EXPIRATION; - } else if ($filter === 'no-expiration') { - $filter = SharesList::FILTER_NO_EXPIRATION; - } else { - $filter = SharesList::FILTER_NONE; - } + $filter = $this->sharesList->filterStringToInt($input->getOption('filter')); if ($user === null && $token === null) { $shares = []; diff --git a/lib/Command/SendShares.php b/lib/Command/SendShares.php new file mode 100644 index 00000000..f9759e9f --- /dev/null +++ b/lib/Command/SendShares.php @@ -0,0 +1,147 @@ + + * + * @author Florent Poinsaut + * @author Roeland Jago Douma + * @author John Molakvoæ + * + * @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 . + * + */ + +namespace OCA\ShareListing\Command; + +use OCA\ShareListing\Service\ReportSender; +use OCA\ShareListing\Service\SharesList; +use OCP\Files\IRootFolder; +use OCP\IUser; +use OCP\IUserManager; +use OCP\Share\IManager as ShareManager; +use OC\Core\Command\Base; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; + +class SendShares extends Base { + + /** @var ShareManager */ + private $shareManager; + + /** @var IUserManager */ + private $userManager; + + /** @var IRootFolder */ + private $rootFolder; + + /** @var ReportSender */ + private $reportSender; + + /** @var SharesList */ + private $sharesList; + + public function __construct( + ShareManager $shareManager, + IUserManager $userManager, + IRootFolder $rootFolder, + ReportSender $reportSender, + SharesList $sharesList + ) { + parent::__construct(); + + $this->shareManager = $shareManager; + $this->userManager = $userManager; + $this->rootFolder = $rootFolder; + $this->reportSender = $reportSender; + $this->sharesList = $sharesList; + } + + public function configure() { + $this->setName('sharing:send') + ->setDescription('Send list who has access to shares by owner') + ->addOption( + 'recipients', + 'r', + InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, + 'Email recipients' + ) + ->addOption( + 'user', + 'u', + InputOption::VALUE_OPTIONAL, + 'Will list shares of the given user' + ) + ->addOption( + 'path', + 'p', + InputOption::VALUE_OPTIONAL, + 'Will only consider the given path' + )->addOption( + 'token', + 't', + InputOption::VALUE_OPTIONAL, + 'Will only consider the given token' + )->addOption( + 'filter', + 'f', + InputOption::VALUE_OPTIONAL, + 'Filter shares, possible values: owner, initiator, recipient, token, has-expiration, no-expiration' + ); + parent::configure(); + } + + protected function execute(InputInterface $input, OutputInterface $output): int { + $this->checkAllRequiredOptionsAreNotEmpty($input); + + $user = $input->getOption('user'); + $path = $input->getOption('path'); + $token = $input->getOption('token'); + $filter = $this->sharesList->filterStringToInt($input->getOption('filter')); + $recipients = $input->getOption('recipients'); + + $this->reportSender->sendReport( + $recipients, + $user, + $filter, + $path, + $token + ); + + return 0; + } + + private function checkAllRequiredOptionsAreNotEmpty(InputInterface $input) + { + $errors = []; + $recipients = $this->getDefinition()->getOption('recipients'); + + /** @var InputOption $recipient */ + foreach ([$recipients] as $recipient) { + $name = $recipient->getName(); + $value = $input->getOption($name); + + if ($value === null || $value === '' || ($recipient->isArray() && empty($value))) { + $errors[] = sprintf('The required option --%s is not set or is empty', $name); + } + } + + if (count($errors)) { + throw new \InvalidArgumentException(implode("\n\n", $errors)); + } + } +} diff --git a/lib/Service/ReportSender.php b/lib/Service/ReportSender.php new file mode 100644 index 00000000..09e7534f --- /dev/null +++ b/lib/Service/ReportSender.php @@ -0,0 +1,140 @@ + + * + * @author Florent Poinsaut + * @author Robin Appelman + * + * @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 . + * + */ + +namespace OCA\ShareListing\Service; + +use iter; +use OCA\ShareListing\Service\SharesList; +use OCP\Defaults; +use OCP\IConfig; +use OCP\IUser; +use OCP\IUserManager; +use OCP\L10N\IFactory; +use OCP\Mail\IMailer; +use OCP\Util; +use Psr\Log\LoggerInterface; +use Symfony\Component\Serializer\Encoder\JsonEncoder; +use Symfony\Component\Serializer\Encoder\CsvEncoder; +use Symfony\Component\Serializer\Serializer; + +class ReportSender { + public const ACTIVITY_LIMIT = 20; + + private $mailer; + private $userManager; + private $defaults; + private $l10nFactory; + private $logger; + + /** @var SharesList */ + private $sharesList; + + public function __construct( + IConfig $config, + IMailer $mailer, + IUserManager $userManager, + Defaults $defaults, + IFactory $l10nFactory, + LoggerInterface $logger, + SharesList $sharesList + ) { + $this->config = $config; + $this->mailer = $mailer; + $this->userManager = $userManager; + $this->defaults = $defaults; + $this->l10nFactory = $l10nFactory; + $this->logger = $logger; + $this->sharesList = $sharesList; + } + + public function sendReport( + array $to, + ?string $userId = '', + int $filter = SharesList::FILTER_NONE, + string $path = null, + string $token = null + ) { + $defaultLanguage = $this->config->getSystemValue('default_language', 'en'); + $userLanguages = $this->config->getUserValue($userId, 'core', 'lang'); + $language = (!empty($userLanguages)) ? $userLanguages : $defaultLanguage; + + $l10n = $this->l10nFactory->get('shareslist', $language); + + $month = (new \DateTimeImmutable())->format('F Y'); + + $template = $this->mailer->createEMailTemplate('shareslist.Notification', [ + 'month' => $month, + ]); + $template->setSubject($l10n->t('Monthly shares reports for %s', $month)); + $template->addHeader(); + + $template->addBodyText('You can find the list of shares reports for the month of ' . $month . ':'); + $template->addBodyListItem('JSON'); + $template->addBodyListItem('CSV'); + + $template->addFooter('', $language); + + if ($userId === null && $token === null) { + $shares = []; + $this->userManager->callForSeenUsers(function (IUser $user) use ($token, $path, $filter, &$shares) { + $tmp = $this->sharesList->getFormattedShares($user->getUID(), $filter, $path, $token); + foreach ($tmp as $share) { + $shares[] = $share; + } + }); + } else { + $shares = iter\toArray($this->sharesList->getFormattedShares($userId, $filter, $path, $token)); + } + + $encoders = [new CsvEncoder(), new JsonEncoder()]; + $serializer = new Serializer([], $encoders); + $json_attachment = $this->mailer->createAttachment( + $serializer->serialize($shares, 'json', ['json_encode_options' => JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE]), + 'report.json', + 'application/json; charset=utf-8' + ); + $csv_attachment = $this->mailer->createAttachment( + $serializer->serialize($shares, 'csv', []), + 'report.csv', + 'text/csv; charset=utf-8' + ); + + $message = $this->mailer->createMessage(); + $message->setTo($to); + $message->useTemplate($template); + $message->setFrom([Util::getDefaultEmailAddress('no-reply') => $this->defaults->getName()]); + $message->attach($json_attachment); + $message->attach($csv_attachment); + + try { + $this->mailer->send($message); + } catch (\Exception $e) { + $this->logger->error($e->getMessage()); + return; + } + } +} diff --git a/lib/Service/SharesList.php b/lib/Service/SharesList.php index d8de0c41..9abaaa37 100644 --- a/lib/Service/SharesList.php +++ b/lib/Service/SharesList.php @@ -3,6 +3,7 @@ /** * @copyright Copyright (c) 2018 Roeland Jago Douma * + * @author Florent Poinsaut * @author Roeland Jago Douma * @author John Molakvoæ * @@ -281,4 +282,29 @@ public function formatShare(IShare $share): array { return $data; } + + public function filterStringToInt(?string $filterString): int { + switch ($filterString) { + case 'owner': + $filter = SharesList::FILTER_OWNER; + break; + case 'initiator': + $filter = SharesList::FILTER_INITIATOR; + break; + case 'recipient': + $filter = SharesList::FILTER_RECIPIENT; + break; + case 'has-expiration': + $filter = SharesList::FILTER_HAS_EXPIRATION; + break; + case 'no-expiration': + $filter = SharesList::FILTER_NO_EXPIRATION; + break; + default: + $filter = SharesList::FILTER_NONE; + break; + } + + return $filter; + } } From 82364da3af6ff629ed2d110ae61465facca40177 Mon Sep 17 00:00:00 2001 From: Florent Poinsaut Date: Wed, 28 Sep 2022 08:55:10 +0000 Subject: [PATCH 04/18] add output option for shareslist command Signed-off-by: Florent Poinsaut --- lib/BackgroundJob/EmailNotification.php | 68 ------------------------- lib/Command/ListShares.php | 11 +++- lib/Command/SendShares.php | 1 - lib/Service/ReportSender.php | 9 +--- lib/Service/SharesList.php | 21 ++++++++ 5 files changed, 32 insertions(+), 78 deletions(-) delete mode 100644 lib/BackgroundJob/EmailNotification.php diff --git a/lib/BackgroundJob/EmailNotification.php b/lib/BackgroundJob/EmailNotification.php deleted file mode 100644 index 5ae43060..00000000 --- a/lib/BackgroundJob/EmailNotification.php +++ /dev/null @@ -1,68 +0,0 @@ - - * @author Morris Jobke - * - * @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 - * - */ - -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); - } - } -} diff --git a/lib/Command/ListShares.php b/lib/Command/ListShares.php index 9922db7d..52ac7dde 100644 --- a/lib/Command/ListShares.php +++ b/lib/Command/ListShares.php @@ -89,8 +89,14 @@ public function configure() { 'f', InputOption::VALUE_OPTIONAL, 'Filter shares, possible values: owner, initiator, recipient, token, has-expiration, no-expiration' + ) + ->addOption( + 'output', + 'o', + InputOption::VALUE_OPTIONAL, + 'Output format (json or csv, default is json)', + 'json' ); - parent::configure(); } protected function execute(InputInterface $input, OutputInterface $output): int { @@ -98,6 +104,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $path = $input->getOption('path'); $token = $input->getOption('token'); $filter = $this->sharesList->filterStringToInt($input->getOption('filter')); + $outputOpt = $input->getOption('output'); if ($user === null && $token === null) { $shares = []; @@ -111,7 +118,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $shares = iter\toArray($this->sharesList->getFormattedShares($user, $filter, $path, $token)); } - $output->writeln(json_encode($shares, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)); + $output->writeln($this->sharesList->getSerializedShares($shares, $outputOpt)); return 0; } } diff --git a/lib/Command/SendShares.php b/lib/Command/SendShares.php index f9759e9f..498bb2d7 100644 --- a/lib/Command/SendShares.php +++ b/lib/Command/SendShares.php @@ -102,7 +102,6 @@ public function configure() { InputOption::VALUE_OPTIONAL, 'Filter shares, possible values: owner, initiator, recipient, token, has-expiration, no-expiration' ); - parent::configure(); } protected function execute(InputInterface $input, OutputInterface $output): int { diff --git a/lib/Service/ReportSender.php b/lib/Service/ReportSender.php index 09e7534f..b445307a 100644 --- a/lib/Service/ReportSender.php +++ b/lib/Service/ReportSender.php @@ -37,9 +37,6 @@ use OCP\Mail\IMailer; use OCP\Util; use Psr\Log\LoggerInterface; -use Symfony\Component\Serializer\Encoder\JsonEncoder; -use Symfony\Component\Serializer\Encoder\CsvEncoder; -use Symfony\Component\Serializer\Serializer; class ReportSender { public const ACTIVITY_LIMIT = 20; @@ -110,15 +107,13 @@ public function sendReport( $shares = iter\toArray($this->sharesList->getFormattedShares($userId, $filter, $path, $token)); } - $encoders = [new CsvEncoder(), new JsonEncoder()]; - $serializer = new Serializer([], $encoders); $json_attachment = $this->mailer->createAttachment( - $serializer->serialize($shares, 'json', ['json_encode_options' => JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE]), + $this->sharesList->getSerializedShares($shares, 'json'), 'report.json', 'application/json; charset=utf-8' ); $csv_attachment = $this->mailer->createAttachment( - $serializer->serialize($shares, 'csv', []), + $this->sharesList->getSerializedShares($shares, 'csv'), 'report.csv', 'text/csv; charset=utf-8' ); diff --git a/lib/Service/SharesList.php b/lib/Service/SharesList.php index 9abaaa37..d7f84123 100644 --- a/lib/Service/SharesList.php +++ b/lib/Service/SharesList.php @@ -35,6 +35,9 @@ use OCP\Share; use OCP\Share\IManager as ShareManager; use OCP\Share\IShare; +use Symfony\Component\Serializer\Encoder\JsonEncoder; +use Symfony\Component\Serializer\Encoder\CsvEncoder; +use Symfony\Component\Serializer\Serializer; class SharesList { @@ -307,4 +310,22 @@ public function filterStringToInt(?string $filterString): int { return $filter; } + + public function getSerializedShares(array $shares, ?string $format = 'json'): string + { + switch ($format) { + case 'csv': + $encoders = [new CsvEncoder()]; + $context = []; + break; + default: + $encoders = [new JsonEncoder()]; + $format = 'json'; + $context = ['json_encode_options' => JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE]; + break; + } + + $serializer = new Serializer([], $encoders); + return $serializer->serialize($shares, $format, $context); + } } From 534146d0dcdb465dd2a44411e246281ba505d412 Mon Sep 17 00:00:00 2001 From: Florent Poinsaut Date: Fri, 30 Sep 2022 13:45:13 +0000 Subject: [PATCH 05/18] Update README Signed-off-by: Florent Poinsaut --- README.md | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index c958e7aa..a0539781 100644 --- a/README.md +++ b/README.md @@ -2,18 +2,18 @@ This app allows generating reports of shares on the system. - ## Usage -#### Command: -``` -./occ sharing:list [-u|--user [USER]] [-p|--path [PATH]] [-t|--token [TOKEN]] [-f|--filter [FILTER]] +### Command + +```sh +./occ sharing:list [-u|--user [USER]] [-p|--path [PATH]] [-t|--token [TOKEN]] [-f|--filter [FILTER]] [-o|--output FORMAT] ``` Without options, the command yields the unfiltered list of all shares.\ With options, the list is narrowed down using the filters set. -#### Options: +### Options * `-u [USER]` or `--user [USER]`\ List only shares of the given user. @@ -24,13 +24,14 @@ With options, the list is narrowed down using the filters set. * `-f [FILTER]` or `--filter [FILTER]`\ List only shares where the TYPE matches the argument.\ Possible values for the filter argument: {owner, initiator, recipient} - +* `-o FORMAT` or `--output FORMAT`\ + Set the output format (json or csv, default is json). ## Examples To better illustrate how the app work see the examples below: -### Example 1 +### Example 1 Listing all shares user0 is a participant in (be it owner, initiator or recipient): @@ -115,7 +116,8 @@ Listing all shares user0 is a participant in (be it owner, initiator or recipien ] ``` -### Example 3: +### Example 3 + List all info about all shares `./occ sharing:list` From 6f37d4f43bec11ed83033623a9ceeaa9db32750f Mon Sep 17 00:00:00 2001 From: Florent Poinsaut Date: Tue, 11 Oct 2022 12:40:52 +0000 Subject: [PATCH 06/18] Fix getAllShares Signed-off-by: Florent Poinsaut --- lib/Command/ListShares.php | 12 +----------- lib/Service/SharesList.php | 31 ++++++++++++++++++------------- 2 files changed, 19 insertions(+), 24 deletions(-) diff --git a/lib/Command/ListShares.php b/lib/Command/ListShares.php index 52ac7dde..d2407c5f 100644 --- a/lib/Command/ListShares.php +++ b/lib/Command/ListShares.php @@ -106,17 +106,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $filter = $this->sharesList->filterStringToInt($input->getOption('filter')); $outputOpt = $input->getOption('output'); - if ($user === null && $token === null) { - $shares = []; - $this->userManager->callForSeenUsers(function (IUser $user) use ($token, $path, $filter, &$shares) { - $tmp = $this->sharesList->getFormattedShares($user->getUID(), $filter, $path, $token); - foreach ($tmp as $share) { - $shares[] = $share; - } - }); - } else { - $shares = iter\toArray($this->sharesList->getFormattedShares($user, $filter, $path, $token)); - } + $shares = iter\toArray($this->sharesList->getFormattedShares($user, $filter, $path, $token)); $output->writeln($this->sharesList->getSerializedShares($shares, $outputOpt)); return 0; diff --git a/lib/Service/SharesList.php b/lib/Service/SharesList.php index d7f84123..aa0e13ce 100644 --- a/lib/Service/SharesList.php +++ b/lib/Service/SharesList.php @@ -76,7 +76,7 @@ private function getShareTypes(): array { ]; } - public function get(string $userId, int $filter, string $path = null, string $token = null): \Iterator { + public function get(?string $userId, int $filter, string $path = null, string $token = null): \Iterator { $shares = $this->getShares($userId); // If path is set. Filter for the current user @@ -208,7 +208,7 @@ public function getSub(string $userId, int $filter, string $path): \Iterator { return $shares; } - public function getFormattedShares(string $userId = '', int $filter = self::FILTER_NONE, string $path = null, string $token = null): \Iterator { + public function getFormattedShares(?string $userId = '', int $filter = self::FILTER_NONE, string $path = null, string $token = null): \Iterator { $shares = $this->get($userId, $filter, $path, $token); $formattedShares = iter\map(function (IShare $share): array { @@ -218,23 +218,28 @@ public function getFormattedShares(string $userId = '', int $filter = self::FILT return $formattedShares; } - private function getShares(string $userId): \Iterator { - $shareTypes = $this->getShareTypes(); + private function getShares(?string $userId): \Iterator { + if (empty($userId)) { + $shares = $this->shareManager->getAllShares(); + } else { + $shareTypes = $this->getShareTypes(); - foreach ($shareTypes as $shareType) { - $shares = $this->shareManager->getSharesBy($userId, $shareType, null, true, -1, 0); + foreach ($shareTypes as $shareType) { + $shares = $this->shareManager->getSharesBy($userId, $shareType, null, true, -1, 0); - foreach ($shares as $share) { - yield $share; - } + if ($shareType !== \OCP\Share\IShare::TYPE_LINK) { + foreach ($shares as $share) { + yield $share; + } - if ($shareType !== \OCP\Share\IShare::TYPE_LINK) { - $shares = $this->shareManager->getSharedWith($userId, $shareType, null, -1, 0); - foreach ($shares as $share) { - yield $share; + $shares = $this->shareManager->getSharedWith($userId, $shareType, null, -1, 0); } } } + + foreach ($shares as $share) { + yield $share; + } } public function formatShare(IShare $share): array { From 449ed3442776319e87c483262d138224c1f3ba42 Mon Sep 17 00:00:00 2001 From: Florent Poinsaut Date: Tue, 11 Oct 2022 12:51:04 +0000 Subject: [PATCH 07/18] use new getAllShares method Signed-off-by: Florent Poinsaut --- lib/Service/ReportSender.php | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/lib/Service/ReportSender.php b/lib/Service/ReportSender.php index b445307a..75f28da0 100644 --- a/lib/Service/ReportSender.php +++ b/lib/Service/ReportSender.php @@ -95,17 +95,7 @@ public function sendReport( $template->addFooter('', $language); - if ($userId === null && $token === null) { - $shares = []; - $this->userManager->callForSeenUsers(function (IUser $user) use ($token, $path, $filter, &$shares) { - $tmp = $this->sharesList->getFormattedShares($user->getUID(), $filter, $path, $token); - foreach ($tmp as $share) { - $shares[] = $share; - } - }); - } else { - $shares = iter\toArray($this->sharesList->getFormattedShares($userId, $filter, $path, $token)); - } + $shares = iter\toArray($this->sharesList->getFormattedShares($userId, $filter, $path, $token)); $json_attachment = $this->mailer->createAttachment( $this->sharesList->getSerializedShares($shares, 'json'), From 1d2b505c7c481856e22fdccc313407e699ed0a6d Mon Sep 17 00:00:00 2001 From: Carl Schwan Date: Thu, 8 Sep 2022 11:11:39 +0200 Subject: [PATCH 08/18] Show share expiration if it exits Signed-off-by: Carl Schwan --- lib/Service/SharesList.php | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/lib/Service/SharesList.php b/lib/Service/SharesList.php index aa0e13ce..d349a9ea 100644 --- a/lib/Service/SharesList.php +++ b/lib/Service/SharesList.php @@ -119,18 +119,6 @@ public function get(?string $userId, int $filter, string $path = null, string $t }, $shares); } - if ($filter === self::FILTER_HAS_EXPIRATION) { - $shares = iter\filter(function (IShare $share) use ($userId): bool { - return $share->getExpirationDate() !== null; - }, $shares); - } - - if ($filter === self::FILTER_NO_EXPIRATION) { - $shares = iter\filter(function (IShare $share) use ($userId): bool { - return $share->getExpirationDate() === null; - }, $shares); - } - $shares = iter\filter(function (IShare $share): bool { try { $userFolder = $this->rootFolder->getUserFolder($share->getShareOwner()); From 3ba68c1e47521122137bdf120fc07461fead00e9 Mon Sep 17 00:00:00 2001 From: Carl Schwan Date: Thu, 8 Sep 2022 12:33:34 +0200 Subject: [PATCH 09/18] Add filtering Signed-off-by: Carl Schwan --- lib/Command/ListShares.php | 24 +++++++++++++++--------- lib/Service/SharesList.php | 14 +++++++++++++- 2 files changed, 28 insertions(+), 10 deletions(-) diff --git a/lib/Command/ListShares.php b/lib/Command/ListShares.php index d2407c5f..2ede63ed 100644 --- a/lib/Command/ListShares.php +++ b/lib/Command/ListShares.php @@ -89,13 +89,6 @@ public function configure() { 'f', InputOption::VALUE_OPTIONAL, 'Filter shares, possible values: owner, initiator, recipient, token, has-expiration, no-expiration' - ) - ->addOption( - 'output', - 'o', - InputOption::VALUE_OPTIONAL, - 'Output format (json or csv, default is json)', - 'json' ); } @@ -103,8 +96,21 @@ protected function execute(InputInterface $input, OutputInterface $output): int $user = $input->getOption('user'); $path = $input->getOption('path'); $token = $input->getOption('token'); - $filter = $this->sharesList->filterStringToInt($input->getOption('filter')); - $outputOpt = $input->getOption('output'); + $filter = $input->getOption('filter'); + + if ($filter === 'owner') { + $filter = SharesList::FILTER_OWNER; + } elseif ($filter === 'initiator') { + $filter = SharesList::FILTER_INITIATOR; + } else if ($filter === 'recipient') { + $filter = SharesList::FILTER_RECIPIENT; + } else if ($filter === 'has-expiration') { + $filter = SharesList::FILTER_HAS_EXPIRATION; + } else if ($filter === 'no-expiration') { + $filter = SharesList::FILTER_NO_EXPIRATION; + } else { + $filter = SharesList::FILTER_NONE; + } $shares = iter\toArray($this->sharesList->getFormattedShares($user, $filter, $path, $token)); diff --git a/lib/Service/SharesList.php b/lib/Service/SharesList.php index d349a9ea..3af71f08 100644 --- a/lib/Service/SharesList.php +++ b/lib/Service/SharesList.php @@ -119,6 +119,18 @@ public function get(?string $userId, int $filter, string $path = null, string $t }, $shares); } + if ($filter === self::FILTER_HAS_EXPIRATION) { + $shares = iter\filter(function (IShare $share) use ($userId): bool { + return $share->getExpirationDate() !== null; + }, $shares); + } + + if ($filter === self::FILTER_NO_EXPIRATION) { + $shares = iter\filter(function (IShare $share) use ($userId): bool { + return $share->getExpirationDate() === null; + }, $shares); + } + $shares = iter\filter(function (IShare $share): bool { try { $userFolder = $this->rootFolder->getUserFolder($share->getShareOwner()); @@ -196,7 +208,7 @@ public function getSub(string $userId, int $filter, string $path): \Iterator { return $shares; } - public function getFormattedShares(?string $userId = '', int $filter = self::FILTER_NONE, string $path = null, string $token = null): \Iterator { + public function getFormattedShares(string $userId = '', int $filter = self::FILTER_NONE, string $path = null, string $token = null): \Iterator { $shares = $this->get($userId, $filter, $path, $token); $formattedShares = iter\map(function (IShare $share): array { From c6fcad80dd5c211a3241abb087fafd23ba8f9574 Mon Sep 17 00:00:00 2001 From: Florent Poinsaut Date: Tue, 11 Oct 2022 15:48:27 +0000 Subject: [PATCH 10/18] Write report on Nextcloud instead of send by email Signed-off-by: Florent Poinsaut --- lib/Command/SendShares.php | 50 ++++++++++++---- lib/Service/ReportSender.php | 111 ++++++++++++++++++++++++++--------- 2 files changed, 120 insertions(+), 41 deletions(-) diff --git a/lib/Command/SendShares.php b/lib/Command/SendShares.php index 498bb2d7..77275c87 100644 --- a/lib/Command/SendShares.php +++ b/lib/Command/SendShares.php @@ -78,7 +78,13 @@ public function configure() { 'recipients', 'r', InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, - 'Email recipients' + 'Recipients users of generated reports' + ) + ->addOption( + 'target-path', + 'x', + InputOption::VALUE_REQUIRED, + 'Generated reports will be stored on this path' ) ->addOption( 'user', @@ -112,31 +118,51 @@ protected function execute(InputInterface $input, OutputInterface $output): int $token = $input->getOption('token'); $filter = $this->sharesList->filterStringToInt($input->getOption('filter')); $recipients = $input->getOption('recipients'); + $targetPath = $input->getOption('target-path'); + + $dateTime = new \DateTimeImmutable(); + + foreach ($recipients as $recipient) { + $reports = $this->reportSender->createReport( + $recipient, + $targetPath, + $dateTime, + $user, + $filter, + $path, + $token + ); - $this->reportSender->sendReport( - $recipients, - $user, - $filter, - $path, - $token - ); + $this->reportSender->sendReport($recipient, $dateTime, $reports); + } return 0; } - private function checkAllRequiredOptionsAreNotEmpty(InputInterface $input) + protected function checkAllRequiredOptionsAreNotEmpty(InputInterface $input) { $errors = []; + + if (!$input->getOption('target-path')) { + $errors[] = 'The required option --target-path is not set or is empty.'; + } + $recipients = $this->getDefinition()->getOption('recipients'); /** @var InputOption $recipient */ foreach ([$recipients] as $recipient) { $name = $recipient->getName(); - $value = $input->getOption($name); + $values = $input->getOption($name); - if ($value === null || $value === '' || ($recipient->isArray() && empty($value))) { - $errors[] = sprintf('The required option --%s is not set or is empty', $name); + if ($values === null || $values === '' || ($recipient->isArray() && empty($values))) { + $errors[] = sprintf('The required option --%s is not set or is empty.', $name); } + + foreach ($values as $value) { + if (!$this->userManager->userExists($value)) { + $errors[] = sprintf('The recipient user %s does not exist.', $value); + } + } } if (count($errors)) { diff --git a/lib/Service/ReportSender.php b/lib/Service/ReportSender.php index 75f28da0..ee6e3367 100644 --- a/lib/Service/ReportSender.php +++ b/lib/Service/ReportSender.php @@ -30,8 +30,10 @@ use iter; use OCA\ShareListing\Service\SharesList; use OCP\Defaults; +use OCP\Files\FileInfo; +use OCP\Files\IRootFolder; use OCP\IConfig; -use OCP\IUser; +use OCP\IURLGenerator; use OCP\IUserManager; use OCP\L10N\IFactory; use OCP\Mail\IMailer; @@ -49,6 +51,10 @@ class ReportSender { /** @var SharesList */ private $sharesList; + /** @var IRootFolder */ + private $root; + /** @var IURLGenerator */ + protected $url; public function __construct( IConfig $config, @@ -57,7 +63,9 @@ public function __construct( Defaults $defaults, IFactory $l10nFactory, LoggerInterface $logger, - SharesList $sharesList + SharesList $sharesList, + IRootFolder $root, + IURLGenerator $url ) { $this->config = $config; $this->mailer = $mailer; @@ -66,54 +74,77 @@ public function __construct( $this->l10nFactory = $l10nFactory; $this->logger = $logger; $this->sharesList = $sharesList; + $this->root = $root; + $this->url = $url; } - public function sendReport( - array $to, + public function createReport( + string $recipient, + string $targetPath, + \DateTimeImmutable $dateTime, ?string $userId = '', int $filter = SharesList::FILTER_NONE, string $path = null, string $token = null ) { + $userFolder = $this->root->getUserFolder($recipient); + + if ($userFolder->nodeExists($targetPath)) { + $folder = $userFolder->get($targetPath); + if ($folder->getType() !== FileInfo::TYPE_FOLDER) { + return ['error' => 'Target path ' . $targetPath . ' is not a folder']; + } + } else { + $folder = $userFolder->newFolder($targetPath); + } + + $shares = iter\toArray($this->sharesList->getFormattedShares($userId, $filter, $path, $token)); + + $formatedDateTime = $dateTime->format('YmdHi'); + $reports = []; + foreach (['json', 'csv'] as $format) { + $fileName=$formatedDateTime.' - Shares report.'.$format; + $savedFile = $folder->newFile($fileName); + $savedFile->putContent($this->sharesList->getSerializedShares($shares, $format)); + $reports[] = [ + 'name' => $savedFile->getName(), + 'url' => $this->url->linkToRouteAbsolute('files.viewcontroller.showFile', ['fileid' => $savedFile->getId()]) + ]; + } + + return $reports; + } + + public function sendReport(string $recipient, \DateTimeImmutable $dateTime, array $reports) { $defaultLanguage = $this->config->getSystemValue('default_language', 'en'); - $userLanguages = $this->config->getUserValue($userId, 'core', 'lang'); + $userLanguages = $this->config->getUserValue($recipient, 'core', 'lang'); $language = (!empty($userLanguages)) ? $userLanguages : $defaultLanguage; $l10n = $this->l10nFactory->get('shareslist', $language); - $month = (new \DateTimeImmutable())->format('F Y'); - $template = $this->mailer->createEMailTemplate('shareslist.Notification', [ - 'month' => $month, + 'date-time' => $dateTime, ]); - $template->setSubject($l10n->t('Monthly shares reports for %s', $month)); - $template->addHeader(); - $template->addBodyText('You can find the list of shares reports for the month of ' . $month . ':'); - $template->addBodyListItem('JSON'); - $template->addBodyListItem('CSV'); + $formatedDateTime = $dateTime->format(\DateTimeInterface::COOKIE); + $template->setSubject($l10n->t('Shares reports generated on %s', $formatedDateTime)); + $template->addHeader(); + $template->addBodyText('You can find the list of shares reports generated on ' . $formatedDateTime . ':'); + + foreach ($reports as $report) { + $template->addBodyListItem( + ''.$report['name'].'', + '', + '', + $report['name'].': '.$report['url']); + } $template->addFooter('', $language); - $shares = iter\toArray($this->sharesList->getFormattedShares($userId, $filter, $path, $token)); - - $json_attachment = $this->mailer->createAttachment( - $this->sharesList->getSerializedShares($shares, 'json'), - 'report.json', - 'application/json; charset=utf-8' - ); - $csv_attachment = $this->mailer->createAttachment( - $this->sharesList->getSerializedShares($shares, 'csv'), - 'report.csv', - 'text/csv; charset=utf-8' - ); - $message = $this->mailer->createMessage(); - $message->setTo($to); + $message->setTo([$this->getEmailAdressFromUserId($recipient)]); $message->useTemplate($template); $message->setFrom([Util::getDefaultEmailAddress('no-reply') => $this->defaults->getName()]); - $message->attach($json_attachment); - $message->attach($csv_attachment); try { $this->mailer->send($message); @@ -122,4 +153,26 @@ public function sendReport( return; } } + + protected function getEmailAdressFromUserId(string $userId): ?string { + $user = $this->userManager->get($userId); + if ($user === null) { + $this->logger->warning( + 'ShareList error, the user "' . $userId . '" does not exist.', + ['app' => $this->appName] + ); + return null; + } + + $email = $user->getEMailAddress(); + if ($email === null || $email === '') { + $this->logger->warning( + 'ShareList error, the user "' . $userId . '" does not have an email set up.', + ['app' => $this->appName] + ); + return null; + } + + return $email; + } } From 5cb6deb7ed072623dc4d4b56f336914186f06e42 Mon Sep 17 00:00:00 2001 From: Florent Poinsaut Date: Wed, 12 Oct 2022 07:49:49 +0000 Subject: [PATCH 11/18] Refacto + class var fixes Signed-off-by: Florent Poinsaut --- lib/Command/SendShares.php | 14 -------------- lib/Service/ReportSender.php | 8 ++++++++ 2 files changed, 8 insertions(+), 14 deletions(-) diff --git a/lib/Command/SendShares.php b/lib/Command/SendShares.php index 77275c87..59ac938d 100644 --- a/lib/Command/SendShares.php +++ b/lib/Command/SendShares.php @@ -29,26 +29,16 @@ use OCA\ShareListing\Service\ReportSender; use OCA\ShareListing\Service\SharesList; -use OCP\Files\IRootFolder; -use OCP\IUser; use OCP\IUserManager; -use OCP\Share\IManager as ShareManager; use OC\Core\Command\Base; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; class SendShares extends Base { - - /** @var ShareManager */ - private $shareManager; - /** @var IUserManager */ private $userManager; - /** @var IRootFolder */ - private $rootFolder; - /** @var ReportSender */ private $reportSender; @@ -56,17 +46,13 @@ class SendShares extends Base { private $sharesList; public function __construct( - ShareManager $shareManager, IUserManager $userManager, - IRootFolder $rootFolder, ReportSender $reportSender, SharesList $sharesList ) { parent::__construct(); - $this->shareManager = $shareManager; $this->userManager = $userManager; - $this->rootFolder = $rootFolder; $this->reportSender = $reportSender; $this->sharesList = $sharesList; } diff --git a/lib/Service/ReportSender.php b/lib/Service/ReportSender.php index ee6e3367..78c26c53 100644 --- a/lib/Service/ReportSender.php +++ b/lib/Service/ReportSender.php @@ -43,6 +43,11 @@ class ReportSender { public const ACTIVITY_LIMIT = 20; + /** @var string */ + private $appName; + /** @var Iconfig */ + private $config; + private $mailer; private $userManager; private $defaults; @@ -57,6 +62,7 @@ class ReportSender { protected $url; public function __construct( + string $appName, IConfig $config, IMailer $mailer, IUserManager $userManager, @@ -67,6 +73,7 @@ public function __construct( IRootFolder $root, IURLGenerator $url ) { + $this->appName = $appName; $this->config = $config; $this->mailer = $mailer; $this->userManager = $userManager; @@ -90,6 +97,7 @@ public function createReport( $userFolder = $this->root->getUserFolder($recipient); if ($userFolder->nodeExists($targetPath)) { + /** @var Folder $folder */ $folder = $userFolder->get($targetPath); if ($folder->getType() !== FileInfo::TYPE_FOLDER) { return ['error' => 'Target path ' . $targetPath . ' is not a folder']; From f7769980272f920848f25fb82f7edde0a9de2dc2 Mon Sep 17 00:00:00 2001 From: Florent Poinsaut Date: Tue, 11 Oct 2022 11:27:13 +0000 Subject: [PATCH 12/18] Add CSV output format Signed-off-by: Florent Poinsaut --- lib/Command/ListShares.php | 24 +++++++++--------------- 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/lib/Command/ListShares.php b/lib/Command/ListShares.php index 2ede63ed..d2407c5f 100644 --- a/lib/Command/ListShares.php +++ b/lib/Command/ListShares.php @@ -89,6 +89,13 @@ public function configure() { 'f', InputOption::VALUE_OPTIONAL, 'Filter shares, possible values: owner, initiator, recipient, token, has-expiration, no-expiration' + ) + ->addOption( + 'output', + 'o', + InputOption::VALUE_OPTIONAL, + 'Output format (json or csv, default is json)', + 'json' ); } @@ -96,21 +103,8 @@ protected function execute(InputInterface $input, OutputInterface $output): int $user = $input->getOption('user'); $path = $input->getOption('path'); $token = $input->getOption('token'); - $filter = $input->getOption('filter'); - - if ($filter === 'owner') { - $filter = SharesList::FILTER_OWNER; - } elseif ($filter === 'initiator') { - $filter = SharesList::FILTER_INITIATOR; - } else if ($filter === 'recipient') { - $filter = SharesList::FILTER_RECIPIENT; - } else if ($filter === 'has-expiration') { - $filter = SharesList::FILTER_HAS_EXPIRATION; - } else if ($filter === 'no-expiration') { - $filter = SharesList::FILTER_NO_EXPIRATION; - } else { - $filter = SharesList::FILTER_NONE; - } + $filter = $this->sharesList->filterStringToInt($input->getOption('filter')); + $outputOpt = $input->getOption('output'); $shares = iter\toArray($this->sharesList->getFormattedShares($user, $filter, $path, $token)); From cc0cf1bbe3c02ec1d682174dd2bdf2f1ed28ded0 Mon Sep 17 00:00:00 2001 From: Florent Poinsaut Date: Tue, 11 Oct 2022 12:40:52 +0000 Subject: [PATCH 13/18] Fix getAllShares Signed-off-by: Florent Poinsaut --- lib/Service/SharesList.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Service/SharesList.php b/lib/Service/SharesList.php index 3af71f08..aa0e13ce 100644 --- a/lib/Service/SharesList.php +++ b/lib/Service/SharesList.php @@ -208,7 +208,7 @@ public function getSub(string $userId, int $filter, string $path): \Iterator { return $shares; } - public function getFormattedShares(string $userId = '', int $filter = self::FILTER_NONE, string $path = null, string $token = null): \Iterator { + public function getFormattedShares(?string $userId = '', int $filter = self::FILTER_NONE, string $path = null, string $token = null): \Iterator { $shares = $this->get($userId, $filter, $path, $token); $formattedShares = iter\map(function (IShare $share): array { From 95670ccad3a3e28f2f6817f0e4d72a8b70a59dcf Mon Sep 17 00:00:00 2001 From: Florent Poinsaut Date: Wed, 12 Oct 2022 09:30:10 +0000 Subject: [PATCH 14/18] Add an Abstract Command class Signed-off-by: Florent Poinsaut --- lib/Command/AbstractCommand.php | 78 +++++++++++++++++++++++++++++++++ lib/Command/ListShares.php | 69 +++++------------------------ lib/Command/SendShares.php | 38 +++------------- 3 files changed, 93 insertions(+), 92 deletions(-) create mode 100644 lib/Command/AbstractCommand.php diff --git a/lib/Command/AbstractCommand.php b/lib/Command/AbstractCommand.php new file mode 100644 index 00000000..7a3bf562 --- /dev/null +++ b/lib/Command/AbstractCommand.php @@ -0,0 +1,78 @@ + + * + * @author Florent Poinsaut + * @author Roeland Jago Douma + * @author John Molakvoæ + * + * @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 . + * + */ + +namespace OCA\ShareListing\Command; + +use OCA\ShareListing\Service\SharesList; +use OC\Core\Command\Base; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; + +abstract class AbstractCommand extends Base { + /** @var SharesList */ + protected $sharesList; + + public function __construct(SharesList $sharesList) { + parent::__construct(); + + $this->sharesList = $sharesList; + } + + public function configure() { + $this->addOption( + 'user', + 'u', + InputOption::VALUE_OPTIONAL, + 'Will list shares of the given user' + ) + ->addOption( + 'path', + 'p', + InputOption::VALUE_OPTIONAL, + 'Will only consider the given path' + )->addOption( + 'token', + 't', + InputOption::VALUE_OPTIONAL, + 'Will only consider the given token' + )->addOption( + 'filter', + 'f', + InputOption::VALUE_OPTIONAL, + 'Filter shares, possible values: owner, initiator, recipient, token, has-expiration, no-expiration' + ); + } + + protected function getOptions(InputInterface $input): array { + $user = $input->getOption('user'); + $path = $input->getOption('path'); + $token = $input->getOption('token'); + $filter = $this->sharesList->filterStringToInt($input->getOption('filter')); + + return [$user, $path, $token, $filter]; + } +} diff --git a/lib/Command/ListShares.php b/lib/Command/ListShares.php index d2407c5f..eb9683c9 100644 --- a/lib/Command/ListShares.php +++ b/lib/Command/ListShares.php @@ -27,69 +27,23 @@ namespace OCA\ShareListing\Command; use iter; -use OCA\ShareListing\Service\SharesList; -use OCP\Files\IRootFolder; -use OCP\IUser; -use OCP\IUserManager; -use OCP\Share\IManager as ShareManager; -use OC\Core\Command\Base; -use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; -class ListShares extends Base { - - /** @var ShareManager */ - private $shareManager; - - /** @var IUserManager */ - private $userManager; - - /** @var IRootFolder */ - private $rootFolder; - - /** @var SharesList */ - private $sharesList; - - public function __construct(ShareManager $shareManager, - IUserManager $userManager, - IRootFolder $rootFolder, - SharesList $sharesList) { - parent::__construct(); - - $this->shareManager = $shareManager; - $this->userManager = $userManager; - $this->rootFolder = $rootFolder; - $this->sharesList = $sharesList; - - } - +class ListShares extends AbstractCommand { public function configure() { + parent::configure(); + $this->setName('sharing:list') ->setDescription('List who has access to shares by owner') ->addOption( - 'user', - 'u', - InputOption::VALUE_OPTIONAL, - 'Will list shares of the given user' - ) - ->addOption( - 'path', - 'p', - InputOption::VALUE_OPTIONAL, - 'Will only consider the given path' - )->addOption( - 'token', - 't', - InputOption::VALUE_OPTIONAL, - 'Will only consider the given token' - )->addOption( - 'filter', - 'f', + 'output', + 'o', InputOption::VALUE_OPTIONAL, - 'Filter shares, possible values: owner, initiator, recipient, token, has-expiration, no-expiration' - ) + 'Output format (json or csv, default is json)', + 'json' + ) ->addOption( 'output', 'o', @@ -98,12 +52,9 @@ public function configure() { 'json' ); } - + protected function execute(InputInterface $input, OutputInterface $output): int { - $user = $input->getOption('user'); - $path = $input->getOption('path'); - $token = $input->getOption('token'); - $filter = $this->sharesList->filterStringToInt($input->getOption('filter')); + [$user, $path, $token, $filter] = $this->getOptions($input); $outputOpt = $input->getOption('output'); $shares = iter\toArray($this->sharesList->getFormattedShares($user, $filter, $path, $token)); diff --git a/lib/Command/SendShares.php b/lib/Command/SendShares.php index 59ac938d..89c6520f 100644 --- a/lib/Command/SendShares.php +++ b/lib/Command/SendShares.php @@ -30,34 +30,31 @@ use OCA\ShareListing\Service\ReportSender; use OCA\ShareListing\Service\SharesList; use OCP\IUserManager; -use OC\Core\Command\Base; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; -class SendShares extends Base { +class SendShares extends AbstractCommand { /** @var IUserManager */ private $userManager; /** @var ReportSender */ private $reportSender; - /** @var SharesList */ - private $sharesList; - public function __construct( IUserManager $userManager, ReportSender $reportSender, SharesList $sharesList ) { - parent::__construct(); + parent::__construct($sharesList); $this->userManager = $userManager; $this->reportSender = $reportSender; - $this->sharesList = $sharesList; } public function configure() { + parent::configure(); + $this->setName('sharing:send') ->setDescription('Send list who has access to shares by owner') ->addOption( @@ -71,38 +68,13 @@ public function configure() { 'x', InputOption::VALUE_REQUIRED, 'Generated reports will be stored on this path' - ) - ->addOption( - 'user', - 'u', - InputOption::VALUE_OPTIONAL, - 'Will list shares of the given user' - ) - ->addOption( - 'path', - 'p', - InputOption::VALUE_OPTIONAL, - 'Will only consider the given path' - )->addOption( - 'token', - 't', - InputOption::VALUE_OPTIONAL, - 'Will only consider the given token' - )->addOption( - 'filter', - 'f', - InputOption::VALUE_OPTIONAL, - 'Filter shares, possible values: owner, initiator, recipient, token, has-expiration, no-expiration' ); } protected function execute(InputInterface $input, OutputInterface $output): int { $this->checkAllRequiredOptionsAreNotEmpty($input); - $user = $input->getOption('user'); - $path = $input->getOption('path'); - $token = $input->getOption('token'); - $filter = $this->sharesList->filterStringToInt($input->getOption('filter')); + [$user, $path, $token, $filter] = $this->getOptions($input); $recipients = $input->getOption('recipients'); $targetPath = $input->getOption('target-path'); From a5ba7a28ee0618b2de829847f2703ccda66ee005 Mon Sep 17 00:00:00 2001 From: Florent Poinsaut Date: Thu, 13 Oct 2022 16:11:51 +0000 Subject: [PATCH 15/18] Fix bad files id with multiples recipients Signed-off-by: Florent Poinsaut --- lib/Command/SendShares.php | 4 ++-- lib/Service/ReportSender.php | 46 +++++++++++++++++++++--------------- 2 files changed, 29 insertions(+), 21 deletions(-) diff --git a/lib/Command/SendShares.php b/lib/Command/SendShares.php index 89c6520f..b270c8de 100644 --- a/lib/Command/SendShares.php +++ b/lib/Command/SendShares.php @@ -81,7 +81,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $dateTime = new \DateTimeImmutable(); foreach ($recipients as $recipient) { - $reports = $this->reportSender->createReport( + $this->reportSender->createReport( $recipient, $targetPath, $dateTime, @@ -91,7 +91,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $token ); - $this->reportSender->sendReport($recipient, $dateTime, $reports); + $this->reportSender->sendReport($recipient, $dateTime); } return 0; diff --git a/lib/Service/ReportSender.php b/lib/Service/ReportSender.php index 78c26c53..dcd16d26 100644 --- a/lib/Service/ReportSender.php +++ b/lib/Service/ReportSender.php @@ -31,6 +31,7 @@ use OCA\ShareListing\Service\SharesList; use OCP\Defaults; use OCP\Files\FileInfo; +use OCP\Files\Folder; use OCP\Files\IRootFolder; use OCP\IConfig; use OCP\IURLGenerator; @@ -54,6 +55,8 @@ class ReportSender { private $l10nFactory; private $logger; + /** @var array */ + protected $reports = []; /** @var SharesList */ private $sharesList; /** @var IRootFolder */ @@ -94,36 +97,40 @@ public function createReport( string $path = null, string $token = null ) { - $userFolder = $this->root->getUserFolder($recipient); + $userFolder = $this->root->getUserFolder($recipient); if ($userFolder->nodeExists($targetPath)) { /** @var Folder $folder */ $folder = $userFolder->get($targetPath); if ($folder->getType() !== FileInfo::TYPE_FOLDER) { - return ['error' => 'Target path ' . $targetPath . ' is not a folder']; + $this->logger->warning( + 'Target path ' . $targetPath . ' is not a folder', + ['app' => $this->appName] + ); } } else { $folder = $userFolder->newFolder($targetPath); } - $shares = iter\toArray($this->sharesList->getFormattedShares($userId, $filter, $path, $token)); - + $formats = ['json', 'csv']; $formatedDateTime = $dateTime->format('YmdHi'); - $reports = []; - foreach (['json', 'csv'] as $format) { - $fileName=$formatedDateTime.' - Shares report.'.$format; - $savedFile = $folder->newFile($fileName); - $savedFile->putContent($this->sharesList->getSerializedShares($shares, $format)); - $reports[] = [ - 'name' => $savedFile->getName(), - 'url' => $this->url->linkToRouteAbsolute('files.viewcontroller.showFile', ['fileid' => $savedFile->getId()]) - ]; + foreach ($formats as $key => $format) { + $fileName = $formatedDateTime . ' - Shares report.' . $format; + if (!array_key_exists($fileName, $this->reports)) { + if ($key === array_key_first($formats)) { + $shares = iter\toArray($this->sharesList->getFormattedShares($userId, $filter, $path, $token)); + } + $reportFile = $folder->newFile($fileName); + $reportFile->putContent($this->sharesList->getSerializedShares($shares, $format)); + $this->reports[$reportFile->getName()] = $this->url->linkToRouteAbsolute( + 'files.View.showFile', + ['fileid' => $reportFile->getId()] + ); + } } - - return $reports; } - public function sendReport(string $recipient, \DateTimeImmutable $dateTime, array $reports) { + public function sendReport(string $recipient, \DateTimeImmutable $dateTime) { $defaultLanguage = $this->config->getSystemValue('default_language', 'en'); $userLanguages = $this->config->getUserValue($recipient, 'core', 'lang'); $language = (!empty($userLanguages)) ? $userLanguages : $defaultLanguage; @@ -139,12 +146,13 @@ public function sendReport(string $recipient, \DateTimeImmutable $dateTime, arra $template->addHeader(); $template->addBodyText('You can find the list of shares reports generated on ' . $formatedDateTime . ':'); - foreach ($reports as $report) { + foreach ($this->reports as $name => $url) { $template->addBodyListItem( - ''.$report['name'].'', + '' . $name . '', '', '', - $report['name'].': '.$report['url']); + $name . ': ' . $url + ); } $template->addFooter('', $language); From 67f299c133882673e31aef26f399243af8dc5994 Mon Sep 17 00:00:00 2001 From: Florent Poinsaut Date: Wed, 19 Oct 2022 15:52:01 +0000 Subject: [PATCH 16/18] Add diff report Signed-off-by: Florent Poinsaut --- composer.json | 3 +- composer.lock | 45 +++++++++++- lib/Command/ListShares.php | 7 -- lib/Command/SendShares.php | 12 +++- lib/Service/ReportSender.php | 134 +++++++++++++++++++++++++++++++---- 5 files changed, 178 insertions(+), 23 deletions(-) diff --git a/composer.json b/composer.json index 45376fd8..f79cf8cc 100644 --- a/composer.json +++ b/composer.json @@ -4,7 +4,8 @@ "license": "MIT", "require": { "nikic/iter": "^2.2", - "symfony/serializer": "^5.4" + "symfony/serializer": "^5.4", + "swaggest/json-diff": "^3.9" }, "require-dev": { "phpunit/phpunit": "^9", diff --git a/composer.lock b/composer.lock index c9c367d4..a0876f4f 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "280059a3e5c545fce72e8e3f92894660", + "content-hash": "2e26a4680f38483a82654971170dc65d", "packages": [ { "name": "nikic/iter", @@ -56,6 +56,49 @@ }, "time": "2021-08-02T15:04:32+00:00" }, + { + "name": "swaggest/json-diff", + "version": "v3.9.0", + "source": { + "type": "git", + "url": "https://github.com/swaggest/json-diff.git", + "reference": "ff3a7921e9f1aa096067eb541fcfd0e7611c558c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/swaggest/json-diff/zipball/ff3a7921e9f1aa096067eb541fcfd0e7611c558c", + "reference": "ff3a7921e9f1aa096067eb541fcfd0e7611c558c", + "shasum": "" + }, + "require": { + "ext-json": "*" + }, + "require-dev": { + "phperf/phpunit": "4.8.37" + }, + "type": "library", + "autoload": { + "psr-4": { + "Swaggest\\JsonDiff\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Viacheslav Poturaev", + "email": "vearutop@gmail.com" + } + ], + "description": "JSON diff/rearrange/patch/pointer library for PHP", + "support": { + "issues": "https://github.com/swaggest/json-diff/issues", + "source": "https://github.com/swaggest/json-diff/tree/v3.9.0" + }, + "time": "2022-08-29T15:04:08+00:00" + }, { "name": "symfony/deprecation-contracts", "version": "v2.5.1", diff --git a/lib/Command/ListShares.php b/lib/Command/ListShares.php index eb9683c9..6e94d604 100644 --- a/lib/Command/ListShares.php +++ b/lib/Command/ListShares.php @@ -37,13 +37,6 @@ public function configure() { $this->setName('sharing:list') ->setDescription('List who has access to shares by owner') - ->addOption( - 'output', - 'o', - InputOption::VALUE_OPTIONAL, - 'Output format (json or csv, default is json)', - 'json' - ) ->addOption( 'output', 'o', diff --git a/lib/Command/SendShares.php b/lib/Command/SendShares.php index b270c8de..5127f4fe 100644 --- a/lib/Command/SendShares.php +++ b/lib/Command/SendShares.php @@ -57,6 +57,12 @@ public function configure() { $this->setName('sharing:send') ->setDescription('Send list who has access to shares by owner') + ->addOption( + 'diff', + 'd', + InputOption::VALUE_NONE, + 'Create a differential report in json format from the last available report' + ) ->addOption( 'recipients', 'r', @@ -75,6 +81,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $this->checkAllRequiredOptionsAreNotEmpty($input); [$user, $path, $token, $filter] = $this->getOptions($input); + $diff = $input->getOption('diff'); $recipients = $input->getOption('recipients'); $targetPath = $input->getOption('target-path'); @@ -91,9 +98,12 @@ protected function execute(InputInterface $input, OutputInterface $output): int $token ); + if ($diff) { + $this->reportSender->diff($recipient, $targetPath); + } + $this->reportSender->sendReport($recipient, $dateTime); } - return 0; } diff --git a/lib/Service/ReportSender.php b/lib/Service/ReportSender.php index dcd16d26..f9cfbd0b 100644 --- a/lib/Service/ReportSender.php +++ b/lib/Service/ReportSender.php @@ -27,12 +27,21 @@ namespace OCA\ShareListing\Service; +use Icewind\SMB\Exception\NotFoundException; use iter; +use OC\Files\Search\SearchBinaryOperator; +use OC\Files\Search\SearchComparison; +use OC\Files\Search\SearchOrder; +use OC\Files\Search\SearchQuery; use OCA\ShareListing\Service\SharesList; use OCP\Defaults; use OCP\Files\FileInfo; use OCP\Files\Folder; +use OCP\Files\InvalidDirectoryException; use OCP\Files\IRootFolder; +use OCP\Files\Search\ISearchBinaryOperator; +use OCP\Files\Search\ISearchComparison; +use OCP\Files\Search\ISearchOrder; use OCP\IConfig; use OCP\IURLGenerator; use OCP\IUserManager; @@ -40,14 +49,18 @@ use OCP\Mail\IMailer; use OCP\Util; use Psr\Log\LoggerInterface; +use Swaggest\JsonDiff\JsonDiff; -class ReportSender { - public const ACTIVITY_LIMIT = 20; +class ReportSender +{ + protected const REPORT_NAME = ' - Shares report.'; /** @var string */ private $appName; /** @var Iconfig */ private $config; + /** @var ?array */ + protected $diffReport = null; private $mailer; private $userManager; @@ -115,22 +128,27 @@ public function createReport( $formats = ['json', 'csv']; $formatedDateTime = $dateTime->format('YmdHi'); foreach ($formats as $key => $format) { - $fileName = $formatedDateTime . ' - Shares report.' . $format; + $fileName = $formatedDateTime . self::REPORT_NAME . $format; if (!array_key_exists($fileName, $this->reports)) { if ($key === array_key_first($formats)) { $shares = iter\toArray($this->sharesList->getFormattedShares($userId, $filter, $path, $token)); } $reportFile = $folder->newFile($fileName); - $reportFile->putContent($this->sharesList->getSerializedShares($shares, $format)); - $this->reports[$reportFile->getName()] = $this->url->linkToRouteAbsolute( - 'files.View.showFile', - ['fileid' => $reportFile->getId()] - ); + $data = $this->sharesList->getSerializedShares($shares, $format); + $reportFile->putContent($data); + $this->reports[$reportFile->getName()] = [ + 'url' => $this->url->linkToRouteAbsolute( + 'files.View.showFile', + ['fileid' => $reportFile->getId()] + ), + 'data' => $data + ]; } } } - public function sendReport(string $recipient, \DateTimeImmutable $dateTime) { + public function sendReport(string $recipient, \DateTimeImmutable $dateTime) + { $defaultLanguage = $this->config->getSystemValue('default_language', 'en'); $userLanguages = $this->config->getUserValue($recipient, 'core', 'lang'); $language = (!empty($userLanguages)) ? $userLanguages : $defaultLanguage; @@ -146,12 +164,21 @@ public function sendReport(string $recipient, \DateTimeImmutable $dateTime) { $template->addHeader(); $template->addBodyText('You can find the list of shares reports generated on ' . $formatedDateTime . ':'); - foreach ($this->reports as $name => $url) { + foreach ($this->reports as $name => $value) { $template->addBodyListItem( - '' . $name . '', + '' . $name . '', '', '', - $name . ': ' . $url + $name . ': ' . $value['url'] + ); + } + if ($this->diffReport !== null) { + $template->addBodyText('You can also find the differential between this report and the previous one:'); + $template->addBodyListItem( + '' . $this->diffReport['fileName'] . '', + '', + '', + $this->diffReport['fileName'] . ': ' . $this->diffReport['url'] ); } @@ -170,7 +197,8 @@ public function sendReport(string $recipient, \DateTimeImmutable $dateTime) { } } - protected function getEmailAdressFromUserId(string $userId): ?string { + protected function getEmailAdressFromUserId(string $userId): ?string + { $user = $this->userManager->get($userId); if ($user === null) { $this->logger->warning( @@ -191,4 +219,84 @@ protected function getEmailAdressFromUserId(string $userId): ?string { return $email; } + + public function diff( + string $userId, + string $dir + ) { + $userFolder = $this->root->getUserFolder($userId); + + if ($userFolder->nodeExists($dir)) { + /** @var Folder $folder */ + $folder = $userFolder->get($dir); + if ($folder->getType() !== FileInfo::TYPE_FOLDER) { + throw new InvalidDirectoryException('Invalid directory, "' . $dir . '" not a folder'); + } + } else { + throw new InvalidDirectoryException('Invalid directory, "' . $dir . '" does not exist'); + } + + $search = $userFolder->search( + new SearchQuery( + new SearchBinaryOperator( + ISearchBinaryOperator::OPERATOR_OR, + [ + new SearchComparison( + ISearchComparison::COMPARE_LIKE, + 'name', + '%' . self::REPORT_NAME . 'json' + ) + ] + ), + 1, + 1, + [new SearchOrder(ISearchOrder::DIRECTION_DESCENDING, 'mtime')] + ) + ); + + if (empty($search)) { + throw new NotFoundException('No previous report found on this folder.'); + } + + $previousFile = $search[0]; + $previousFilename = $previousFile->getName(); + $previousDateTime = substr($previousFilename, 0, 12); + $previousContent = json_decode($previousFile->getContent()); + $previousContentWithId = []; + foreach ($previousContent as $value) { + $previousContentWithId[$value->id] = $value; + } + + $newFilename = array_keys($this->reports)[0]; + $newDateTime = substr($newFilename, 0, 12); + $newContent = json_decode(array_values($this->reports)[0]['data']); + $newContentWithId = []; + foreach ($newContent as $value) { + $newContentWithId[$value->id] = $value; + } + + $jsonDiff = new JsonDiff( + $previousContentWithId, + $newContentWithId, + JsonDiff::REARRANGE_ARRAYS + JsonDiff::COLLECT_MODIFIED_DIFF + ); + + $reportFilename = $previousDateTime . ' - ' . $newDateTime . ' - Shares report diff.json'; + $reportFile = $folder->newFile($reportFilename); + $res = [ + 'added' => $jsonDiff->getAdded(), + 'removed' => $jsonDiff->getRemoved(), + 'modified' => $jsonDiff->getModifiedDiff() + ]; + + $reportFile->putContent(json_encode($res, JSON_PRETTY_PRINT)); + + $this->diffReport = [ + 'url' => $this->url->linkToRouteAbsolute( + 'files.View.showFile', + ['fileid' => $reportFile->getId()] + ), + 'fileName' => $reportFilename + ]; + } } From 35c98cba3bdb38dbb98a98b327f6006174013953 Mon Sep 17 00:00:00 2001 From: Florent Poinsaut Date: Thu, 3 Nov 2022 15:12:03 +0000 Subject: [PATCH 17/18] add permissions mapping Signed-off-by: Florent Poinsaut --- lib/Service/ReportSender.php | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/lib/Service/ReportSender.php b/lib/Service/ReportSender.php index f9cfbd0b..3c47ac00 100644 --- a/lib/Service/ReportSender.php +++ b/lib/Service/ReportSender.php @@ -27,7 +27,6 @@ namespace OCA\ShareListing\Service; -use Icewind\SMB\Exception\NotFoundException; use iter; use OC\Files\Search\SearchBinaryOperator; use OC\Files\Search\SearchComparison; @@ -39,6 +38,7 @@ use OCP\Files\Folder; use OCP\Files\InvalidDirectoryException; use OCP\Files\IRootFolder; +use OCP\Files\NotFoundException; use OCP\Files\Search\ISearchBinaryOperator; use OCP\Files\Search\ISearchComparison; use OCP\Files\Search\ISearchOrder; @@ -182,6 +182,14 @@ public function sendReport(string $recipient, \DateTimeImmutable $dateTime) ); } + $template->addBodyText('Permissions mapping:'); + $template->addBodyListItem('1 = read'); + $template->addBodyListItem('2 = update'); + $template->addBodyListItem('4 = create'); + $template->addBodyListItem('8 = delete'); + $template->addBodyListItem('16 = share'); + $template->addBodyListItem('31 = all (default: 31, for public shares: 1)'); + $template->addFooter('', $language); $message = $this->mailer->createMessage(); From f5d84b566cdd7454cf01d972d7f54262c42dd664 Mon Sep 17 00:00:00 2001 From: Florent Poinsaut Date: Thu, 3 Nov 2022 15:27:28 +0000 Subject: [PATCH 18/18] Add send command in README Signed-off-by: Florent Poinsaut --- README.md | 33 +++++++++++++++++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index a0539781..afd6c060 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,9 @@ This app allows generating reports of shares on the system. ## Usage -### Command +### Commands + +#### List ```sh ./occ sharing:list [-u|--user [USER]] [-p|--path [PATH]] [-t|--token [TOKEN]] [-f|--filter [FILTER]] [-o|--output FORMAT] @@ -13,7 +15,7 @@ This app allows generating reports of shares on the system. Without options, the command yields the unfiltered list of all shares.\ With options, the list is narrowed down using the filters set. -### Options +##### Options * `-u [USER]` or `--user [USER]`\ List only shares of the given user. @@ -27,6 +29,33 @@ With options, the list is narrowed down using the filters set. * `-o FORMAT` or `--output FORMAT`\ Set the output format (json or csv, default is json). +#### Send + +```sh +./occ sharing:send [-u|--user USER] [-p|--path PATH] [-t|--token TOKEN] [-f|--filter FILTER] [-o|--output FORMAT] +``` + +Without options, the command yields the unfiltered list of all shares.\ +With options, the list is narrowed down using the filters set. + +##### Options + +* `-r` or `--recipients`\ + Recipients users of generated reports. +* `-x` or `--target-path`\ + Generated reports will be stored on this path. +* `-d` or `--diff`\ + Create a differential report in json format from the last available report. +* `-u [USER]` or `--user [USER]`\ + List only shares of the given user. +* `-p [PATH]` or `--path [PATH]`\ + List only shares within the given path. +* `-t [TOKEN]` or `--token [TOKEN]`\ + List only shares that use a token that (at least partly) matches the argument. +* `-f [FILTER]` or `--filter [FILTER]`\ + List only shares where the TYPE matches the argument.\ + Possible values for the filter argument: {owner, initiator, recipient} + ## Examples To better illustrate how the app work see the examples below: