diff --git a/.travis.yml b/.travis.yml index 0a3c744e..621f4532 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,8 +1,8 @@ language: php php: - - 7.1 - 7.2 - 7.3 + - 7.4snapshot env: global: diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php index 51f23027..59b1a9f4 100644 --- a/lib/AppInfo/Application.php +++ b/lib/AppInfo/Application.php @@ -25,18 +25,25 @@ use OCA\Guests\GroupBackend; use OCA\Guests\GuestManager; use OCA\Guests\Hooks; +use OCA\Guests\Notifications\Notifier; use OCA\Guests\RestrictionManager; use OCA\Guests\UserBackend; use OCP\AppFramework\App; +use OCP\IUser; +use OCP\Notification\IManager as INotificationManager; class Application extends App { + + public const APP_ID = 'guests'; + public function __construct(array $urlParams = array()) { - parent::__construct('guests', $urlParams); + parent::__construct(self::APP_ID, $urlParams); } public function setup() { $this->setupGuestManagement(); $this->setupGuestRestrictions(); + $this->setupNotifications(); } public function lateSetup() { @@ -71,6 +78,10 @@ private function getHookManager() { return $this->getContainer()->query(Hooks::class); } + private function getNotificationManager(): INotificationManager { + return $this->getContainer()->query(INotificationManager::class); + } + private function setupGuestManagement() { $container = $this->getContainer(); /** @var Server $server */ @@ -85,7 +96,8 @@ private function setupGuestManagement() { $server->getEventDispatcher()->addListener( 'OCA\Files::loadAdditionalScripts', function () { - \OCP\Util::addScript('guests', 'main'); + \OCP\Util::addScript(self::APP_ID, 'main'); + \OCP\Util::addStyle(self::APP_ID, 'app'); } ); } @@ -93,6 +105,7 @@ function () { $server->getGroupManager()->addBackend($container->query(GroupBackend::class)); /** @var Hooks $hooks */ $server->getEventDispatcher()->addListener('OCP\Share::postShare', [$this->getHookManager(), 'handlePostShare']); + $server->getEventDispatcher()->addListener(IUser::class . '::firstLogin', [$this->getHookManager(), 'handleFirstLogin']); } private function setupGuestRestrictions() { @@ -116,4 +129,10 @@ private function setupGuestRestrictions() { }); } } + + private function setupNotifications(): void { + $notificationManager = $this->getNotificationManager(); + $notificationManager->registerNotifierService(Notifier::class); + } + } diff --git a/lib/Hooks.php b/lib/Hooks.php index 8799702e..10aa2801 100644 --- a/lib/Hooks.php +++ b/lib/Hooks.php @@ -23,56 +23,62 @@ namespace OCA\Guests; use OC\Files\Filesystem; +use OCA\Files\Exception\TransferOwnershipException; +use OCA\Files\Service\OwnershipTransferService; +use OCA\Guests\AppInfo\Application; use OCA\Guests\Storage\ReadOnlyJail; use OCP\AppFramework\Db\DoesNotExistException; +use OCP\AppFramework\IAppContainer; +use OCP\AppFramework\QueryException; use OCP\Constants; -use OCP\Files\IHomeStorage; use OCP\Files\Storage\IStorage; use OCP\IConfig; -use OCP\IGroupManager; use OCP\ILogger; use OCP\IRequest; +use OCP\IUser; use OCP\IUserManager; use OCP\IUserSession; +use OCP\Notification\IManager as INotificationManager; use OCP\Security\ICrypto; +use OCP\Share\IManager as IShareManager; use OCP\Share\IShare; use Symfony\Component\EventDispatcher\GenericEvent; class Hooks { - /** - * @var ILogger - */ + /** @var ILogger */ private $logger; - /** - * @var IUserSession - */ + /** @var IUserSession */ private $userSession; - /** - * @var IRequest - */ + + /** @var IRequest */ private $request; - /** - * @var Mail - */ + /** @var Mail */ private $mail; - /** - * @var IUserManager - */ + /** @var IUserManager */ private $userManager; - /** - * @var ICrypto - */ + /** @var ICrypto */ private $crypto; /** @var GuestManager */ private $guestManager; + /** @var UserBackend */ + private $userBackend; + + /** @var IAppContainer */ + private $container; + + /** @var INotificationManager */ + private $notificationManager; + /** @var IShareManager */ + private $shareManager; + public function __construct( ILogger $logger, IUserSession $userSession, @@ -81,7 +87,11 @@ public function __construct( IUserManager $userManager, IConfig $config, ICrypto $crypto, - GuestManager $guestManager + GuestManager $guestManager, + UserBackend $userBackend, + IAppContainer $container, + INotificationManager $notificationManager, + IShareManager $shareManager ) { $this->logger = $logger; $this->userSession = $userSession; @@ -91,6 +101,10 @@ public function __construct( $this->config = $config; $this->crypto = $crypto; $this->guestManager = $guestManager; + $this->userBackend = $userBackend; + $this->container = $container; + $this->notificationManager = $notificationManager; + $this->shareManager = $shareManager; } public function handlePostShare(GenericEvent $event) { @@ -103,7 +117,7 @@ public function handlePostShare(GenericEvent $event) { if (!$isGuest) { $this->logger->debug( "ignoring user '$shareWith', not a guest", - ['app' => 'guests'] + ['app' => Application::APP_ID] ); return; @@ -112,7 +126,7 @@ public function handlePostShare(GenericEvent $event) { if (!($share->getNodeType() === 'folder' || $share->getNodeType() === 'file')) { $this->logger->debug( "ignoring share for itemType " . $share->getNodeType(), - ['app' => 'guests'] + ['app' => Application::APP_ID] ); return; @@ -129,7 +143,7 @@ public function handlePostShare(GenericEvent $event) { } $this->logger->debug("checking if '$shareWith' has a password", - ['app' => 'guests']); + ['app' => Application::APP_ID]); $passwordToken = $this->config->getUserValue( @@ -160,7 +174,7 @@ public function handlePostShare(GenericEvent $event) { $share->setMailSend(false); } } catch (DoesNotExistException $ex) { - $this->logger->error("'$shareWith' does not exist", ['app' => 'guests']); + $this->logger->error("'$shareWith' does not exist", ['app' => Application::APP_ID]); } } @@ -182,4 +196,80 @@ public function setupReadonlyFilesystem(array $params) { }); } } + + public function handleFirstLogin(GenericEvent $event): void { + if ($this->config->getSystemValue('migrate_guest_user_data', false) === false) { + return; + } + + /** @var IUser $user */ + $user = $event->getSubject(); + $this->logger->debug('User ' . $user->getUID() . ' logged in for the very first time. Checking guests data import.'); + + $email = $user->getEMailAddress(); + if ($email === null) { + $this->logger->info('User ' . $user->getUID() . ' does not have an e-mail address set. Skipping guests data import.'); + return; + } + + if (!$this->userBackend->userExists($email)) { + $this->logger->info('No guest user for ' . $email . ' found. Skipping guests data import.'); + return; + } + + if ($email === $user->getUID()) { + // This is the guest user, logging in for the very first time + return; + } + + try { + /** @var OwnershipTransferService $ownershipTransferService */ + $ownershipTransferService = $this->container->query(OwnershipTransferService::class); + } catch (QueryException $e) { + $this->logger->logException($e, [ + 'level' => ILogger::ERROR, + 'message' => 'Could not resolve ownership transfer service to import guest user data', + ]); + return; + } + + $guestUser = $this->userManager->get($email); + if ($guestUser === null) { + $this->logger->warning("Guest user $email does not exist (anymore)"); + return; + } + try { + $ownershipTransferService->transfer( + $guestUser, + $user, + '/' + ); + } catch (TransferOwnershipException $e) { + $this->logger->logException($e, [ + 'level' => ILogger::ERROR, + 'message' => 'Could not import guest user data', + ]); + return; + } + + // Update incomming shares + $shares = $this->shareManager->getSharedWith($guestUser->getUID(), IShare::TYPE_USER); + foreach ($shares as $share) { + $share->setSharedWith($user->getUID()); + $this->shareManager->updateShare($share); + } + + // Disable previous account + $guestUser->setEnabled(false); + + $notification = $this->notificationManager->createNotification(); + $notification + ->setApp(Application::APP_ID) + ->setSubject('data_migrated_to_system_user') + ->setObject('user', $email) + ->setDateTime(new \DateTime()) + ->setUser($user->getUID()); + $this->notificationManager->notify($notification); + } + } diff --git a/lib/Notifications/Notifier.php b/lib/Notifications/Notifier.php new file mode 100644 index 00000000..816c6bea --- /dev/null +++ b/lib/Notifications/Notifier.php @@ -0,0 +1,80 @@ + + * + * @author 2019 Christoph Wurst + * + * @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\Guests\Notifications; + +use InvalidArgumentException; +use OCA\Guests\AppInfo\Application; +use OCP\IURLGenerator; +use OCP\L10N\IFactory; +use OCP\Notification\INotification; +use OCP\Notification\INotifier; + +class Notifier implements INotifier { + + /** @var IFactory */ + private $factory; + + /** @var IURLGenerator */ + private $url; + + public function __construct(IFactory $factory, + IURLGenerator $url) { + $this->factory = $factory; + $this->url = $url; + } + + public function getID(): string { + return Application::APP_ID; + } + + public function getName(): string { + return $this->factory->get(Application::APP_ID)->t('Guests'); + } + + public function prepare(INotification $notification, string $languageCode): INotification { + if ($notification->getApp() !== Application::APP_ID) { + // Not my app => throw + throw new InvalidArgumentException(); + } + + // Read the language from the notification + $l = $this->factory->get(Application::APP_ID, $languageCode); + switch ($notification->getSubject()) { + case 'data_migrated_to_system_user': + $notification->setParsedSubject( + $l->t('Data imported') + )->setParsedMessage( + $l->t('Data from your previous guest account was successfully imported into your new account.') + ); + + $notification->setIcon($this->url->getAbsoluteURL($this->url->imagePath('core', 'actions/info.svg'))); + + return $notification; + + default: + // Unknown subject => Unknown notification => throw + throw new InvalidArgumentException(); + } + } +} diff --git a/tests/unit/AppWhitelistTest.php b/tests/unit/AppWhitelistTest.php index 976f6589..8d866711 100644 --- a/tests/unit/AppWhitelistTest.php +++ b/tests/unit/AppWhitelistTest.php @@ -28,24 +28,25 @@ use OCP\IL10N; use OCP\IURLGenerator; use OCP\IUser; +use PHPUnit\Framework\MockObject\MockObject; use Test\TestCase; class AppWhitelistTest extends TestCase { - /** @var Config|\PHPUnit_Framework_MockObject_MockObject */ + /** @var Config|MockObject */ private $config; - /** @var GuestManager|\PHPUnit_Framework_MockObject_MockObject */ + /** @var GuestManager|MockObject */ private $guestManager; - /** @var IL10N|\PHPUnit_Framework_MockObject_MockObject */ + /** @var IL10N|MockObject */ private $l10n; - /** @var IAppManager|\PHPUnit_Framework_MockObject_MockObject */ + /** @var IAppManager|MockObject */ private $appManager; - /** @var IURLGenerator|\PHPUnit_Framework_MockObject_MockObject */ + /** @var IURLGenerator|MockObject */ private $urlGenerator; /** @var AppWhitelist */ private $appWhitelist; - protected function setUp() { + protected function setUp(): void { parent::setUp(); $this->config = $this->createMock(Config::class); diff --git a/tests/unit/GroupBackendTest.php b/tests/unit/GroupBackendTest.php index 905a77e5..5802ae59 100644 --- a/tests/unit/GroupBackendTest.php +++ b/tests/unit/GroupBackendTest.php @@ -26,20 +26,21 @@ use OCA\Guests\GuestManager; use OCP\IUser; use OCP\IUserSession; +use PHPUnit\Framework\MockObject\MockObject; use Test\TestCase; class GroupBackendTest extends TestCase { - /** @var GuestManager|\PHPUnit_Framework_MockObject_MockObject */ + /** @var GuestManager|MockObject */ private $guestManager; - /** @var Config|\PHPUnit_Framework_MockObject_MockObject */ + /** @var Config|MockObject */ private $config; - /** @var IUserSession|\PHPUnit_Framework_MockObject_MockObject */ + /** @var IUserSession|MockObject */ private $userSession; /** @var GroupBackend */ private $backend; - protected function setUp() { + protected function setUp(): void { parent::setUp(); $this->guestManager = $this->createMock(GuestManager::class); diff --git a/tests/unit/GuestManagerTest.php b/tests/unit/GuestManagerTest.php index 072f2292..1553ba6d 100644 --- a/tests/unit/GuestManagerTest.php +++ b/tests/unit/GuestManagerTest.php @@ -31,30 +31,31 @@ use OCP\Security\ICrypto; use OCP\Security\ISecureRandom; use OCP\Share\IManager; +use PHPUnit\Framework\MockObject\MockObject; use Test\TestCase; class GuestManagerTest extends TestCase { - /** @var UserBackend|\PHPUnit_Framework_MockObject_MockObject */ + /** @var UserBackend|MockObject */ private $userBackend; - /** @var IUserSession|\PHPUnit_Framework_MockObject_MockObject */ + /** @var IUserSession|MockObject */ private $userSession; - /** @var IConfig|\PHPUnit_Framework_MockObject_MockObject */ + /** @var IConfig|MockObject */ private $config; - /** @var ISecureRandom|\PHPUnit_Framework_MockObject_MockObject */ + /** @var ISecureRandom|MockObject */ private $random; - /** @var ICrypto|\PHPUnit_Framework_MockObject_MockObject */ + /** @var ICrypto|MockObject */ private $crypto; - /** @var IManager|\PHPUnit_Framework_MockObject_MockObject */ + /** @var IManager|MockObject */ private $shareManager; - /** @var IDBConnection|\PHPUnit_Framework_MockObject_MockObject */ + /** @var IDBConnection|MockObject */ private $conneciton; - /** @var IEventDispatcher|\PHPUnit_Framework_MockObject_MockObject */ + /** @var IEventDispatcher|MockObject */ private $eventDispatcher; /** @var GuestManager */ private $guestManager; - protected function setUp() { + protected function setUp(): void { parent::setUp(); $this->userBackend = $this->createMock(UserBackend::class); diff --git a/tests/unit/UserBackendTest.php b/tests/unit/UserBackendTest.php index 603a657f..99e234a6 100644 --- a/tests/unit/UserBackendTest.php +++ b/tests/unit/UserBackendTest.php @@ -44,7 +44,7 @@ private function clearGuests() { $query->delete('guests_users')->execute(); } - protected function setUp() { + protected function setUp(): void { parent::setUp(); $this->clearGuests(); @@ -59,10 +59,10 @@ protected function setUp() { ); } - protected function tearDown() { + protected function tearDown(): void { $this->clearGuests(); - return parent::tearDown(); + parent::tearDown(); } public function testCreate() {