diff --git a/appinfo/app.php b/appinfo/app.php
index c65a0cd5..c66afec1 100755
--- a/appinfo/app.php
+++ b/appinfo/app.php
@@ -61,3 +61,16 @@
\OCP\IUser::class . '::firstLogin',
[$handler, 'checkForcePasswordChangeOnFirstLogin']
);
+
+$app = new \OCA\PasswordPolicy\AppInfo\Application();
+$app->registerNotifier();
+
+// only load notification JS code in the logged in layout page (not public links not login page)
+$request = \OC::$server->getRequest();
+if (\OC::$server->getUserSession() !== null && \OC::$server->getUserSession()->getUser() !== null
+ && substr($request->getScriptName(), 0 - strlen('/index.php')) === '/index.php'
+ && substr($request->getPathInfo(), 0, strlen('/s/')) !== '/s/'
+ && substr($request->getPathInfo(), 0, strlen('/login')) !== '/login') {
+
+ \OCP\Util::addScript('password_policy', 'notification');
+}
diff --git a/appinfo/info.xml b/appinfo/info.xml
index e4f355b7..993a9a41 100644
--- a/appinfo/info.xml
+++ b/appinfo/info.xml
@@ -17,7 +17,7 @@ Administrators find the configuration options in the 'Security' section of the o
https://doc.owncloud.com/server/10.0/admin_manual/configuration/server/security/password_policy.html
-
+ PasswordPolicy
@@ -30,6 +30,9 @@ Administrators find the configuration options in the 'Security' section of the o
OCA\PasswordPolicy\Authentication\AccountModule
+
+ OCA\PasswordPolicy\Jobs\PasswordExpirationNotifierJob
+ https://raw.githubusercontent.com/owncloud/screenshots/master/password_policy/owncloud-app-password_policy.jpghttps://raw.githubusercontent.com/owncloud/screenshots/master/password_policy/owncloud-app-password_policy2.jpg
diff --git a/js/notification.js b/js/notification.js
new file mode 100644
index 00000000..d0748318
--- /dev/null
+++ b/js/notification.js
@@ -0,0 +1,25 @@
+/*
+ * Copyright (c) 2018 Vincent Petry
+ *
+ * This file is licensed under the Affero General Public License version 3
+ * or later.
+ *
+ * See the COPYING-README file.
+ *
+ */
+(function () {
+
+ $(document).ready(function() {
+ // convert action URL to redirect
+ $('body').on('OCA.Notification.Action', function(e) {
+ if (e.notification.app === 'password_policy'
+ && (e.notification.object_type === 'about_to_expire' || e.notification.object_type === 'expired')
+ && e.action.type === 'GET'
+ ) {
+ OC.redirect(e.notification.link);
+ return false;
+ }
+ });
+ });
+})();
+
diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php
index 2afdb2e0..c47a56bc 100644
--- a/lib/AppInfo/Application.php
+++ b/lib/AppInfo/Application.php
@@ -21,6 +21,8 @@
namespace OCA\PasswordPolicy\AppInfo;
use OCP\AppFramework\App;
+use OCP\Notification\Events\RegisterNotifierEvent;
+use OCA\PasswordPolicy\Notifier;
class Application extends App {
@@ -28,4 +30,15 @@ public function __construct (array $urlParams = []) {
parent::__construct('password_policy', $urlParams);
}
+ /**
+ * Registers the notifier
+ */
+ public function registerNotifier() {
+ $container = $this->getContainer();
+ $dispatcher = $container->getServer()->getEventDispatcher();
+ $dispatcher->addListener(RegisterNotifierEvent::NAME, function (RegisterNotifierEvent $event) use ($container) {
+ $l10n = $container->getServer()->getL10N('password_policy');
+ $event->registerNotifier($container->query(Notifier::class), 'password_policy', $l10n->t('Password Policy'));
+ });
+ }
}
diff --git a/lib/Controller/PasswordController.php b/lib/Controller/PasswordController.php
index 70690007..fdee56c3 100644
--- a/lib/Controller/PasswordController.php
+++ b/lib/Controller/PasswordController.php
@@ -141,7 +141,7 @@ public function update($current_password, $new_password, $confirm_password, $red
if ($new_password !== $confirm_password) {
return $this->createPasswordTemplateResponse(
$redirect_url,
- $this->l10n->t('New passwords are not the same.')
+ $this->l10n->t('Password confirmation does not match the password.')
);
}
@@ -150,7 +150,7 @@ public function update($current_password, $new_password, $confirm_password, $red
if(!$this->userManager->checkPassword($user->getUID(), $current_password)) {
return $this->createPasswordTemplateResponse(
$redirect_url,
- $this->l10n->t('Incorrect current password supplied.')
+ $this->l10n->t('The current password is incorrect.')
);
}
diff --git a/lib/Controller/SettingsController.php b/lib/Controller/SettingsController.php
index 26f0ee3f..86a3e212 100644
--- a/lib/Controller/SettingsController.php
+++ b/lib/Controller/SettingsController.php
@@ -26,6 +26,7 @@
use OCP\IRequest;
use OCP\Settings\ISettings;
use OCP\Template;
+use OCA\PasswordPolicy\UserNotificationConfigHandler;
class SettingsController extends Controller implements ISettings {
@@ -49,6 +50,8 @@ class SettingsController extends Controller implements ISettings {
'spv_password_history_value' => 3,
'spv_user_password_expiration_checked' => false,
'spv_user_password_expiration_value' => 90,
+ 'spv_user_password_expiration_notification_checked' => false,
+ 'spv_user_password_expiration_notification_value' => UserNotificationConfigHandler::DEFAULT_EXPIRATION_FOR_NORMAL_NOTIFICATION,
'spv_user_password_force_change_on_first_login_checked' => false,
'spv_expiration_password_checked' => false,
'spv_expiration_password_value' => 7,
@@ -56,6 +59,17 @@ class SettingsController extends Controller implements ISettings {
'spv_expiration_nopassword_value' => 7,
];
+ /**
+ * functions to convert values between what is shown and what is stored
+ * these functions must be defined in this class, they're per config key
+ */
+ const CONVERSIONS = [
+ 'spv_user_password_expiration_notification_value' => [
+ 'in' => 'daysToSeconds',
+ 'out' => 'secondsToDays',
+ ],
+ ];
+
public function __construct($appName,
IRequest $request,
IConfig $config) {
@@ -71,6 +85,10 @@ public function updatePolicy() {
if ($this->request->getParam($key) !== null) {
if ($key !== 'spv_def_special_chars_value' && \substr($key, -6) === '_value') {
$value = \min(\max(0, (int)$this->request->getParam($key)), 255);
+ if (isset(self::CONVERSIONS[$key]['in'])) {
+ $convertFuncName = self::CONVERSIONS[$key]['in'];
+ $value = $this->$convertFuncName($value);
+ }
$this->config->setAppValue('password_policy', $key, $value);
} else {
$this->config->setAppValue('password_policy', $key, \strip_tags($this->request->getParam($key)));
@@ -92,9 +110,33 @@ public function getPriority() {
public function getPanel() {
$template = new Template('password_policy', 'admin');
foreach(self::DEFAULTS as $key => $default) {
- $template->assign($key, $this->config->getAppValue('password_policy', $key, $default));
+ $value = $this->config->getAppValue('password_policy', $key, $default);
+ if (isset(self::CONVERSIONS[$key]['out'])) {
+ $convertFuncName = self::CONVERSIONS[$key]['out'];
+ $value = $this->$convertFuncName($value);
+ }
+ $template->assign($key, $value);
}
return $template;
}
+ /**
+ * Convert the days to seconds
+ * @param int $days
+ * @return int the number of seconds
+ */
+ private function daysToSeconds($days) {
+ return $days * 24 * 60 * 60;
+ }
+
+ /**
+ * Convert seconds to days. The value will always be rounded up,
+ * so 1 second will be converted to 1 day
+ * @param int $seconds the number of seconds to be converted
+ * @return int the number of days in those seconds, rounded up
+ */
+ private function secondsToDays($seconds) {
+ $floatDays = $seconds / (24 * 60 * 60);
+ return \intval(\ceil($floatDays));
+ }
}
diff --git a/lib/Db/OldPasswordMapper.php b/lib/Db/OldPasswordMapper.php
index 219a9c76..2a20bce6 100644
--- a/lib/Db/OldPasswordMapper.php
+++ b/lib/Db/OldPasswordMapper.php
@@ -63,4 +63,36 @@ public function getLatestPassword($uid) {
}
return $passwords[0];
}
-}
\ No newline at end of file
+
+ /**
+ * Get the passwords that are about to expire or already expired.
+ * Last passwords which have been changed before the timestamp are the ones
+ * selectable. Previous stored passwords won't be included
+ * In addition, passwords from multiple users are expected
+ * @param int $maxTimestamp timestamp marker, last passwords changed before
+ * the timestamp will be selected
+ * @return Generator to traverse the selected passwords
+ */
+ public function getPasswordsAboutToExpire($maxTimestamp) {
+ $query = "SELECT `f`.`id`, `f`.`uid`, `f`.`password`, `f`.`change_time` FROM (";
+ $query .= "SELECT `uid`, max(`change_time`) AS `maxtime` FROM `*PREFIX*user_password_history` GROUP BY `uid`";
+ $query .= ") AS `x` INNER JOIN `*PREFIX*user_password_history` AS `f` ON `f`.`uid` = `x`.`uid` AND `f`.`change_time` = `x`.`maxtime`";
+ $query .= " WHERE `f`.`change_time` < ?";
+
+ $stmt = $this->db->prepare($query);
+ $stmt->bindValue(1, $maxTimestamp);
+ $result = $stmt->execute();
+
+ if ($result === false) {
+ $info = \json_encode($stmt->erroInfo());
+ $message = "Cannot get the passwords that are about to expire. Error: {$info}";
+ \OCP\Util::writeLog('password_policy', $message, \OCP\Util::ERROR);
+ return;
+ }
+
+ while ($row = $stmt->fetch()) {
+ yield OldPassword::fromRow($row);
+ }
+ $stmt->closeCursor();
+ }
+}
diff --git a/lib/HooksHandler.php b/lib/HooksHandler.php
index c79237b3..dd421f8b 100644
--- a/lib/HooksHandler.php
+++ b/lib/HooksHandler.php
@@ -26,12 +26,14 @@
use OCA\PasswordPolicy\Db\OldPasswordMapper;
use OCA\PasswordPolicy\Rules\PasswordExpired;
use OCA\PasswordPolicy\Rules\PolicyException;
+use OCA\PasswordPolicy\UserNotificationConfigHandler;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\IConfig;
use OCP\IL10N;
use OCP\ISession;
use OCP\IUser;
use OCP\Security\IHasher;
+use OCP\Notification\IManager;
use Symfony\Component\EventDispatcher\GenericEvent;
class HooksHandler {
@@ -60,6 +62,12 @@ class HooksHandler {
/** @var ISession */
private $session;
+ /** @var IManager */
+ private $notificationManager;
+
+ /** @var UserNotificationConfigHandler */
+ private $userNotificationConfigHandler;
+
public function __construct(
IConfig $config = null,
Engine $engine = null,
@@ -68,7 +76,9 @@ public function __construct(
IL10N $l10n = null,
PasswordExpired $passwordExpiredRule = null,
OldPasswordMapper $oldPasswordMapper = null,
- ISession $session = null
+ ISession $session = null,
+ IManager $notificationManager = null,
+ UserNotificationConfigHandler $userNotificationConfigHandler = null
) {
$this->config = $config;
$this->engine = $engine;
@@ -78,6 +88,8 @@ public function __construct(
$this->passwordExpiredRule = $passwordExpiredRule;
$this->oldPasswordMapper = $oldPasswordMapper;
$this->session = $session;
+ $this->notificationManager = $notificationManager;
+ $this->userNotificationConfigHandler = $userNotificationConfigHandler;
}
private function fixDI() {
@@ -103,6 +115,8 @@ private function fixDI() {
$this->hasher
);
$this->session = \OC::$server->getSession();
+ $this->notificationManager = \OC::$server->getNotificationManager();
+ $this->userNotificationConfigHandler = new UserNotificationConfigHandler($this->config);
}
}
@@ -196,11 +210,34 @@ public function saveOldPassword(GenericEvent $event) {
$user = $this->getUser($event);
$password = $event->getArgument('password');
+ $userId = $user->getUID();
+
$oldPassword = new OldPassword();
- $oldPassword->setUid($user->getUID());
+ $oldPassword->setUid($userId);
$oldPassword->setPassword($this->hasher->hash($password));
$oldPassword->setChangeTime($this->timeFactory->getTime());
$this->oldPasswordMapper->insert($oldPassword);
+
+ // get previous marks
+ $aboutToExpireMark = $this->userNotificationConfigHandler->getMarkAboutToExpireNotificationSentFor($userId);
+ $expiredMark = $this->userNotificationConfigHandler->getMarkExpiredNotificationSentFor($userId);
+
+ $this->userNotificationConfigHandler->resetExpirationMarks($userId);
+
+ if ($aboutToExpireMark !== null) {
+ $notification = $this->notificationManager->createNotification();
+ $notification->setApp('password_policy')
+ ->setUser($userId)
+ ->setObject('about_to_expire', $aboutToExpireMark);
+ $this->notificationManager->markProcessed($notification);
+ }
+ if ($expiredMark !== null) {
+ $notification = $this->notificationManager->createNotification();
+ $notification->setApp('password_policy')
+ ->setUser($userId)
+ ->setObject('expired', $expiredMark);
+ $this->notificationManager->markProcessed($notification);
+ }
}
public function savePasswordForCreatedUser(GenericEvent $event) {
@@ -214,6 +251,7 @@ public function savePasswordForCreatedUser(GenericEvent $event) {
$oldPassword->setPassword($this->hasher->hash($password));
$oldPassword->setChangeTime($this->timeFactory->getTime());
$this->oldPasswordMapper->insert($oldPassword);
+ $this->userNotificationConfigHandler->resetExpirationMarks($userid);
}
/**
diff --git a/lib/Jobs/PasswordExpirationNotifierJob.php b/lib/Jobs/PasswordExpirationNotifierJob.php
new file mode 100644
index 00000000..bec4110e
--- /dev/null
+++ b/lib/Jobs/PasswordExpirationNotifierJob.php
@@ -0,0 +1,188 @@
+
+ * @copyright Copyright (c) 2018, ownCloud GmbH
+ * @license GPL-2.0
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+namespace OCA\PasswordPolicy\Jobs;
+
+use OC\BackgroundJob\TimedJob;
+use OCP\Notification\IManager;
+use OCP\IURLGenerator;
+use OCP\ILogger;
+use OCP\AppFramework\Utility\ITimeFactory;
+use OCA\PasswordPolicy\Db\OldPasswordMapper;
+use OCA\PasswordPolicy\Db\OldPassword;
+use OCA\PasswordPolicy\UserNotificationConfigHandler;
+
+class PasswordExpirationNotifierJob extends TimedJob {
+ /** @var OldPasswordMapper */
+ private $mapper;
+
+ /** @var $manager */
+ private $manager;
+
+ /** @var UserNotificationConfigHandler */
+ private $unConfigHandler;
+
+ /** @var ITimeFactory */
+ private $timeFactory;
+
+ /** @var IURLGenerator */
+ private $urlGenerator;
+
+ /** @var ILogger */
+ private $logger;
+
+ public function __construct(
+ OldPasswordMapper $mapper,
+ IManager $manager,
+ UserNotificationConfigHandler $unConfigHandler,
+ ITimeFactory $timeFactory,
+ IURLGenerator $urlGenerator,
+ ILogger $logger
+ ) {
+ $this->mapper = $mapper;
+ $this->manager = $manager;
+ $this->unConfigHandler = $unConfigHandler;
+ $this->timeFactory = $timeFactory;
+ $this->urlGenerator = $urlGenerator;
+ $this->logger = $logger;
+
+ $this->setInterval(12 * 60 * 60);
+ }
+
+ protected function run($arguments) {
+ $expirationTime = $this->unConfigHandler->getExpirationTime();
+ if ($expirationTime === null) {
+ return; // expiration not configured
+ }
+
+ $expirationTimeNotification = $this->unConfigHandler->getExpirationTimeForNormalNotification();
+ if ($expirationTimeNotification === null) {
+ $expirationTimeNotification = 0;
+ }
+
+ // ensure ranges are correct
+ if ($expirationTime <= $expirationTimeNotification) {
+ $message = "wrong expiration range: normal ($expirationTimeNotification) < expired ($expirationTime)";
+ $this->logger->warning($message, ['app' => 'password_policy']);
+ return;
+ }
+
+ $notifyAfter = $expirationTime - $expirationTimeNotification;
+
+ $currentTime = $this->timeFactory->getTime();
+
+ $maxTimestamp = $currentTime - $notifyAfter;
+ // passwords changed before $maxTimestamp are expired or about to be expired
+ // according to the expiration time range
+
+ $oldPasswordsAboutToExpire = $this->mapper->getPasswordsAboutToExpire($maxTimestamp);
+ foreach ($oldPasswordsAboutToExpire as $passInfo) {
+ $elapsedTime = $currentTime - $passInfo->getChangeTime();
+ if ($elapsedTime >= $expirationTime) {
+ $this->logger->debug("password timestamp for {$passInfo->getUid()}: {$passInfo->getChangeTime()}; elapsed time: {$elapsedTime} -> EXPIRED", ['app' => 'password_policy']);
+ $this->sendPassExpiredNotification($passInfo, $expirationTime);
+ } elseif ($elapsedTime >= $notifyAfter) {
+ $this->logger->debug("password timestamp for {$passInfo->getUid()}: {$passInfo->getChangeTime()}; elapsed time: {$elapsedTime} -> NOTIFY", ['app' => 'password_policy']);
+ $this->sendAboutToExpireNotification($passInfo, $expirationTime);
+ }
+ }
+ }
+
+ /**
+ * Send an "about to expire" notification using the password information
+ * in $passInfo. The password should expire after $expirationTime (90 days
+ * by default). This information will also be used in the notification
+ * @param OldPassword $passInfo the password information used to send the
+ * notification
+ * @param int $expirationTime the time to expire the password in seconds
+ * (for example, 90 days - in seconds)
+ */
+ private function sendAboutToExpireNotification(OldPassword $passInfo, $expirationTime) {
+ if ($this->unConfigHandler->isSentAboutToExpireNotification($passInfo)) {
+ return; // notification already sent
+ }
+
+ $notificationTimestamp = $this->timeFactory->getTime();
+
+ // we'll use the id of the passInfo as object id and marker
+ $notification = $this->manager->createNotification();
+ $notification->setApp('password_policy')
+ ->setUser($passInfo->getUid())
+ ->setDateTime(new \DateTime("@{$notificationTimestamp}"))
+ ->setObject('about_to_expire', $passInfo->getId())
+ ->setSubject('about_to_expire', [$passInfo->getChangeTime(), $expirationTime])
+ ->setMessage('about_to_expire', [$passInfo->getChangeTime(), $expirationTime])
+ ->setLink($this->getNotificationLink());
+
+ $linkAction = $notification->createAction();
+ $linkAction->setLabel('Change password')
+ ->setLink($this->getNotificationLink(), 'GET');
+ $notification->addAction($linkAction);
+
+ $this->manager->notify($notification);
+
+ $this->unConfigHandler->markAboutToExpireNotificationSentFor($passInfo);
+ }
+
+ /**
+ * Send an "expired" notification using the password information
+ * in $passInfo. The password should expire after $expirationTime (90 days
+ * by default). This information will also be used in the notification
+ * @param OldPassword $passInfo the password information used to send the
+ * notification
+ * @param int $expirationTime the time to expire the password in seconds
+ * (for example, 90 days - in seconds)
+ */
+ private function sendPassExpiredNotification(OldPassword $passInfo, $expirationTime) {
+ if ($this->unConfigHandler->isSentExpiredNotification($passInfo)) {
+ return; // notification already sent
+ }
+
+ $notificationTimestamp = $this->timeFactory->getTime();
+
+ // we'll use the id of the passInfo as object id and marker
+ $notification = $this->manager->createNotification();
+ $notification->setApp('password_policy')
+ ->setUser($passInfo->getUid())
+ ->setDateTime(new \DateTime("@{$notificationTimestamp}"))
+ ->setObject('expired', $passInfo->getId())
+ ->setSubject('expired', [$passInfo->getChangeTime(), $expirationTime])
+ ->setMessage('expired', [$passInfo->getChangeTime(), $expirationTime])
+ ->setLink($this->getNotificationLink());
+
+ $linkAction = $notification->createAction();
+ $linkAction->setLabel('Change password')
+ ->setLink($this->getNotificationLink(), 'GET');
+ $notification->addAction($linkAction);
+
+ $this->manager->notify($notification);
+
+ $this->unConfigHandler->markExpiredNotificationSentFor($passInfo);
+ }
+
+ private function getNotificationLink() {
+ return $this->urlGenerator->linkToRouteAbsolute(
+ 'settings.SettingsPage.getPersonal',
+ ['sectionid' => 'general']
+ );
+ }
+}
diff --git a/lib/Notifier.php b/lib/Notifier.php
new file mode 100644
index 00000000..d21aa6d1
--- /dev/null
+++ b/lib/Notifier.php
@@ -0,0 +1,132 @@
+
+ * @copyright Copyright (c) 2018, ownCloud GmbH
+ * @license GPL-2.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\PasswordPolicy;
+
+use OCP\Notification\INotification;
+use OCP\Notification\INotifier;
+use OCP\Notification\IManager as INotificationManager;
+use OCP\AppFramework\Utility\ITimeFactory;
+use OCP\L10N\IFactory;
+use OCP\IL10N;
+
+class Notifier implements INotifier {
+ /** @var IFactory */
+ protected $factory;
+
+ /** @var ITimeFactory */
+ protected $timeFactory;
+ /**
+ * @param \OCP\L10N\IFactory $factory
+ */
+ public function __construct(
+ IFactory $factory,
+ ITimeFactory $timeFactory
+ ) {
+ $this->factory = $factory;
+ $this->timeFactory = $timeFactory;
+ }
+ /**
+ * @param INotification $notification
+ * @param string $languageCode The code of the language that should be used to prepare the notification
+ * @return INotification
+ */
+ public function prepare(INotification $notification, $languageCode) {
+ if ($notification->getApp() !== 'password_policy') {
+ throw new \InvalidArgumentException();
+ }
+ // Read the language from the notification
+ $l = $this->factory->get('password_policy', $languageCode);
+ switch ($notification->getObjectType()) {
+ case 'about_to_expire':
+ return $this->formatAboutToExpire($notification, $l);
+ case 'expired':
+ return $this->formatExpired($notification, $l);
+ default:
+ throw new \InvalidArgumentException();
+ }
+ }
+
+ private function formatAboutToExpire(INotification $notification, IL10N $l) {
+ $params = $notification->getSubjectParameters();
+ $notification->setParsedSubject(
+ (string) $l->t('Password expiration notice', $params)
+ );
+
+ $messageParams = $notification->getMessageParameters();
+ $currentTime = $this->timeFactory->getTime();
+ $currentDateTime = new \DateTime("@{$currentTime}");
+ $passwordTime = $messageParams[0];
+ $expirationTime = $messageParams[1];
+ $targetExpirationTime = $passwordTime + $expirationTime;
+ $expirationDateTime = new \DateTime("@{$targetExpirationTime}");
+ $interval = $currentDateTime->diff($expirationDateTime);
+
+ if ($interval->invert) {
+ $notification->setParsedMessage(
+ (string) $l->t('Your password expired %1$s days ago', [$interval->days])
+ );
+ } else {
+ $notification->setParsedMessage(
+ (string) $l->t('You have %1$s days to change your password', [$interval->days])
+ );
+ }
+
+ foreach ($notification->getActions() as $action) {
+ switch ($action->getLabel()) {
+ case 'Change password':
+ $action->setParsedLabel(
+ (string) $l->t('Change Password')
+ );
+ break;
+ }
+
+ $notification->addParsedAction($action);
+ }
+
+ return $notification;
+ }
+
+ private function formatExpired(INotification $notification, IL10N $l) {
+ $params = $notification->getSubjectParameters();
+ $notification->setParsedSubject(
+ (string) $l->t('Your password has expired', $params)
+ );
+
+ $messageParams = $notification->getMessageParameters();
+
+ $notification->setParsedMessage(
+ (string) $l->t('Please change your password to gain back access to your account', $messageParams)
+ );
+
+ foreach ($notification->getActions() as $action) {
+ switch ($action->getLabel()) {
+ case 'Change password':
+ $action->setParsedLabel(
+ (string) $l->t('Change Password')
+ );
+ break;
+ }
+
+ $notification->addParsedAction($action);
+ }
+
+ return $notification;
+ }
+}
diff --git a/lib/Rules/PasswordHistory.php b/lib/Rules/PasswordHistory.php
index c135d276..67f3d19b 100644
--- a/lib/Rules/PasswordHistory.php
+++ b/lib/Rules/PasswordHistory.php
@@ -65,7 +65,7 @@ public function verify($password, $val, $uid) {
foreach($oldPasswords as $oldPassword) {
if ($this->hasher->verify($password, $oldPassword->getPassword())) {
throw new PolicyException(
- $this->l10n->t('The password must be different to your previous %d passwords.', [$val]));
+ $this->l10n->t('The password must be different than your previous %d passwords.', [$val]));
}
}
}
diff --git a/lib/UserNotificationConfigHandler.php b/lib/UserNotificationConfigHandler.php
new file mode 100644
index 00000000..368e4742
--- /dev/null
+++ b/lib/UserNotificationConfigHandler.php
@@ -0,0 +1,183 @@
+
+ * @copyright Copyright (c) 2018, ownCloud GmbH
+ * @license GPL-2.0
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+namespace OCA\PasswordPolicy;
+
+use OCP\IConfig;
+use OCA\PasswordPolicy\Db\OldPassword;
+
+class UserNotificationConfigHandler {
+ const DEFAULT_EXPIRATION_FOR_NORMAL_NOTIFICATION = 30 * 24 * 60 * 60; // 30 days
+
+ /** @var IConfig */
+ private $config;
+
+ public function __construct(IConfig $config) {
+ $this->config = $config;
+ }
+
+ /**
+ * Return the number of seconds until the passwords should expire or
+ * null if it isn't set (or disabled) or has a non parseable value
+ * @return int|null seconds
+ */
+ public function getExpirationTime() {
+ $isChecked = $this->config->getAppValue(
+ 'password_policy',
+ 'spv_user_password_expiration_checked',
+ false
+ );
+ if (!\filter_var($isChecked, FILTER_VALIDATE_BOOLEAN)) {
+ return null;
+ }
+
+ $expirationTime = $this->config->getAppValue(
+ 'password_policy',
+ 'spv_user_password_expiration_value',
+ null
+ );
+ if ($expirationTime === null || !\is_numeric($expirationTime) || $expirationTime < 0) {
+ return null; // passwords don't expire or have weird value
+ }
+ // the expiration time is currently stored in days, so we need to convert
+ // it to seconds.
+ return \intval($expirationTime) * 24 * 60 * 60;
+ }
+
+ /**
+ * Return the number of seconds until a user should receive a notification
+ * that their password is about to expire. This _should_ be less than the value
+ * returned by the getExpirationTime function (you'll need to verify it outside)
+ * It will return null if the value isn't set (or disabled) or it has a
+ * non-parseable value
+ * @return int|null seconds
+ */
+ public function getExpirationTimeForNormalNotification() {
+ $isChecked = $this->config->getAppValue(
+ 'password_policy',
+ 'spv_user_password_expiration_notification_checked',
+ false
+ );
+ if (!\filter_var($isChecked, FILTER_VALIDATE_BOOLEAN)) {
+ return null;
+ }
+
+ $expirationTime = $this->config->getAppValue(
+ 'password_policy',
+ 'spv_user_password_expiration_notification_value',
+ self::DEFAULT_EXPIRATION_FOR_NORMAL_NOTIFICATION);
+ if ($expirationTime === null || !\is_numeric($expirationTime) || $expirationTime < 0) {
+ return null; // passwords don't expire or have weird value
+ }
+ return \intval($expirationTime);
+ }
+
+ /**
+ * Mark that a "password about to expire" notification has been sent.
+ * Note that we're using the id of the passInfo as marker, but this might change
+ * @param OldPassword $passInfo the information about the password. It has
+ * to include the userid owning the password and an id for the password
+ */
+ public function markAboutToExpireNotificationSentFor(OldPassword $passInfo) {
+ $this->config->setUserValue($passInfo->getUid(), 'password_policy', 'aboutToExpireSent', $passInfo->getId());
+ }
+
+ /**
+ * Mark that a "password expired" notification has been sent.
+ * Note that we're using the id of the passInfo as marker, but this might change
+ * @param OldPassword $passInfo the information about the password. It has
+ * to include the userid owning the password and an id for the password
+ */
+ public function markExpiredNotificationSentFor(OldPassword $passInfo) {
+ $this->config->setUserValue($passInfo->getUid(), 'password_policy', 'expiredSent', $passInfo->getId());
+ }
+
+ /**
+ * Get the mark set with markAboutToExpireNotificationSentFor for the specified user
+ * @param string $userid the user id to get the mark from
+ * @return string|null the mark or null if there is no mark
+ */
+ public function getMarkAboutToExpireNotificationSentFor($userid) {
+ return $this->config->getUserValue($userid, 'password_policy', 'aboutToExpireSent', null);
+ }
+
+ /**
+ * Get the mark set with markExpiredNotificationSentFor for the specified user
+ * @param string $userid the user id to get the mark from
+ * @return string|null the mark or null if there is no mark
+ */
+ public function getMarkExpiredNotificationSentFor($userid) {
+ return $this->config->getUserValue($userid, 'password_policy', 'expiredSent', null);
+ }
+
+ /**
+ * Check if a "password about to expire" notification has been sent for that
+ * password
+ * @param OldPassword $passInfo the password information to be checked
+ * @return bool true if the notification has been sent already, false otherwise.
+ * Note that we'll check only with the last password id sent
+ */
+ public function isSentAboutToExpireNotification(OldPassword $passInfo) {
+ $storedId = $this->config->getUserValue($passInfo->getUid(), 'password_policy', 'aboutToExpireSent', null);
+ if ($storedId === null) {
+ return false; // notification not sent
+ } elseif (\intval($storedId) !== $passInfo->getId()) {
+ // if the password id doesn't match the one stored, the notification hasn't been sent
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Check if a "password expired" notification has been sent for that
+ * password
+ * @param OldPassword $passInfo the password information to be checked
+ * @return bool true if the notification has been sent already, false otherwise.
+ * Note that we'll check only with the last password id sent
+ */
+ public function isSentExpiredNotification(OldPassword $passInfo) {
+ $storedId = $this->config->getUserValue($passInfo->getUid(), 'password_policy', 'expiredSent', null);
+ if ($storedId === null) {
+ return false; // notification not sent
+ } elseif (\intval($storedId) !== $passInfo->getId()) {
+ // if the password id doesn't match the one stored, the notification hasn't been sent
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Reset the marks created with markAboutToExpireNotificationSentFor and
+ * markExpiredNotificationSentFor functions. This function should be call
+ * once the password for the user has been changed
+ * @param string $uid the id if the user that has changed his password
+ */
+ public function resetExpirationMarks($uid) {
+ $targetKeys = [
+ 'aboutToExpireSent',
+ 'expiredSent',
+ ];
+ foreach ($targetKeys as $targetKey) {
+ $this->config->deleteUserValue($uid, 'password_policy', $targetKey);
+ }
+ }
+}
diff --git a/templates/admin.php b/templates/admin.php
index 24e24263..51341e3f 100644
--- a/templates/admin.php
+++ b/templates/admin.php
@@ -80,7 +80,7 @@