Generate backups code notification if not enable but 2fa is
Generate a notification to generate backup codes if you enable an other 2FA provider but backup codes are not yet generated. * Add event listner * Insert background job * Background job tests and emits notification every 2 weeks * If the backup codes are generated the next run will remove the job Signed-off-by: Roeland Jago Douma <roeland@famdouma.nl>
This commit is contained in:
parent
a95154642d
commit
956fe1b867
|
@ -8,18 +8,21 @@ $baseDir = $vendorDir;
|
||||||
return array(
|
return array(
|
||||||
'OCA\\TwoFactorBackupCodes\\Activity\\Provider' => $baseDir . '/../lib/Activity/Provider.php',
|
'OCA\\TwoFactorBackupCodes\\Activity\\Provider' => $baseDir . '/../lib/Activity/Provider.php',
|
||||||
'OCA\\TwoFactorBackupCodes\\AppInfo\\Application' => $baseDir . '/../lib/AppInfo/Application.php',
|
'OCA\\TwoFactorBackupCodes\\AppInfo\\Application' => $baseDir . '/../lib/AppInfo/Application.php',
|
||||||
|
'OCA\\TwoFactorBackupCodes\\BackgroundJob\\RememberBackupCodesJob' => $baseDir . '/../lib/BackgroundJob/RememberBackupCodesJob.php',
|
||||||
'OCA\\TwoFactorBackupCodes\\Controller\\SettingsController' => $baseDir . '/../lib/Controller/SettingsController.php',
|
'OCA\\TwoFactorBackupCodes\\Controller\\SettingsController' => $baseDir . '/../lib/Controller/SettingsController.php',
|
||||||
'OCA\\TwoFactorBackupCodes\\Db\\BackupCode' => $baseDir . '/../lib/Db/BackupCode.php',
|
'OCA\\TwoFactorBackupCodes\\Db\\BackupCode' => $baseDir . '/../lib/Db/BackupCode.php',
|
||||||
'OCA\\TwoFactorBackupCodes\\Db\\BackupCodeMapper' => $baseDir . '/../lib/Db/BackupCodeMapper.php',
|
'OCA\\TwoFactorBackupCodes\\Db\\BackupCodeMapper' => $baseDir . '/../lib/Db/BackupCodeMapper.php',
|
||||||
'OCA\\TwoFactorBackupCodes\\Event\\CodesGenerated' => $baseDir . '/../lib/Event/CodesGenerated.php',
|
'OCA\\TwoFactorBackupCodes\\Event\\CodesGenerated' => $baseDir . '/../lib/Event/CodesGenerated.php',
|
||||||
'OCA\\TwoFactorBackupCodes\\Listener\\ActivityPublisher' => $baseDir . '/../lib/Listener/ActivityPublisher.php',
|
'OCA\\TwoFactorBackupCodes\\Listener\\ActivityPublisher' => $baseDir . '/../lib/Listener/ActivityPublisher.php',
|
||||||
'OCA\\TwoFactorBackupCodes\\Listener\\IListener' => $baseDir . '/../lib/Listener/IListener.php',
|
'OCA\\TwoFactorBackupCodes\\Listener\\IListener' => $baseDir . '/../lib/Listener/IListener.php',
|
||||||
|
'OCA\\TwoFactorBackupCodes\\Listener\\ProviderEnabled' => $baseDir . '/../lib/Listener/ProviderEnabled.php',
|
||||||
'OCA\\TwoFactorBackupCodes\\Listener\\RegistryUpdater' => $baseDir . '/../lib/Listener/RegistryUpdater.php',
|
'OCA\\TwoFactorBackupCodes\\Listener\\RegistryUpdater' => $baseDir . '/../lib/Listener/RegistryUpdater.php',
|
||||||
'OCA\\TwoFactorBackupCodes\\Migration\\Version1002Date20170607104347' => $baseDir . '/../lib/Migration/Version1002Date20170607104347.php',
|
'OCA\\TwoFactorBackupCodes\\Migration\\Version1002Date20170607104347' => $baseDir . '/../lib/Migration/Version1002Date20170607104347.php',
|
||||||
'OCA\\TwoFactorBackupCodes\\Migration\\Version1002Date20170607113030' => $baseDir . '/../lib/Migration/Version1002Date20170607113030.php',
|
'OCA\\TwoFactorBackupCodes\\Migration\\Version1002Date20170607113030' => $baseDir . '/../lib/Migration/Version1002Date20170607113030.php',
|
||||||
'OCA\\TwoFactorBackupCodes\\Migration\\Version1002Date20170919123342' => $baseDir . '/../lib/Migration/Version1002Date20170919123342.php',
|
'OCA\\TwoFactorBackupCodes\\Migration\\Version1002Date20170919123342' => $baseDir . '/../lib/Migration/Version1002Date20170919123342.php',
|
||||||
'OCA\\TwoFactorBackupCodes\\Migration\\Version1002Date20170926101419' => $baseDir . '/../lib/Migration/Version1002Date20170926101419.php',
|
'OCA\\TwoFactorBackupCodes\\Migration\\Version1002Date20170926101419' => $baseDir . '/../lib/Migration/Version1002Date20170926101419.php',
|
||||||
'OCA\\TwoFactorBackupCodes\\Migration\\Version1002Date20180821043638' => $baseDir . '/../lib/Migration/Version1002Date20180821043638.php',
|
'OCA\\TwoFactorBackupCodes\\Migration\\Version1002Date20180821043638' => $baseDir . '/../lib/Migration/Version1002Date20180821043638.php',
|
||||||
|
'OCA\\TwoFactorBackupCodes\\Notifications\\Notifier' => $baseDir . '/../lib/Notifications/Notifier.php',
|
||||||
'OCA\\TwoFactorBackupCodes\\Provider\\BackupCodesProvider' => $baseDir . '/../lib/Provider/BackupCodesProvider.php',
|
'OCA\\TwoFactorBackupCodes\\Provider\\BackupCodesProvider' => $baseDir . '/../lib/Provider/BackupCodesProvider.php',
|
||||||
'OCA\\TwoFactorBackupCodes\\Service\\BackupCodeStorage' => $baseDir . '/../lib/Service/BackupCodeStorage.php',
|
'OCA\\TwoFactorBackupCodes\\Service\\BackupCodeStorage' => $baseDir . '/../lib/Service/BackupCodeStorage.php',
|
||||||
'OCA\\TwoFactorBackupCodes\\Settings\\Personal' => $baseDir . '/../lib/Settings/Personal.php',
|
'OCA\\TwoFactorBackupCodes\\Settings\\Personal' => $baseDir . '/../lib/Settings/Personal.php',
|
||||||
|
|
|
@ -23,18 +23,21 @@ class ComposerStaticInitTwoFactorBackupCodes
|
||||||
public static $classMap = array (
|
public static $classMap = array (
|
||||||
'OCA\\TwoFactorBackupCodes\\Activity\\Provider' => __DIR__ . '/..' . '/../lib/Activity/Provider.php',
|
'OCA\\TwoFactorBackupCodes\\Activity\\Provider' => __DIR__ . '/..' . '/../lib/Activity/Provider.php',
|
||||||
'OCA\\TwoFactorBackupCodes\\AppInfo\\Application' => __DIR__ . '/..' . '/../lib/AppInfo/Application.php',
|
'OCA\\TwoFactorBackupCodes\\AppInfo\\Application' => __DIR__ . '/..' . '/../lib/AppInfo/Application.php',
|
||||||
|
'OCA\\TwoFactorBackupCodes\\BackgroundJob\\RememberBackupCodesJob' => __DIR__ . '/..' . '/../lib/BackgroundJob/RememberBackupCodesJob.php',
|
||||||
'OCA\\TwoFactorBackupCodes\\Controller\\SettingsController' => __DIR__ . '/..' . '/../lib/Controller/SettingsController.php',
|
'OCA\\TwoFactorBackupCodes\\Controller\\SettingsController' => __DIR__ . '/..' . '/../lib/Controller/SettingsController.php',
|
||||||
'OCA\\TwoFactorBackupCodes\\Db\\BackupCode' => __DIR__ . '/..' . '/../lib/Db/BackupCode.php',
|
'OCA\\TwoFactorBackupCodes\\Db\\BackupCode' => __DIR__ . '/..' . '/../lib/Db/BackupCode.php',
|
||||||
'OCA\\TwoFactorBackupCodes\\Db\\BackupCodeMapper' => __DIR__ . '/..' . '/../lib/Db/BackupCodeMapper.php',
|
'OCA\\TwoFactorBackupCodes\\Db\\BackupCodeMapper' => __DIR__ . '/..' . '/../lib/Db/BackupCodeMapper.php',
|
||||||
'OCA\\TwoFactorBackupCodes\\Event\\CodesGenerated' => __DIR__ . '/..' . '/../lib/Event/CodesGenerated.php',
|
'OCA\\TwoFactorBackupCodes\\Event\\CodesGenerated' => __DIR__ . '/..' . '/../lib/Event/CodesGenerated.php',
|
||||||
'OCA\\TwoFactorBackupCodes\\Listener\\ActivityPublisher' => __DIR__ . '/..' . '/../lib/Listener/ActivityPublisher.php',
|
'OCA\\TwoFactorBackupCodes\\Listener\\ActivityPublisher' => __DIR__ . '/..' . '/../lib/Listener/ActivityPublisher.php',
|
||||||
'OCA\\TwoFactorBackupCodes\\Listener\\IListener' => __DIR__ . '/..' . '/../lib/Listener/IListener.php',
|
'OCA\\TwoFactorBackupCodes\\Listener\\IListener' => __DIR__ . '/..' . '/../lib/Listener/IListener.php',
|
||||||
|
'OCA\\TwoFactorBackupCodes\\Listener\\ProviderEnabled' => __DIR__ . '/..' . '/../lib/Listener/ProviderEnabled.php',
|
||||||
'OCA\\TwoFactorBackupCodes\\Listener\\RegistryUpdater' => __DIR__ . '/..' . '/../lib/Listener/RegistryUpdater.php',
|
'OCA\\TwoFactorBackupCodes\\Listener\\RegistryUpdater' => __DIR__ . '/..' . '/../lib/Listener/RegistryUpdater.php',
|
||||||
'OCA\\TwoFactorBackupCodes\\Migration\\Version1002Date20170607104347' => __DIR__ . '/..' . '/../lib/Migration/Version1002Date20170607104347.php',
|
'OCA\\TwoFactorBackupCodes\\Migration\\Version1002Date20170607104347' => __DIR__ . '/..' . '/../lib/Migration/Version1002Date20170607104347.php',
|
||||||
'OCA\\TwoFactorBackupCodes\\Migration\\Version1002Date20170607113030' => __DIR__ . '/..' . '/../lib/Migration/Version1002Date20170607113030.php',
|
'OCA\\TwoFactorBackupCodes\\Migration\\Version1002Date20170607113030' => __DIR__ . '/..' . '/../lib/Migration/Version1002Date20170607113030.php',
|
||||||
'OCA\\TwoFactorBackupCodes\\Migration\\Version1002Date20170919123342' => __DIR__ . '/..' . '/../lib/Migration/Version1002Date20170919123342.php',
|
'OCA\\TwoFactorBackupCodes\\Migration\\Version1002Date20170919123342' => __DIR__ . '/..' . '/../lib/Migration/Version1002Date20170919123342.php',
|
||||||
'OCA\\TwoFactorBackupCodes\\Migration\\Version1002Date20170926101419' => __DIR__ . '/..' . '/../lib/Migration/Version1002Date20170926101419.php',
|
'OCA\\TwoFactorBackupCodes\\Migration\\Version1002Date20170926101419' => __DIR__ . '/..' . '/../lib/Migration/Version1002Date20170926101419.php',
|
||||||
'OCA\\TwoFactorBackupCodes\\Migration\\Version1002Date20180821043638' => __DIR__ . '/..' . '/../lib/Migration/Version1002Date20180821043638.php',
|
'OCA\\TwoFactorBackupCodes\\Migration\\Version1002Date20180821043638' => __DIR__ . '/..' . '/../lib/Migration/Version1002Date20180821043638.php',
|
||||||
|
'OCA\\TwoFactorBackupCodes\\Notifications\\Notifier' => __DIR__ . '/..' . '/../lib/Notifications/Notifier.php',
|
||||||
'OCA\\TwoFactorBackupCodes\\Provider\\BackupCodesProvider' => __DIR__ . '/..' . '/../lib/Provider/BackupCodesProvider.php',
|
'OCA\\TwoFactorBackupCodes\\Provider\\BackupCodesProvider' => __DIR__ . '/..' . '/../lib/Provider/BackupCodesProvider.php',
|
||||||
'OCA\\TwoFactorBackupCodes\\Service\\BackupCodeStorage' => __DIR__ . '/..' . '/../lib/Service/BackupCodeStorage.php',
|
'OCA\\TwoFactorBackupCodes\\Service\\BackupCodeStorage' => __DIR__ . '/..' . '/../lib/Service/BackupCodeStorage.php',
|
||||||
'OCA\\TwoFactorBackupCodes\\Settings\\Personal' => __DIR__ . '/..' . '/../lib/Settings/Personal.php',
|
'OCA\\TwoFactorBackupCodes\\Settings\\Personal' => __DIR__ . '/..' . '/../lib/Settings/Personal.php',
|
||||||
|
|
|
@ -29,8 +29,14 @@ use OCA\TwoFactorBackupCodes\Db\BackupCodeMapper;
|
||||||
use OCA\TwoFactorBackupCodes\Event\CodesGenerated;
|
use OCA\TwoFactorBackupCodes\Event\CodesGenerated;
|
||||||
use OCA\TwoFactorBackupCodes\Listener\ActivityPublisher;
|
use OCA\TwoFactorBackupCodes\Listener\ActivityPublisher;
|
||||||
use OCA\TwoFactorBackupCodes\Listener\IListener;
|
use OCA\TwoFactorBackupCodes\Listener\IListener;
|
||||||
|
use OCA\TwoFactorBackupCodes\Listener\ProviderEnabled;
|
||||||
use OCA\TwoFactorBackupCodes\Listener\RegistryUpdater;
|
use OCA\TwoFactorBackupCodes\Listener\RegistryUpdater;
|
||||||
|
use OCA\TwoFactorBackupCodes\Notifications\Notifier;
|
||||||
use OCP\AppFramework\App;
|
use OCP\AppFramework\App;
|
||||||
|
use OCP\Authentication\TwoFactorAuth\IRegistry;
|
||||||
|
use OCP\Authentication\TwoFactorAuth\RegistryEvent;
|
||||||
|
use OCP\IL10N;
|
||||||
|
use OCP\Notification\IManager;
|
||||||
use OCP\Util;
|
use OCP\Util;
|
||||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||||
|
|
||||||
|
@ -44,6 +50,7 @@ class Application extends App {
|
||||||
*/
|
*/
|
||||||
public function register() {
|
public function register() {
|
||||||
$this->registerHooksAndEvents();
|
$this->registerHooksAndEvents();
|
||||||
|
$this->registerNotification();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -66,6 +73,27 @@ class Application extends App {
|
||||||
$listener->handle($event);
|
$listener->handle($event);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$eventDispatcher->addListener(IRegistry::EVENT_PROVIDER_ENABLED, function(RegistryEvent $event) use ($container) {
|
||||||
|
/** @var IListener $listener */
|
||||||
|
$listener = $container->query(ProviderEnabled::class);
|
||||||
|
$listener->handle($event);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function registerNotification() {
|
||||||
|
$container = $this->getContainer();
|
||||||
|
/** @var IManager $manager */
|
||||||
|
$manager = $container->query(IManager::class);
|
||||||
|
$manager->registerNotifier(
|
||||||
|
function() use ($container) {
|
||||||
|
return $container->query(Notifier::class);
|
||||||
|
},
|
||||||
|
function () use ($container) {
|
||||||
|
$l = $container->query(IL10N::class);
|
||||||
|
return ['id' => 'twofactor_backupcodes', 'name' => $l->t('Second-factor backup codes')];
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function deleteUser($params) {
|
public function deleteUser($params) {
|
||||||
|
|
|
@ -0,0 +1,92 @@
|
||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
/**
|
||||||
|
* @copyright Copyright (c) 2018, Roeland Jago Douma <roeland@famdouma.nl>
|
||||||
|
*
|
||||||
|
* @author Roeland Jago Douma <roeland@famdouma.nl>
|
||||||
|
*
|
||||||
|
* @license GNU AGPL version 3 or any later version
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace OCA\TwoFactorBackupCodes\BackgroundJob;
|
||||||
|
|
||||||
|
use OC\BackgroundJob\TimedJob;
|
||||||
|
use OCP\AppFramework\Utility\ITimeFactory;
|
||||||
|
use OCP\Authentication\TwoFactorAuth\IRegistry;
|
||||||
|
use OCP\BackgroundJob\IJobList;
|
||||||
|
use OCP\IUserManager;
|
||||||
|
use OCP\Notification\IManager;
|
||||||
|
|
||||||
|
class RememberBackupCodesJob extends TimedJob {
|
||||||
|
|
||||||
|
/** @var IRegistry */
|
||||||
|
private $registry;
|
||||||
|
|
||||||
|
/** @var IUserManager */
|
||||||
|
private $userManager;
|
||||||
|
|
||||||
|
/** @var ITimeFactory */
|
||||||
|
private $time;
|
||||||
|
|
||||||
|
/** @var IManager */
|
||||||
|
private $notificationManager;
|
||||||
|
|
||||||
|
/** @var IJobList */
|
||||||
|
private $jobList;
|
||||||
|
|
||||||
|
public function __construct(IRegistry $registry,
|
||||||
|
IUserManager $userManager,
|
||||||
|
ITimeFactory $timeFactory,
|
||||||
|
IManager $notificationManager,
|
||||||
|
IJobList $jobList) {
|
||||||
|
$this->registry = $registry;
|
||||||
|
$this->userManager = $userManager;
|
||||||
|
$this->time = $timeFactory;
|
||||||
|
$this->notificationManager = $notificationManager;
|
||||||
|
$this->jobList = $jobList;
|
||||||
|
|
||||||
|
$this->setInterval(60*60*24*14);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function run($argument) {
|
||||||
|
$uid = $argument['uid'];
|
||||||
|
$user = $this->userManager->get($uid);
|
||||||
|
|
||||||
|
if ($user === null) {
|
||||||
|
// We can't run with an invalid user
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$providers = $this->registry->getProviderStates($user);
|
||||||
|
if (isset($providers['backup_codes']) && $providers['backup_codes'] === true) {
|
||||||
|
// Backup codes already generated lets remove this job
|
||||||
|
$this->jobList->remove(self::class, $argument);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$date = new \DateTime();
|
||||||
|
$date->setTimestamp($this->time->getTime());
|
||||||
|
|
||||||
|
$notification = $this->notificationManager->createNotification();
|
||||||
|
$notification->setApp('twofactor_backupcodes')
|
||||||
|
->setUser($user->getUID())
|
||||||
|
->setDateTime($date)
|
||||||
|
->setObject('create', 'codes')
|
||||||
|
->setSubject('create_backupcodes');
|
||||||
|
$this->notificationManager->notify($notification);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,61 @@
|
||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
/**
|
||||||
|
* @copyright Copyright (c) 2018, Roeland Jago Douma <roeland@famdouma.nl>
|
||||||
|
*
|
||||||
|
* @author Roeland Jago Douma <roeland@famdouma.nl>
|
||||||
|
*
|
||||||
|
* @license GNU AGPL version 3 or any later version
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace OCA\TwoFactorBackupCodes\Listener;
|
||||||
|
|
||||||
|
use OCA\TwoFactorBackupCodes\BackgroundJob\RememberBackupCodesJob;
|
||||||
|
use OCP\Authentication\TwoFactorAuth\IRegistry;
|
||||||
|
use OCP\Authentication\TwoFactorAuth\RegistryEvent;
|
||||||
|
use OCP\BackgroundJob\IJobList;
|
||||||
|
use Symfony\Component\EventDispatcher\Event;
|
||||||
|
|
||||||
|
class ProviderEnabled implements IListener {
|
||||||
|
|
||||||
|
/** @var IRegistry */
|
||||||
|
private $registry;
|
||||||
|
|
||||||
|
/** @var IJobList */
|
||||||
|
private $jobList;
|
||||||
|
|
||||||
|
public function __construct(IRegistry $registry,
|
||||||
|
IJobList $jobList) {
|
||||||
|
$this->registry = $registry;
|
||||||
|
$this->jobList = $jobList;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function handle(Event $event) {
|
||||||
|
if (!($event instanceof RegistryEvent)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$providers = $this->registry->getProviderStates($event->getUser());
|
||||||
|
if (isset($providers['backup_codes']) && $providers['backup_codes'] === true) {
|
||||||
|
// Backup codes already generated nothing to do here
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->jobList->add(RememberBackupCodesJob::class, ['uid' => $event->getUser()->getUID()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,64 @@
|
||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
/**
|
||||||
|
* @copyright Copyright (c) 2018, Roeland Jago Douma <roeland@famdouma.nl>
|
||||||
|
*
|
||||||
|
* @author Roeland Jago Douma <roeland@famdouma.nl>
|
||||||
|
*
|
||||||
|
* @license GNU AGPL version 3 or any later version
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace OCA\TwoFactorBackupCodes\Notifications;
|
||||||
|
|
||||||
|
use OCP\L10N\IFactory;
|
||||||
|
use OCP\Notification\INotification;
|
||||||
|
use OCP\Notification\INotifier;
|
||||||
|
|
||||||
|
class Notifier implements INotifier {
|
||||||
|
|
||||||
|
/** @var IFactory */
|
||||||
|
private $factory;
|
||||||
|
|
||||||
|
public function __construct(IFactory $factory) {
|
||||||
|
$this->factory = $factory;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function prepare(INotification $notification, $languageCode) {
|
||||||
|
if ($notification->getApp() !== 'twofactor_backupcodes') {
|
||||||
|
// Not my app => throw
|
||||||
|
throw new \InvalidArgumentException();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read the language from the notification
|
||||||
|
$l = $this->factory->get('twofactor_backupcodes', $languageCode);
|
||||||
|
|
||||||
|
switch ($notification->getSubject()) {
|
||||||
|
case 'create_backupcodes':
|
||||||
|
$notification->setParsedSubject(
|
||||||
|
$l->t('Generate backup codes')
|
||||||
|
)->setParsedMessage(
|
||||||
|
$l->t('You have enabled two-factor authentication but have not yet generated backup codes. Be sure to do this in case you lose access to your second factor.')
|
||||||
|
);
|
||||||
|
return $notification;
|
||||||
|
|
||||||
|
default:
|
||||||
|
// Unknown subject => Unknown notification => throw
|
||||||
|
throw new \InvalidArgumentException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,153 @@
|
||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
/**
|
||||||
|
* @copyright Copyright (c) 2018, Roeland Jago Douma <roeland@famdouma.nl>
|
||||||
|
*
|
||||||
|
* @author Roeland Jago Douma <roeland@famdouma.nl>
|
||||||
|
*
|
||||||
|
* @license GNU AGPL version 3 or any later version
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace OCA\TwoFactorBackupCodes\Tests\Unit\BackgroundJob;
|
||||||
|
|
||||||
|
use OCA\TwoFactorBackupCodes\BackgroundJob\RememberBackupCodesJob;
|
||||||
|
use OCP\AppFramework\Utility\ITimeFactory;
|
||||||
|
use OCP\Authentication\TwoFactorAuth\IRegistry;
|
||||||
|
use OCP\BackgroundJob\IJobList;
|
||||||
|
use OCP\IUser;
|
||||||
|
use OCP\IUserManager;
|
||||||
|
use OCP\Notification\IManager;
|
||||||
|
use OCP\Notification\INotification;
|
||||||
|
use Test\TestCase;
|
||||||
|
|
||||||
|
class RememberBackupCodesJobTest extends TestCase {
|
||||||
|
|
||||||
|
/** @var IRegistry|\PHPUnit\Framework\MockObject\MockObject */
|
||||||
|
private $registry;
|
||||||
|
|
||||||
|
/** @var IUserManager|\PHPUnit\Framework\MockObject\MockObject */
|
||||||
|
private $userManager;
|
||||||
|
|
||||||
|
/** @var ITimeFactory|\PHPUnit\Framework\MockObject\MockObject */
|
||||||
|
private $time;
|
||||||
|
|
||||||
|
/** @var IManager|\PHPUnit\Framework\MockObject\MockObject */
|
||||||
|
private $notificationManager;
|
||||||
|
|
||||||
|
/** @var IJobList|\PHPUnit\Framework\MockObject\MockObject */
|
||||||
|
private $jobList;
|
||||||
|
|
||||||
|
/** @var RememberBackupCodesJob */
|
||||||
|
private $job;
|
||||||
|
|
||||||
|
public function setUp() {
|
||||||
|
parent::setUp();
|
||||||
|
|
||||||
|
$this->registry = $this->createMock(IRegistry::class);
|
||||||
|
$this->userManager = $this->createMock(IUserManager::class);
|
||||||
|
$this->time = $this->createMock(ITimeFactory::class);
|
||||||
|
$this->time->method('getTime')
|
||||||
|
->willReturn(10000000);
|
||||||
|
$this->notificationManager = $this->createMock(IManager::class);
|
||||||
|
$this->jobList = $this->createMock(IJobList::class);
|
||||||
|
|
||||||
|
$this->job = new RememberBackupCodesJob(
|
||||||
|
$this->registry,
|
||||||
|
$this->userManager,
|
||||||
|
$this->time,
|
||||||
|
$this->notificationManager,
|
||||||
|
$this->jobList
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testInvalidUID() {
|
||||||
|
$this->userManager->method('get')
|
||||||
|
->with('invalidUID')
|
||||||
|
->willReturn(null);
|
||||||
|
|
||||||
|
$this->notificationManager->expects($this->never())
|
||||||
|
->method($this->anything());
|
||||||
|
$this->jobList->expects($this->never())
|
||||||
|
->method($this->anything());
|
||||||
|
|
||||||
|
$this->invokePrivate($this->job, 'run', [['uid' => 'invalidUID']]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testBackupCodesGenerated() {
|
||||||
|
$user = $this->createMock(IUser::class);
|
||||||
|
$user->method('getUID')
|
||||||
|
->willReturn('validUID');
|
||||||
|
$this->userManager->method('get')
|
||||||
|
->with('validUID')
|
||||||
|
->willReturn($user);
|
||||||
|
|
||||||
|
$this->registry->method('getProviderStates')
|
||||||
|
->with($user)
|
||||||
|
->willReturn([
|
||||||
|
'backup_codes' => true
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->jobList->expects($this->once())
|
||||||
|
->method('remove')
|
||||||
|
->with(
|
||||||
|
RememberBackupCodesJob::class,
|
||||||
|
['uid' => 'validUID']
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->notificationManager->expects($this->never())
|
||||||
|
->method($this->anything());
|
||||||
|
|
||||||
|
$this->invokePrivate($this->job, 'run', [['uid' => 'validUID']]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testNotificationSend() {
|
||||||
|
$user = $this->createMock(IUser::class);
|
||||||
|
$user->method('getUID')
|
||||||
|
->willReturn('validUID');
|
||||||
|
$this->userManager->method('get')
|
||||||
|
->with('validUID')
|
||||||
|
->willReturn($user);
|
||||||
|
|
||||||
|
$this->registry->method('getProviderStates')
|
||||||
|
->with($user)
|
||||||
|
->willReturn([
|
||||||
|
'backup_codes' => false
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->jobList->expects($this->never())
|
||||||
|
->method($this->anything());
|
||||||
|
|
||||||
|
$date = new \DateTime();
|
||||||
|
$date->setTimestamp($this->time->getTime());
|
||||||
|
|
||||||
|
$this->notificationManager->method('createNotification')
|
||||||
|
->willReturn(\OC::$server->query(IManager::class)->createNotification());
|
||||||
|
|
||||||
|
$this->notificationManager->expects($this->once())
|
||||||
|
->method('notify')
|
||||||
|
->with($this->callback(function (INotification $n) {
|
||||||
|
return $n->getApp() === 'twofactor_backupcodes' &&
|
||||||
|
$n->getUser() === 'validUID' &&
|
||||||
|
$n->getDateTime()->getTimestamp() === 10000000 &&
|
||||||
|
$n->getObjectType() === 'create' &&
|
||||||
|
$n->getObjectId() === 'codes' &&
|
||||||
|
$n->getSubject() === 'create_backupcodes';
|
||||||
|
}));
|
||||||
|
|
||||||
|
$this->invokePrivate($this->job, 'run', [['uid' => 'validUID']]);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,107 @@
|
||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
/**
|
||||||
|
* @copyright Copyright (c) 2018, Roeland Jago Douma <roeland@famdouma.nl>
|
||||||
|
*
|
||||||
|
* @author Roeland Jago Douma <roeland@famdouma.nl>
|
||||||
|
*
|
||||||
|
* @license GNU AGPL version 3 or any later version
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace OCA\TwoFactorBackupCodes\Tests\Unit\Listener;
|
||||||
|
|
||||||
|
use OCA\TwoFactorBackupCodes\BackgroundJob\RememberBackupCodesJob;
|
||||||
|
use OCA\TwoFactorBackupCodes\Listener\ProviderEnabled;
|
||||||
|
use OCP\Authentication\TwoFactorAuth\IRegistry;
|
||||||
|
use OCP\Authentication\TwoFactorAuth\RegistryEvent;
|
||||||
|
use OCP\BackgroundJob\IJobList;
|
||||||
|
use OCP\IUser;
|
||||||
|
use Symfony\Component\EventDispatcher\Event;
|
||||||
|
use Test\TestCase;
|
||||||
|
|
||||||
|
class ProviderEnabledTest extends TestCase {
|
||||||
|
|
||||||
|
/** @var IRegistry|\PHPUnit\Framework\MockObject\MockObject */
|
||||||
|
private $registy;
|
||||||
|
|
||||||
|
/** @var IJobList|\PHPUnit\Framework\MockObject\MockObject */
|
||||||
|
private $jobList;
|
||||||
|
|
||||||
|
/** @var ProviderEnabled */
|
||||||
|
private $listener;
|
||||||
|
|
||||||
|
protected function setUp() {
|
||||||
|
parent::setUp();
|
||||||
|
|
||||||
|
$this->registy = $this->createMock(IRegistry::class);
|
||||||
|
$this->jobList = $this->createMock(IJobList::class);
|
||||||
|
|
||||||
|
$this->listener = new ProviderEnabled($this->registy, $this->jobList);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testHandleGenericEvent() {
|
||||||
|
$event = $this->createMock(Event::class);
|
||||||
|
$this->jobList->expects($this->never())
|
||||||
|
->method($this->anything());
|
||||||
|
|
||||||
|
$this->listener->handle($event);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testHandleCodesGeneratedEventAlraedyBackupcodes() {
|
||||||
|
$user = $this->createMock(IUser::class);
|
||||||
|
$user->method('getUID')
|
||||||
|
->willReturn('myUID');
|
||||||
|
$event = $this->createMock(RegistryEvent::class);
|
||||||
|
$event->method('getUser')
|
||||||
|
->willReturn($user);
|
||||||
|
|
||||||
|
$this->registy->method('getProviderStates')
|
||||||
|
->with($user)
|
||||||
|
->willReturn([
|
||||||
|
'backup_codes' => true,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->jobList->expects($this->never())
|
||||||
|
->method($this->anything());
|
||||||
|
|
||||||
|
$this->listener->handle($event);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testHandleCodesGeneratedEventNoBackupcodes() {
|
||||||
|
$user = $this->createMock(IUser::class);
|
||||||
|
$user->method('getUID')
|
||||||
|
->willReturn('myUID');
|
||||||
|
$event = $this->createMock(RegistryEvent::class);
|
||||||
|
$event->method('getUser')
|
||||||
|
->willReturn($user);
|
||||||
|
|
||||||
|
$this->registy->method('getProviderStates')
|
||||||
|
->with($user)
|
||||||
|
->willReturn([
|
||||||
|
'backup_codes' => false,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->jobList->expects($this->once())
|
||||||
|
->method('add')
|
||||||
|
->with(
|
||||||
|
$this->equalTo(RememberBackupCodesJob::class),
|
||||||
|
$this->equalTo(['uid' => 'myUID'])
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->listener->handle($event);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,120 @@
|
||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
/**
|
||||||
|
* @copyright Copyright (c) 2018, Roeland Jago Douma <roeland@famdouma.nl>
|
||||||
|
*
|
||||||
|
* @author Roeland Jago Douma <roeland@famdouma.nl>
|
||||||
|
*
|
||||||
|
* @license GNU AGPL version 3 or any later version
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace OCA\TwoFactorBackupCodes\Tests\Unit\Notification;
|
||||||
|
|
||||||
|
use OCA\TwoFactorBackupCodes\Notifications\Notifier;
|
||||||
|
use OCP\IL10N;
|
||||||
|
use OCP\L10N\IFactory;
|
||||||
|
use OCP\Notification\INotification;
|
||||||
|
use Test\TestCase;
|
||||||
|
|
||||||
|
class NotifierTest extends TestCase {
|
||||||
|
/** @var Notifier */
|
||||||
|
protected $notifier;
|
||||||
|
|
||||||
|
/** @var IFactory|\PHPUnit\Framework\MockObject\MockObject */
|
||||||
|
protected $factory;
|
||||||
|
/** @var IL10N|\PHPUnit\Framework\MockObject\MockObject */
|
||||||
|
protected $l;
|
||||||
|
|
||||||
|
protected function setUp() {
|
||||||
|
parent::setUp();
|
||||||
|
|
||||||
|
$this->l = $this->createMock(IL10N::class);
|
||||||
|
$this->l->expects($this->any())
|
||||||
|
->method('t')
|
||||||
|
->willReturnCallback(function($string, $args) {
|
||||||
|
return vsprintf($string, $args);
|
||||||
|
});
|
||||||
|
$this->factory = $this->createMock(IFactory::class);
|
||||||
|
$this->factory->expects($this->any())
|
||||||
|
->method('get')
|
||||||
|
->willReturn($this->l);
|
||||||
|
|
||||||
|
$this->notifier = new Notifier(
|
||||||
|
$this->factory
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @expectedException \InvalidArgumentException
|
||||||
|
*/
|
||||||
|
public function testPrepareWrongApp() {
|
||||||
|
/** @var INotification|\PHPUnit_Framework_MockObject_MockObject $notification */
|
||||||
|
$notification = $this->createMock(INotification::class);
|
||||||
|
$notification->expects($this->once())
|
||||||
|
->method('getApp')
|
||||||
|
->willReturn('notifications');
|
||||||
|
$notification->expects($this->never())
|
||||||
|
->method('getSubject');
|
||||||
|
|
||||||
|
$this->notifier->prepare($notification, 'en');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @expectedException \InvalidArgumentException
|
||||||
|
*/
|
||||||
|
public function testPrepareWrongSubject() {
|
||||||
|
/** @var INotification|\PHPUnit_Framework_MockObject_MockObject $notification */
|
||||||
|
$notification = $this->createMock(INotification::class);
|
||||||
|
$notification->expects($this->once())
|
||||||
|
->method('getApp')
|
||||||
|
->willReturn('twofactor_backupcodes');
|
||||||
|
$notification->expects($this->once())
|
||||||
|
->method('getSubject')
|
||||||
|
->willReturn('wrong subject');
|
||||||
|
|
||||||
|
$this->notifier->prepare($notification, 'en');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testPrepare() {
|
||||||
|
/** @var \OCP\Notification\INotification|\PHPUnit_Framework_MockObject_MockObject $notification */
|
||||||
|
$notification = $this->createMock(INotification::class);
|
||||||
|
|
||||||
|
$notification->expects($this->once())
|
||||||
|
->method('getApp')
|
||||||
|
->willReturn('twofactor_backupcodes');
|
||||||
|
$notification->expects($this->once())
|
||||||
|
->method('getSubject')
|
||||||
|
->willReturn('create_backupcodes');
|
||||||
|
|
||||||
|
$this->factory->expects($this->once())
|
||||||
|
->method('get')
|
||||||
|
->with('twofactor_backupcodes', 'nl')
|
||||||
|
->willReturn($this->l);
|
||||||
|
|
||||||
|
$notification->expects($this->once())
|
||||||
|
->method('setParsedSubject')
|
||||||
|
->with('Generate backup codes')
|
||||||
|
->willReturnSelf();
|
||||||
|
$notification->expects($this->once())
|
||||||
|
->method('setParsedMessage')
|
||||||
|
->with('You have enabled two-factor authentication but have not yet generated backup codes. Be sure to do this in case you lose access to your second factor.')
|
||||||
|
->willReturnSelf();
|
||||||
|
|
||||||
|
$return = $this->notifier->prepare($notification, 'nl');
|
||||||
|
$this->assertEquals($notification, $return);
|
||||||
|
}
|
||||||
|
}
|
|
@ -27,8 +27,11 @@ namespace Test\Authentication\TwoFactorAuth;
|
||||||
use OC\Authentication\TwoFactorAuth\Db\ProviderUserAssignmentDao;
|
use OC\Authentication\TwoFactorAuth\Db\ProviderUserAssignmentDao;
|
||||||
use OC\Authentication\TwoFactorAuth\Registry;
|
use OC\Authentication\TwoFactorAuth\Registry;
|
||||||
use OCP\Authentication\TwoFactorAuth\IProvider;
|
use OCP\Authentication\TwoFactorAuth\IProvider;
|
||||||
|
use OCP\Authentication\TwoFactorAuth\IRegistry;
|
||||||
|
use OCP\Authentication\TwoFactorAuth\RegistryEvent;
|
||||||
use OCP\IUser;
|
use OCP\IUser;
|
||||||
use PHPUnit_Framework_MockObject_MockObject;
|
use PHPUnit_Framework_MockObject_MockObject;
|
||||||
|
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||||
use Test\TestCase;
|
use Test\TestCase;
|
||||||
|
|
||||||
class RegistryTest extends TestCase {
|
class RegistryTest extends TestCase {
|
||||||
|
@ -39,12 +42,16 @@ class RegistryTest extends TestCase {
|
||||||
/** @var Registry */
|
/** @var Registry */
|
||||||
private $registry;
|
private $registry;
|
||||||
|
|
||||||
|
/** @var EventDispatcherInterface|\PHPUnit_Framework_MockObject_MockObject */
|
||||||
|
private $dispatcher;
|
||||||
|
|
||||||
protected function setUp() {
|
protected function setUp() {
|
||||||
parent::setUp();
|
parent::setUp();
|
||||||
|
|
||||||
$this->dao = $this->createMock(ProviderUserAssignmentDao::class);
|
$this->dao = $this->createMock(ProviderUserAssignmentDao::class);
|
||||||
|
$this->dispatcher = $this->createMock(EventDispatcherInterface::class);
|
||||||
|
|
||||||
$this->registry = new Registry($this->dao);
|
$this->registry = new Registry($this->dao, $this->dispatcher);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testGetProviderStates() {
|
public function testGetProviderStates() {
|
||||||
|
@ -68,6 +75,15 @@ class RegistryTest extends TestCase {
|
||||||
$this->dao->expects($this->once())->method('persist')->with('p1', 'user123',
|
$this->dao->expects($this->once())->method('persist')->with('p1', 'user123',
|
||||||
true);
|
true);
|
||||||
|
|
||||||
|
$this->dispatcher->expects($this->once())
|
||||||
|
->method('dispatch')
|
||||||
|
->with(
|
||||||
|
$this->equalTo(IRegistry::EVENT_PROVIDER_ENABLED),
|
||||||
|
$this->callback(function(RegistryEvent $e) use ($user, $provider) {
|
||||||
|
return $e->getUser() === $user && $e->getProvider() === $provider;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
$this->registry->enableProviderFor($provider, $user);
|
$this->registry->enableProviderFor($provider, $user);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -79,6 +95,16 @@ class RegistryTest extends TestCase {
|
||||||
$this->dao->expects($this->once())->method('persist')->with('p1', 'user123',
|
$this->dao->expects($this->once())->method('persist')->with('p1', 'user123',
|
||||||
false);
|
false);
|
||||||
|
|
||||||
|
|
||||||
|
$this->dispatcher->expects($this->once())
|
||||||
|
->method('dispatch')
|
||||||
|
->with(
|
||||||
|
$this->equalTo(IRegistry::EVENT_PROVIDER_DISABLED),
|
||||||
|
$this->callback(function(RegistryEvent $e) use ($user, $provider) {
|
||||||
|
return $e->getUser() === $user && $e->getProvider() === $provider;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
$this->registry->disableProviderFor($provider, $user);
|
$this->registry->disableProviderFor($provider, $user);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue