='fix/no-issue/no-reshare-perms-4-email-shares'>fix/no-issue/no-reshare-perms-4-email-shares Nextcloud server, a safe home for all your data: https://github.com/nextcloud/serverwww-data
aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
Diffstat
-rw-r--r--.gitignore1
-rw-r--r--apps/twofactor_backupcodes/appinfo/app.php22
-rw-r--r--apps/twofactor_backupcodes/appinfo/database.xml48
-rw-r--r--apps/twofactor_backupcodes/appinfo/info.xml19
-rw-r--r--apps/twofactor_backupcodes/appinfo/routes.php35
-rw-r--r--apps/twofactor_backupcodes/css/style.css25
-rw-r--r--apps/twofactor_backupcodes/js/settings.js16
-rw-r--r--apps/twofactor_backupcodes/js/settingsview.js120
-rw-r--r--apps/twofactor_backupcodes/lib/Controller/SettingsController.php73
-rw-r--r--apps/twofactor_backupcodes/lib/Db/BackupCode.php46
-rw-r--r--apps/twofactor_backupcodes/lib/Db/BackupCodeMapper.php66
-rw-r--r--apps/twofactor_backupcodes/lib/Provider/BackupCodesProvider.php102
-rw-r--r--apps/twofactor_backupcodes/lib/Service/BackupCodeStorage.php121
-rw-r--r--apps/twofactor_backupcodes/settings/personal.php5
-rw-r--r--apps/twofactor_backupcodes/templates/challenge.php8
-rw-r--r--apps/twofactor_backupcodes/templates/personal.php12
-rw-r--r--apps/twofactor_backupcodes/tests/Integration/Db/BackupCodeMapperTest.php113
-rw-r--r--apps/twofactor_backupcodes/tests/Integration/Service/BackupCodeStorageTest.php90
-rw-r--r--apps/twofactor_backupcodes/tests/Unit/Controller/SettingsControllerTest.php95
-rw-r--r--apps/twofactor_backupcodes/tests/Unit/Provider/BackupCodesProviderTest.php103
-rw-r--r--apps/twofactor_backupcodes/tests/Unit/Service/BackupCodeStorageTest.php228
-rw-r--r--build/integration/features/provisioning-v1.feature1
-rw-r--r--core/Controller/TwoFactorChallengeController.php9
-rw-r--r--core/css/styles.css2
-rw-r--r--core/shipped.json2
-rw-r--r--core/templates/twofactorselectchallenge.php10
-rw-r--r--core/templates/twofactorshowchallenge.php10
-rw-r--r--lib/composer/composer/autoload_classmap.php1
-rw-r--r--lib/composer/composer/autoload_static.php1
-rw-r--r--lib/private/Authentication/TwoFactorAuth/Manager.php20
-rw-r--r--lib/private/L10N/Factory.php57
-rw-r--r--lib/private/L10N/LanguageNotFoundException.php26
-rw-r--r--lib/private/Setup/PostgreSQL.php2
-rw-r--r--lib/public/AppFramework/Http/OCSResponse.php5
-rw-r--r--tests/Core/Controller/TwoFactorChallengeControllerTest.php19
-rw-r--r--tests/lib/App/ManagerTest.php16
-rw-r--r--tests/lib/AppTest.php9
-rw-r--r--tests/lib/Authentication/TwoFactorAuth/ManagerTest.php84
-rw-r--r--tests/lib/L10N/FactoryTest.php110
-rw-r--r--version.php2
40 files changed, 1676 insertions, 58 deletions
diff --git a/.gitignore b/.gitignore
index 0811a12f44b..56095af8308 100644
--- a/.gitignore
+++ b/.gitignore
@@ -27,6 +27,7 @@
!/apps/admin_audit
!/apps/updatenotification
!/apps/theming
+!/apps/twofactor_backupcodes
!/apps/workflowengine
/apps/files_external/3rdparty/irodsphp/PHPUnitTest
/apps/files_external/3rdparty/irodsphp/web
diff --git a/apps/twofactor_backupcodes/appinfo/app.php b/apps/twofactor_backupcodes/appinfo/app.php
new file mode 100644
index 00000000000..31f9b6b8eae
--- /dev/null
+++ b/apps/twofactor_backupcodes/appinfo/app.php
@@ -0,0 +1,22 @@
+<?php
+
+/**
+ * @author Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @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/>.
+ *
+ */
+OC_App::registerPersonal('twofactor_backupcodes', 'settings/personal');
diff --git a/apps/twofactor_backupcodes/appinfo/database.xml b/apps/twofactor_backupcodes/appinfo/database.xml
new file mode 100644
index 00000000000..02ca7c93a1e
--- /dev/null
+++ b/apps/twofactor_backupcodes/appinfo/database.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="ISO-8859-1" ?>
+<database>
+ <name>*dbname*</name>
+ <create>true</create>
+ <overwrite>false</overwrite>
+ <charset>utf8</charset>
+ <table>
+ <name>*dbprefix*twofactor_backup_codes</name>
+ <declaration>
+ <field>
+ <name>id</name>
+ <type>integer</type>
+ <autoincrement>1</autoincrement>
+ <default>0</default>
+ <notnull>true</notnull>
+ <length>4</length>
+ </field>
+ <field>
+ <name>user_id</name>
+ <type>text</type>
+ <default></default>
+ <notnull>true</notnull>
+ <length>64</length>
+ </field>
+ <field>
+ <name>code</name>
+ <type>text</type>
+ <notnull>true</notnull>
+ <length>64</length>
+ </field>
+ <field>
+ <name>used</name>
+ <type>integer</type>
+ <notnull>true</notnull>
+ <default>0</default>
+ <length>1</length>
+ </field>
+
+ <index>
+ <name>two_factor_backupcodes_user_id</name>
+ <field>
+ <name>user_id</name>
+ <sorting>ascending</sorting>
+ </field>
+ </index>
+ </declaration>
+ </table>
+</database>
diff --git a/apps/twofactor_backupcodes/appinfo/info.xml b/apps/twofactor_backupcodes/appinfo/info.xml
new file mode 100644
index 00000000000..45e9b022bbf
--- /dev/null
+++ b/apps/twofactor_backupcodes/appinfo/info.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0"?>
+<info>
+ <id>twofactor_backupcodes</id>
+ <name>Two factor backup codes</name>
+ <description>A two-factor auth backup codes provider</description>
+ <licence>agpl</licence>
+ <author>Christoph Wurst</author>
+ <version>1.0.0</version>
+ <namespace>TwoFactor_BackupCodes</namespace>
+ <category>other</category>
+
+ <two-factor-providers>
+ <provider>OCA\TwoFactor_BackupCodes\Provider\BackupCodesProvider</provider>
+ </two-factor-providers>
+
+ <dependencies>
+ <owncloud min-version="9.2" max-version="9.2" />
+ </dependencies>
+</info>
diff --git a/apps/twofactor_backupcodes/appinfo/routes.php b/apps/twofactor_backupcodes/appinfo/routes.php
new file mode 100644
index 00000000000..f2af12e9b45
--- /dev/null
+++ b/apps/twofactor_backupcodes/appinfo/routes.php
@@ -0,0 +1,35 @@
+<?php
+
+/**
+ * @author Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @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/>.
+ *
+ */
+return [
+ 'routes' => [
+ [
+ 'name' => 'settings#state',
+ 'url' => '/settings/state',
+ 'verb' => 'GET'
+ ],
+ [
+ 'name' => 'settings#createCodes',
+ 'url' => '/settings/create',
+ 'verb' => 'POST'
+ ],
+ ]
+];
diff --git a/apps/twofactor_backupcodes/css/style.css b/apps/twofactor_backupcodes/css/style.css
new file mode 100644
index 00000000000..0e15290b86a
--- /dev/null
+++ b/apps/twofactor_backupcodes/css/style.css
@@ -0,0 +1,25 @@
+.challenge-form {
+ margin: 16px auto 1px !important;
+}
+
+.challenge {
+ margin-top: 0 !important;
+ margin-left: 0 !important;
+}
+
+.confirm-inline {
+ position: absolute;
+ right: 10px;
+ top: 0;
+ margin: 0 !important;
+ padding-right: 25px !important;
+ background-color: transparent !important;
+ border: none !important;
+ opacity: .5;
+}
+
+.backup-code {
+ font-family: monospace;
+ letter-spacing: 0.02em;
+ font-size: 1.2em;
+}
diff --git a/apps/twofactor_backupcodes/js/settings.js b/apps/twofactor_backupcodes/js/settings.js
new file mode 100644
index 00000000000..a4045cbc118
--- /dev/null
+++ b/apps/twofactor_backupcodes/js/settings.js
@@ -0,0 +1,16 @@
+/* global OC */
+
+(function (OC) {
+ 'use strict';
+
+ OC.Settings = OC.Settings || {};
+ OC.Settings.TwoFactorBackupCodes = OC.Settings.TwoFactorBackupCodes || {};
+
+ $(function () {
+ var view = new OC.Settings.TwoFactorBackupCodes.View({
+ el: $('#twofactor-backupcodes-settings')
+ });
+ view.render();
+ });
+})(OC);
+
diff --git a/apps/twofactor_backupcodes/js/settingsview.js b/apps/twofactor_backupcodes/js/settingsview.js
new file mode 100644
index 00000000000..3da4c141159
--- /dev/null
+++ b/apps/twofactor_backupcodes/js/settingsview.js
@@ -0,0 +1,120 @@
+/* global Backbone, Handlebars, OC, _ */
+
+(function (OC, Handlebars, $, _) {
+ 'use strict';
+
+ OC.Settings = OC.Settings || {};
+ OC.Settings.TwoFactorBackupCodes = OC.Settings.TwoFactorBackupCodes || {};
+
+ var TEMPLATE = '<div>'
+ + '{{#unless enabled}}'
+ + '<button id="generate-backup-codes">' + t('twofactor_backupcodes', 'Generate backup codes') + '</button>'
+ + '{{else}}'
+ + '<p>'
+ + '{{#unless codes}}'
+ + t('twofactor_backupcodes', 'Backup codes have been generated. {{used}} of {{total}} codes have been used.')
+ + '{{else}}'
+ + t('twofactor_backupcodes', 'These are your backup codes. Please save and/or print them as you will not be able to read the codes again later')
+ + '<ul>'
+ + '{{#each codes}}'
+ + '<li class="backup-code">{{this}}</li>'
+ + '{{/each}}'
+ + '</ul>'
+ + '<a href="{{download}}" class="button" download="Nextcloud-backup-codes.txt">' + t('twofactor_backupcodes', 'Save backup codes') + '</a>'
+ + '<button id="print-backup-codes" class="button">' + t('twofactor_backupcodes', 'Print backup codes') + '</button>'
+ + '{{/unless}}'
+ + '</p>'
+ + '<p>'
+ + '<button id="generate-backup-codes">' + t('twofactor_backupcodes', 'Regenerate backup codes') + '</button>'
+ + '</p>'
+ + '<p>'
+ + t('twofactor_backupcodes', 'If you regenerate backup codes, you automatically invalidate old codes.')
+ + '</p>'
+ + '{{/unless}}'
+ + '</div';
+
+ var View = OC.Backbone.View.extend({
+ _template: undefined,
+ template: function (data) {
+ if (!this._template) {
+ this._template = Handlebars.compile(TEMPLATE);
+ }
+ return this._template(data);
+ },
+ _loading: undefined,
+ _enabled: undefined,
+ _total: undefined,
+ _used: undefined,
+ _codes: undefined,
+ events: {
+ 'click #generate-backup-codes': '_onGenerateBackupCodes',
+ 'click #print-backup-codes': '_onPrintBackupCodes',
+ },
+ initialize: function () {
+ this._load();
+ },
+ render: function () {
+ this.$el.html(this.template({
+ enabled: this._enabled,
+ total: this._total,
+ used: this._used,
+ codes: this._codes,
+ download: this._getDownloadDataHref()
+ }));
+ },
+ _getDownloadDataHref: function () {
+ if (!this._codes) {
+ return '';
+ }
+ return 'data:text/plain,' + encodeURIComponent(_.reduce(this._codes, function (prev, code) {
+ return prev + code + "\r\n";
+ }, ''));
+ },
+ _load: function () {
+ this._loading = true;
+
+ var url = OC.generateUrl('/apps/twofactor_backupcodes/settings/state');
+ var loading = $.ajax(url, {
+ method: 'GET',
+ });
+
+ $.when(loading).done(function (data) {
+ this._enabled = data.enabled;
+ this._total = data.total;
+ this._used = data.used;
+ }.bind(this));
+ $.when(loading).always(function () {
+ this._loading = false;
+ this.render();
+ }.bind(this));
+ },
+ _onGenerateBackupCodes: function () {
+ // Hide old codes
+ this._enabled = false;
+ this.render();
+ $('#generate-backup-codes').addClass('icon-loading-small');
+ var url = OC.generateUrl('/apps/twofactor_backupcodes/settings/create');
+ $.ajax(url, {
+ method: 'POST'
+ }).done(function (data) {
+ this._enabled = data.state.enabled;
+ this._total = data.state.total;
+ this._used = data.state.used;
+ this._codes = data.codes;
+ this.render();
+ }.bind(this)).fail(function () {
+ OC.Notification.showTemporary('An error occurred while generating your backup codes');
+ $('#generate-backup-codes').removeClass('icon-loading-small');
+ });
+ },
+ _onPrintBackupCodes: function () {
+ var url = this._getDownloadDataHref();
+ window.open(url, 'Nextcloud backpu codes');
+ window.print();
+ window.close();
+ }
+ });
+
+ OC.Settings.TwoFactorBackupCodes.View = View;
+
+})(OC, Handlebars, $, _); \ No newline at end of file
diff --git a/apps/twofactor_backupcodes/lib/Controller/SettingsController.php b/apps/twofactor_backupcodes/lib/Controller/SettingsController.php
new file mode 100644
index 00000000000..5130357baa5
--- /dev/null
+++ b/apps/twofactor_backupcodes/lib/Controller/SettingsController.php
@@ -0,0 +1,73 @@
+<?php
+
+/**
+ * @author Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @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\TwoFactor_BackupCodes\Controller;
+
+use OCA\TwoFactor_BackupCodes\Service\BackupCodeStorage;
+use OCP\AppFramework\Controller;
+use OCP\AppFramework\Http\JSONResponse;
+use OCP\IRequest;
+use OCP\IUserSession;
+
+class SettingsController extends Controller {
+
+ /** @var BackupCodeStorage */
+ private $storage;
+
+ /** @var IUserSession */
+ private $userSession;
+
+ /**
+ * @param string $appName
+ * @param IRequest $request
+ * @param BackupCodeStorage $storage
+ * @param IUserSession $userSession
+ */
+ public function __construct($appName, IRequest $request, BackupCodeStorage $storage, IUserSession $userSession) {
+ parent::__construct($appName, $request);
+ $this->userSession = $userSession;
+ $this->storage = $storage;
+ }
+
+ /**
+ * @NoAdminRequired
+ * @return JSONResponse
+ */
+ public function state() {
+ $user = $this->userSession->getUser();
+ return $this->storage->getBackupCodesState($user);
+ }
+
+ /**
+ * @NoAdminRequired
+ * @return JSONResponse
+ */
+ public function createCodes() {
+ $user = $this->userSession->getUser();
+ $codes = $this->storage->createCodes($user);
+ return [
+ 'codes' => $codes,
+ 'state' => $this->storage->getBackupCodesState($user),
+ ];
+ }
+
+}
diff --git a/apps/twofactor_backupcodes/lib/Db/BackupCode.php b/apps/twofactor_backupcodes/lib/Db/BackupCode.php
new file mode 100644
index 00000000000..5bfb681063c
--- /dev/null
+++ b/apps/twofactor_backupcodes/lib/Db/BackupCode.php
@@ -0,0 +1,46 @@
+<?php
+
+/**
+ * @author Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @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\TwoFactor_BackupCodes\Db;
+
+use OCP\AppFramework\Db\Entity;
+
+/**
+ * @method string getUserId()
+ * @method void setUserId(string $userId)
+ * @method string getCode()
+ * @method void setCode(string $code)
+ * @method int getUsed()
+ * @method void setUsed(int $code)
+ */
+class BackupCode extends Entity {
+
+ /** @var string */
+ protected $userId;
+
+ /** @var string */
+ protected $code;
+
+ /** @var int */
+ protected $used;
+
+}
diff --git a/apps/twofactor_backupcodes/lib/Db/BackupCodeMapper.php b/apps/twofactor_backupcodes/lib/Db/BackupCodeMapper.php
new file mode 100644
index 00000000000..d6256929675
--- /dev/null
+++ b/apps/twofactor_backupcodes/lib/Db/BackupCodeMapper.php
@@ -0,0 +1,66 @@
+<?php
+
+/**
+ * @author Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @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\TwoFactor_BackupCodes\Db;
+
+use OCP\AppFramework\Db\Mapper;
+use OCP\DB\QueryBuilder\IQueryBuilder;
+use OCP\IDb;
+use OCP\IUser;
+
+class BackupCodeMapper extends Mapper {
+
+ public function __construct(IDb $db) {
+ parent::__construct($db, 'twofactor_backup_codes');
+ }
+
+ /**
+ * @param IUser $user
+ * @return BackupCode[]
+ */
+ public function getBackupCodes(IUser $user) {
+ /* @var $qb IQueryBuilder */
+ $qb = $this->db->getQueryBuilder();
+
+ $qb->select('id', 'user_id', 'code', 'used')
+ ->from('twofactor_backup_codes')
+ ->where($qb->expr()->eq('user_id', $qb->createNamedParameter($user->getUID())));
+ $result = $qb->execute();
+
+ $rows = $result->fetchAll();
+ $result->closeCursor();
+
+ return array_map(function ($row) {
+ return BackupCode::fromRow($row);
+ }, $rows);
+ }
+
+ public function deleteCodes(IUser $user) {
+ /* @var $qb IQueryBuilder */
+ $qb = $this->db->getQueryBuilder();
+
+ $qb->delete('twofactor_backup_codes')
+ ->where($qb->expr()->eq('user_id', $qb->createNamedParameter($user->getUID())));
+ $qb->execute();
+ }
+
+}
diff --git a/apps/twofactor_backupcodes/lib/Provider/BackupCodesProvider.php b/apps/twofactor_backupcodes/lib/Provider/BackupCodesProvider.php
new file mode 100644
index 00000000000..91975dfad8d
--- /dev/null
+++ b/apps/twofactor_backupcodes/lib/Provider/BackupCodesProvider.php
@@ -0,0 +1,102 @@
+<?php
+
+/**
+ * @author Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @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\TwoFactor_BackupCodes\Provider;
+
+use OCA\TwoFactor_BackupCodes\Service\BackupCodeStorage;
+use OCP\Authentication\TwoFactorAuth\IProvider;
+use OCP\IL10N;
+use OCP\IUser;
+use OCP\Template;
+
+class BackupCodesProvider implements IProvider {
+
+ /** @var BackupCodeStorage */
+ private $storage;
+
+ /** @var IL10N */
+ private $l10n;
+
+ public function __construct(BackupCodeStorage $storage, IL10N $l10n) {
+ $this->l10n = $l10n;
+ $this->storage = $storage;
+ }
+
+ /**
+ * Get unique identifier of this 2FA provider
+ *
+ * @return string
+ */
+ public function getId() {
+ return 'backup_codes';
+ }
+
+ /**
+ * Get the display name for selecting the 2FA provider
+ *
+ * @return string
+ */
+ public function getDisplayName() {
+ return $this->l10n->t('Backup code');
+ }
+
+ /**
+ * Get the description for selecting the 2FA provider
+ *
+ * @return string
+ */
+ public function getDescription() {
+ return $this->l10n->t('Use backup code');
+ }
+
+ /**
+ * Get the template for rending the 2FA provider view
+ *
+ * @param IUser $user
+ * @return Template
+ */
+ public function getTemplate(IUser $user) {
+ $tmpl = new Template('twofactor_backupcodes', 'challenge');
+ return $tmpl;
+ }
+
+ /**
+ * Verify the given challenge
+ *
+ * @param IUser $user
+ * @param string $challenge
+ */
+ public function verifyChallenge(IUser $user, $challenge) {
+ return $this->storage->validateCode($user, $challenge);
+ }
+
+ /**
+ * Decides whether 2FA is enabled for the given user
+ *
+ * @param IUser $user
+ * @return boolean
+ */
+ public function isTwoFactorAuthEnabledForUser(IUser $user) {
+ return $this->storage->hasBackupCodes($user);
+ }
+
+}
diff --git a/apps/twofactor_backupcodes/lib/Service/BackupCodeStorage.php b/apps/twofactor_backupcodes/lib/Service/BackupCodeStorage.php
new file mode 100644
index 00000000000..9c78581255f
--- /dev/null
+++ b/apps/twofactor_backupcodes/lib/Service/BackupCodeStorage.php
@@ -0,0 +1,121 @@
+<?php
+
+/**
+ * @author Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @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\TwoFactor_BackupCodes\Service;
+
+use OCA\TwoFactor_BackupCodes\Db\BackupCode;
+use OCA\TwoFactor_BackupCodes\Db\BackupCodeMapper;
+use OCP\IUser;
+use OCP\Security\IHasher;
+use OCP\Security\ISecureRandom;
+
+class BackupCodeStorage {
+
+ /** @var BackupCodeMapper */
+ private $mapper;
+
+ /** @var IHasher */
+ private $hasher;
+
+ /** @var ISecureRandom */
+ private $random;
+
+ public function __construct(BackupCodeMapper $mapper, ISecureRandom $random, IHasher $hasher) {
+ $this->mapper = $mapper;
+ $this->hasher = $hasher;
+ $this->random = $random;
+ }
+
+ /**
+ * @param IUser $user
+ * @return string[]
+ */
+ public function createCodes(IUser $user, $number = 10) {
+ $result = [];
+
+ // Delete existing ones
+ $this->mapper->deleteCodes($user);
+
+ $uid = $user->getUID();
+ foreach (range(1, min([$number, 20])) as $i) {
+ $code = $this->random->generate(10, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789');
+
+ $dbCode = new BackupCode();
+ $dbCode->setUserId($uid);
+ $dbCode->setCode($this->hasher->hash($code));
+ $dbCode->setUsed(0);
+ $this->mapper->insert($dbCode);
+
+ array_push($result, $code);
+ }
+
+ return $result;
+ }
+
+ /**
+ * @param IUser $user
+ * @return bool
+ */
+ public function hasBackupCodes(IUser $user) {
+ $codes = $this->mapper->getBackupCodes($user);
+ return count($codes) > 0;
+ }
+
+ /**
+ * @param IUser $user
+ * @return array
+ */
+ public function getBackupCodesState(IUser $user) {
+ $codes = $this->mapper->getBackupCodes($user);
+ $total = count($codes);
+ $used = 0;
+ array_walk($codes, function (BackupCode $code) use (&$used) {
+ if (1 === (int) $code->getUsed()) {
+ $used++;
+ }
+ });
+ return [
+ 'enabled' => $total > 0,
+ 'total' => $total,
+ 'used' => $used,
+ ];
+ }
+
+ /**
+ * @param IUser $user
+ * @param string $code
+ * @return bool
+ */
+ public function validateCode(IUser $user, $code) {
+ $dbCodes = $this->mapper->getBackupCodes($user);
+
+ foreach ($dbCodes as $dbCode) {
+ if (0 === (int) $dbCode->getUsed() && $this->hasher->verify($code, $dbCode->getCode())) {
+ $dbCode->setUsed(1);
+ $this->mapper->update($dbCode);
+ return true;
+ }
+ }
+ return false;
+ }
+
+}
diff --git a/apps/twofactor_backupcodes/settings/personal.php b/apps/twofactor_backupcodes/settings/personal.php
new file mode 100644
index 00000000000..037516e39a3
--- /dev/null
+++ b/apps/twofactor_backupcodes/settings/personal.php
@@ -0,0 +1,5 @@
+<?php
+
+$tmpl = new \OCP\Template('twofactor_backupcodes', 'personal');
+
+return $tmpl->fetchPage();
diff --git a/apps/twofactor_backupcodes/templates/challenge.php b/apps/twofactor_backupcodes/templates/challenge.php
new file mode 100644
index 00000000000..e6ec7bb52e6
--- /dev/null
+++ b/apps/twofactor_backupcodes/templates/challenge.php
@@ -0,0 +1,8 @@
+<?php
+style('twofactor_backupcodes', 'style');
+?>
+
+<form method="POST" class="challenge-form">
+ <input type="text" class="challenge" name="challenge" required="required" autofocus autocomplete="off" autocapitalize="off" placeholder="<?php p($l->t('Backup code')) ?>">
+ <input type="submit" class="confirm-inline icon-confirm" value="">
+</form>
diff --git a/apps/twofactor_backupcodes/templates/personal.php b/apps/twofactor_backupcodes/templates/personal.php
new file mode 100644
index 00000000000..23b06e23058
--- /dev/null
+++ b/apps/twofactor_backupcodes/templates/personal.php
@@ -0,0 +1,12 @@
+<?php
+
+script('twofactor_backupcodes', 'settingsview');
+script('twofactor_backupcodes', 'settings');
+style('twofactor_backupcodes', 'style');
+
+?>
+
+<div class="section">
+ <h2><?php p($l->t('Second-factor backup codes')); ?></h2>
+ <div id="twofactor-backupcodes-settings"></div>
+</div>
diff --git a/apps/twofactor_backupcodes/tests/Integration/Db/BackupCodeMapperTest.php b/apps/twofactor_backupcodes/tests/Integration/Db/BackupCodeMapperTest.php
new file mode 100644
index 00000000000..5d7d71dd17a
--- /dev/null
+++ b/apps/twofactor_backupcodes/tests/Integration/Db/BackupCodeMapperTest.php
@@ -0,0 +1,113 @@
+<?php
+
+/**
+ * @author Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @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\TwoFactor_BackupCodes\Tests\Integration\Db;
+
+use OC;
+use OCA\TwoFactor_BackupCodes\Db\BackupCode;
+use OCA\TwoFactor_BackupCodes\Db\BackupCodeMapper;
+use OCP\IDBConnection;
+use OCP\IUser;
+use Test\TestCase;
+
+/**
+ * @group DB
+ */
+class BackupCodeMapperTest extends TestCase {
+
+ /** @var IDBConnection */
+ private $db;
+
+ /** @var BackupCodeMapper */
+ private $mapper;
+
+ /** @var string */
+ private $testUID = 'test123456';
+
+ private function resetDB() {
+ $qb = $this->db->getQueryBuilder();
+ $qb->delete($this->mapper->getTableName())
+ ->where($qb->expr()->eq('user_id', $qb->createNamedParameter($this->testUID)));
+ $qb->execute();
+ }
+
+ protected function setUp() {
+ parent::setUp();
+
+ $this->db = OC::$server->getDatabaseConnection();
+ $this->mapper = OC::$server->query(BackupCodeMapper::class);
+
+ $this->resetDB();
+ }
+
+ protected function tearDown() {
+ parent::tearDown();
+
+ $this->resetDB();
+ }
+
+ public function testGetBackupCodes() {
+ $code1 = new BackupCode();
+ $code1->setUserId($this->testUID);
+ $code1->setCode('1|$2y$10$Fyo.DkMtkaHapVvRVbQBeeIdi5x/6nmPnxiBzD0GDKa08NMus5xze');
+ $code1->setUsed(1);
+
+ $code2 = new BackupCode();
+ $code2->setUserId($this->testUID);
+ $code2->setCode('1|$2y$10$nj3sZaCqGN8t6.SsnNADt.eX34UCkdX6FPx.r.rIwE6Jj3vi5wyt2');
+ $code2->setUsed(0);
+
+ $this->mapper->insert($code1);
+ $this->mapper->insert($code2);
+
+ $user = $this->getMockBuilder(IUser::class)->getMock();
+ $user->expects($this->once())
+ ->method('getUID')
+ ->will($this->returnValue($this->testUID));
+
+ $dbCodes = $this->mapper->getBackupCodes($user);
+
+ $this->assertCount(2, $dbCodes);
+ $this->assertInstanceOf(BackupCode::class, $dbCodes[0]);
+ $this->assertInstanceOf(BackupCode::class, $dbCodes[1]);
+ }
+
+ public function testDeleteCodes() {
+ $code = new BackupCode();
+ $code->setUserId($this->testUID);
+ $code->setCode('1|$2y$10$CagG8pEhZL.xDirtCCP/KuuWtnsAasgq60zY9rU46dBK4w8yW0Z/y');
+ $code->setUsed(1);
+ $user = $this->getMockBuilder(IUser::class)->getMock();
+ $user->expects($this->any())
+ ->method('getUID')
+ ->will($this->returnValue($this->testUID));
+
+ $this->mapper->insert($code);
+
+ $this->assertCount(1, $this->mapper->getBackupCodes($user));
+
+ $this->mapper->deleteCodes($user);
+
+ $this->assertCount(0, $this->mapper->getBackupCodes($user));
+ }
+
+}
diff --git a/apps/twofactor_backupcodes/tests/Integration/Service/BackupCodeStorageTest.php b/apps/twofactor_backupcodes/tests/Integration/Service/BackupCodeStorageTest.php
new file mode 100644
index 00000000000..5517af5ce0d
--- /dev/null
+++ b/apps/twofactor_backupcodes/tests/Integration/Service/BackupCodeStorageTest.php
@@ -0,0 +1,90 @@
+<?php
+
+/**
+ * @author Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @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\TwoFactor_BackupCodes\Tests\Integration\Service;
+
+use OC;
+use OCA\TwoFactor_BackupCodes\Service\BackupCodeStorage;
+use Test\TestCase;
+
+/**
+ * @group DB
+ */
+class BackupCodeStorageTest extends TestCase {
+
+ /** @var BackupCodeStorage */
+ private $storage;
+
+ /** @var string */
+ private $testUID = 'test123456789';
+
+ protected function setUp() {
+ parent::setUp();
+
+ $this->storage = OC::$server->query(BackupCodeStorage::class);
+ }
+
+ public function testSimpleWorkFlow() {
+ $user = $this->getMockBuilder(\OCP\IUser::class)->getMock();
+ $user->expects($this->any())
+ ->method('getUID')
+ ->will($this->returnValue($this->testUID));
+
+ // Create codes
+ $codes = $this->storage->createCodes($user, 5);
+ $this->assertCount(5, $codes);
+ $this->assertTrue($this->storage->hasBackupCodes($user));
+ $initialState = [
+ 'enabled' => true,
+ 'total' => 5,
+ 'used' => 0,
+ ];
+ $this->assertEquals($initialState, $this->storage->getBackupCodesState($user));
+
+ // Use codes
+ $code = $codes[2];
+ $this->assertTrue($this->storage->validateCode($user, $code));
+ // Code must not be used twice
+ $this->assertFalse($this->storage->validateCode($user, $code));
+ // Invalid codes are invalid
+ $this->assertFalse($this->storage->validateCode($user, 'I DO NOT EXIST'));
+ $stateAfter = [
+ 'enabled' => true,
+ 'total' => 5,
+ 'used' => 1,
+ ];
+ $this->assertEquals($stateAfter, $this->storage->getBackupCodesState($user));
+
+ // Deplete codes
+ $this->assertTrue($this->storage->validateCode($user, $codes[0]));
+ $this->assertTrue($this->storage->validateCode($user, $codes[1]));
+ $this->assertTrue($this->storage->validateCode($user, $codes[3]));
+ $this->assertTrue($this->storage->validateCode($user, $codes[4]));
+ $stateAllUsed = [
+ 'enabled' => true,
+ 'total' => 5,
+ 'used' => 5,
+ ];
+ $this->assertEquals($stateAllUsed, $this->storage->getBackupCodesState($user));
+ }
+
+}
diff --git a/apps/twofactor_backupcodes/tests/Unit/Controller/SettingsControllerTest.php b/apps/twofactor_backupcodes/tests/Unit/Controller/SettingsControllerTest.php
new file mode 100644
index 00000000000..918d1a8c64d
--- /dev/null
+++ b/apps/twofactor_backupcodes/tests/Unit/Controller/SettingsControllerTest.php
@@ -0,0 +1,95 @@
+<?php
+
+/**
+ * @author Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @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\TwoFactor_BackupCodes\Tests\Unit\Controller;
+
+use OCA\TwoFactor_BackupCodes\Controller\SettingsController;
+use OCA\TwoFactor_BackupCodes\Service\BackupCodeStorage;
+use OCP\IRequest;
+use OCP\IUser;
+use OCP\IUserSession;
+use Test\TestCase;
+
+class SettingsControllerTest extends TestCase {
+
+ /** @var IRequest|PHPUnit_Framework_MockObject_MockObject */
+ private $request;
+
+ /** @var BackupCodeStorage|PHPUnit_Framework_MockObject_MockObject */
+ private $storage;
+
+ /** @var IUserSession|PHPUnit_Framework_MockObject_MockObject */
+ private $userSession;
+
+ /** @var SettingsController */
+ private $controller;
+
+ protected function setUp() {
+ parent::setUp();
+
+ $this->request = $this->getMockBuilder(IRequest::class)->getMock();
+ $this->storage = $this->getMockBuilder(BackupCodeStorage::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->userSession = $this->getMockBuilder(IUserSession::class)->getMock();
+
+ $this->controller = new SettingsController('twofactor_backupcodes', $this->request, $this->storage, $this->userSession);
+ }
+
+ public function testState() {
+ $user = $this->getMockBuilder(IUser::class)->getMock();
+
+ $this->userSession->expects($this->once())
+ ->method('getUser')
+ ->will($this->returnValue($user));
+ $this->storage->expects($this->once())
+ ->method('getBackupCodesState')
+ ->with($user)
+ ->will($this->returnValue('state'));
+
+ $this->assertEquals('state', $this->controller->state());
+ }
+
+ public function testCreateCodes() {
+ $user = $this->getMockBuilder(IUser::class)->getMock();
+
+ $codes = ['a', 'b'];
+ $this->userSession->expects($this->once())
+ ->method('getUser')
+ ->will($this->returnValue($user));
+ $this->storage->expects($this->once())
+ ->method('createCodes')
+ ->with($user)
+ ->will($this->returnValue($codes));
+ $this->storage->expects($this->once())
+ ->method('getBackupCodesState')
+ ->with($user)
+ ->will($this->returnValue('state'));
+
+ $expected = [
+ 'codes' => $codes,
+ 'state' => 'state',
+ ];
+ $this->assertEquals($expected, $this->controller->createCodes());
+ }
+
+}
diff --git a/apps/twofactor_backupcodes/tests/Unit/Provider/BackupCodesProviderTest.php b/apps/twofactor_backupcodes/tests/Unit/Provider/BackupCodesProviderTest.php
new file mode 100644
index 00000000000..a744a44e609
--- /dev/null
+++ b/apps/twofactor_backupcodes/tests/Unit/Provider/BackupCodesProviderTest.php
@@ -0,0 +1,103 @@
+<?php
+
+/**
+ * @author Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @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\TwoFactor_BackupCodes\Tests\Unit\Provider;
+
+use OCA\TwoFactor_BackupCodes\Provider\BackupCodesProvider;
+use OCA\TwoFactor_BackupCodes\Service\BackupCodeStorage;
+use OCP\IL10N;
+use OCP\IUser;
+use OCP\Template;
+use Test\TestCase;
+
+class BackupCodesProviderTest extends TestCase {
+
+ /** @var BackupCodeStorage|PHPUnit_Framework_MockObject_MockObject */
+ private $storage;
+
+ /** @var IL10N|PHPUnit_Framework_MockObject_MockObject */
+ private $l10n;
+
+ /** @var BackupCodesProvider */
+ private $provider;
+
+ protected function setUp() {
+ parent::setUp();
+
+ $this->storage = $this->getMockBuilder(BackupCodeStorage::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->l10n = $this->getMockBuilder(IL10N::class)->getMock();
+ $this->provider = new BackupCodesProvider($this->storage, $this->l10n);
+ }
+
+ public function testGetId() {
+ $this->assertEquals('backup_codes', $this->provider->getId());
+ }
+
+ public function testGetDisplayName() {
+ $this->l10n->expects($this->once())
+ ->method('t')
+ ->with('Backup code')
+ ->will($this->returnValue('l10n backup code'));
+ $this->assertSame('l10n backup code', $this->provider->getDisplayName());
+ }
+
+ public function testGetDescription() {
+ $this->l10n->expects($this->once())
+ ->method('t')
+ ->with('Use backup code')
+ ->will($this->returnValue('l10n use backup code'));
+ $this->assertSame('l10n use backup code', $this->provider->getDescription());
+ }
+
+ public function testGetTempalte() {
+ $user = $this->getMockBuilder(IUser::class)->getMock();
+ $expected = new Template('twofactor_backupcodes', 'challenge');
+
+ $this->assertEquals($expected, $this->provider->getTemplate($user));
+ }
+
+ public function testVerfiyChallenge() {
+ $user = $this->getMockBuilder(IUser::class)->getMock();
+ $challenge = 'xyz';
+
+ $this->storage->expects($this->once())
+ ->method('validateCode')
+ ->with($user, $challenge)
+ ->will($this->returnValue(false));
+
+ $this->assertFalse($this->provider->verifyChallenge($user, $challenge));
+ }
+
+ public function testIsTwoFactorEnabledForUser() {
+ $user = $this->getMockBuilder(IUser::class)->getMock();
+
+ $this->storage->expects($this->once())
+ ->method('hasBackupCodes')
+ ->with($user)
+ ->will($this->returnValue(true));
+
+ $this->assertTrue($this->provider->isTwoFactorAuthEnabledForUser($user));
+ }
+
+}
diff --git a/apps/twofactor_backupcodes/tests/Unit/Service/BackupCodeStorageTest.php b/apps/twofactor_backupcodes/tests/Unit/Service/BackupCodeStorageTest.php
new file mode 100644
index 00000000000..04c51fa7e14
--- /dev/null
+++ b/apps/twofactor_backupcodes/tests/Unit/Service/BackupCodeStorageTest.php
@@ -0,0 +1,228 @@
+<?php
+
+/**
+ * @author Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @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\TwoFactor_BackupCodes\Tests\Unit\Service;
+
+use OCA\TwoFactor_BackupCodes\Db\BackupCode;
+use OCA\TwoFactor_BackupCodes\Db\BackupCodeMapper;
+use OCA\TwoFactor_BackupCodes\Service\BackupCodeStorage;
+use OCP\IUser;
+use OCP\Security\IHasher;
+use OCP\Security\ISecureRandom;
+use Test\TestCase;
+
+class BackupCodeStorageTest extends TestCase {
+
+ /** @var BackupCodeMapper|PHPUnit_Framework_MockObject_MockObject */
+ private $mapper;
+
+ /** @var ISecureRandom|PHPUnit_Framework_MockObject_MockObject */
+ private $random;
+
+ /** @var IHasher|PHPUnit_Framework_MockObject_MockObject */
+ private $hasher;
+
+ /** @var BackupCodeStorage */
+ private $storage;
+
+ protected function setUp() {
+ parent::setUp();
+
+ $this->mapper = $this->getMockBuilder(BackupCodeMapper::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->random = $this->getMockBuilder(ISecureRandom::class)->getMock();
+ $this->hasher = $this->getMockBuilder(IHasher::class)->getMock();
+ $this->storage = new BackupCodeStorage($this->mapper, $this->random, $this->hasher);
+ }
+
+ public function testCreateCodes() {
+ $user = $this->getMockBuilder(IUser::class)->getMock();
+ $number = 5;
+
+ $user->expects($this->once())
+ ->method('getUID')
+ ->will($this->returnValue('fritz'));
+ $this->random->expects($this->exactly($number))
+ ->method('generate')
+ ->with(10, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789')
+ ->will($this->returnValue('CODEABCDEF'));
+ $this->hasher->expects($this->exactly($number))
+ ->method('hash')
+ ->with('CODEABCDEF')
+ ->will($this->returnValue('HASHEDCODE'));
+ $row = new BackupCode();
+ $row->setUserId('fritz');
+ $row->setCode('HASHEDCODE');
+ $row->setUsed(0);
+ $this->mapper->expects($this->exactly($number))
+ ->method('insert')
+ ->with($this->equalTo($row));
+
+ $codes = $this->storage->createCodes($user, $number);
+ $this->assertCount($number, $codes);
+ foreach ($codes as $code) {
+ $this->assertEquals('CODEABCDEF', $code);
+ }
+ }
+
+ public function testHasBackupCodes() {
+ $user = $this->getMockBuilder(IUser::class)->getMock();
+ $codes = [
+ new BackupCode(),
+ new BackupCode(),
+ ];
+
+ $this->mapper->expects($this->once())
+ ->method('getBackupCodes')
+ ->with($user)
+ ->will($this->returnValue($codes));
+
+ $this->assertTrue($this->storage->hasBackupCodes($user));
+ }
+
+ public function testHasBackupCodesNoCodes() {
+ $user = $this->getMockBuilder(IUser::class)->getMock();
+ $codes = [];
+
+ $this->mapper->expects($this->once())
+ ->method('getBackupCodes')
+ ->with($user)
+ ->will($this->returnValue($codes));
+
+ $this->assertFalse($this->storage->hasBackupCodes($user));
+ }
+
+ public function testGetBackupCodeState() {
+ $user = $this->getMockBuilder(IUser::class)->getMock();
+
+ $code1 = new BackupCode();
+ $code1->setUsed(1);
+ $code2 = new BackupCode();
+ $code2->setUsed('0');
+ $codes = [
+ $code1,
+ $code2,
+ ];
+
+ $this->mapper->expects($this->once())
+ ->method('getBackupCodes')
+ ->with($user)
+ ->will($this->returnValue($codes));
+
+ $expected = [
+ 'enabled' => true,
+ 'total' => 2,
+ 'used' => 1,
+ ];
+ $this->assertEquals($expected, $this->storage->getBackupCodesState($user));
+ }
+
+ public function testGetBackupCodeDisabled() {
+ $user = $this->getMockBuilder(IUser::class)->getMock();
+
+ $codes = [];
+
+ $this->mapper->expects($this->once())
+ ->method('getBackupCodes')
+ ->with($user)
+ ->will($this->returnValue($codes));
+
+ $expected = [
+ 'enabled' => false,
+ 'total' => 0,
+ 'used' => 0,
+ ];
+ $this->assertEquals($expected, $this->storage->getBackupCodesState($user));
+ }
+
+ public function testValidateCode() {
+ $user = $this->getMockBuilder(IUser::class)->getMock();
+ $code = new BackupCode();
+ $code->setUsed(0);
+ $code->setCode('HASHEDVALUE');
+ $codes = [
+ $code,
+ ];
+
+ $this->mapper->expects($this->once())
+ ->method('getBackupCodes')
+ ->with($user)
+ ->will($this->returnValue($codes));
+ $this->hasher->expects($this->once())
+ ->method('verify')
+ ->with('CHALLENGE', 'HASHEDVALUE')
+ ->will($this->returnValue(true));
+ $this->mapper->expects($this->once())
+ ->method('update')
+ ->with($code);
+
+ $this->assertTrue($this->storage->validateCode($user, 'CHALLENGE'));
+
+ $this->assertEquals(1, $code->getUsed());
+ }
+
+ public function testValidateUsedCode() {
+ $user = $this->getMockBuilder(IUser::class)->getMock();
+ $code = new BackupCode();
+ $code->setUsed('1');
+ $code->setCode('HASHEDVALUE');
+ $codes = [
+ $code,
+ ];
+
+ $this->mapper->expects($this->once())
+ ->method('getBackupCodes')
+ ->with($user)
+ ->will($this->returnValue($codes));
+ $this->hasher->expects($this->never())
+ ->method('verifiy');
+ $this->mapper->expects($this->never())
+ ->method('update');
+
+ $this->assertFalse($this->storage->validateCode($user, 'CHALLENGE'));
+ }
+
+ public function testValidateCodeWithWrongHash() {
+ $user = $this->getMockBuilder(IUser::class)->getMock();
+ $code = new BackupCode();
+ $code->setUsed(0);
+ $code->setCode('HASHEDVALUE');
+ $codes = [
+ $code,
+ ];
+
+ $this->mapper->expects($this->once())
+ ->method('getBackupCodes')
+ ->with($user)
+ ->will($this->returnValue($codes));
+ $this->hasher->expects($this->once())
+ ->method('verify')
+ ->with('CHALLENGE', 'HASHEDVALUE')
+ ->will($this->returnValue(false));
+ $this->mapper->expects($this->never())
+ ->method('update');
+
+ $this->assertFalse($this->storage->validateCode($user, 'CHALLENGE'));
+ }
+
+}
diff --git a/build/integration/features/provisioning-v1.feature b/build/integration/features/provisioning-v1.feature
index 785b795bf35..180108b5ccd 100644
--- a/build/integration/features/provisioning-v1.feature
+++ b/build/integration/features/provisioning-v1.feature
@@ -293,6 +293,7 @@ Feature: provisioning
| provisioning_api |
| systemtags |
| theming |
+ | twofactor_backupcodes |
| updatenotification |
| workflowengine |
| files_external |
diff --git a/core/Controller/TwoFactorChallengeController.php b/core/Controller/TwoFactorChallengeController.php
index 48dea062812..b2614138123 100644
--- a/core/Controller/TwoFactorChallengeController.php
+++ b/core/Controller/TwoFactorChallengeController.php
@@ -82,9 +82,11 @@ class TwoFactorChallengeController extends Controller {
public function selectChallenge($redirect_url) {
$user = $this->userSession->getUser();
$providers = $this->twoFactorManager->getProviders($user);
+ $backupProvider = $this->twoFactorManager->getBackupProvider($user);
$data = [
'providers' => $providers,
+ 'backupProvider' => $backupProvider,
'redirect_url' => $redirect_url,
'logout_attribute' => $this->getLogoutAttribute(),
];
@@ -107,6 +109,12 @@ class TwoFactorChallengeController extends Controller {
return new RedirectResponse($this->urlGenerator->linkToRoute('core.TwoFactorChallenge.selectChallenge'));
}
+ $backupProvider = $this->twoFactorManager->getBackupProvider($user);
+ if (!is_null($backupProvider) && $backupProvider->getId() === $provider->getId()) {
+ // Don't show the backup provider link if we're already showing that provider's challenge
+ $backupProvider = null;
+ }
+
if ($this->session->exists('two_factor_auth_error')) {
$this->session->remove('two_factor_auth_error');
$error = true;
@@ -118,6 +126,7 @@ class TwoFactorChallengeController extends Controller {
$data = [
'error' => $error,
'provider' => $provider,
+ 'backupProvider' => $backupProvider,
'logout_attribute' => $this->getLogoutAttribute(),
'template' => $tmpl->fetchPage(),
];
diff --git a/core/css/styles.css b/core/css/styles.css
index 25bc2d086d5..efc49e02a17 100644
--- a/core/css/styles.css
+++ b/core/css/styles.css
@@ -45,7 +45,7 @@ body {
border: none !important;
}
-.two-factor-cancel {
+.two-factor-link {
display: inline-block;
padding: 12px;
color: rgba(255, 255, 255, .75);
diff --git a/core/shipped.json b/core/shipped.json
index f944d9d4c11..0ddaf68eb64 100644
--- a/core/shipped.json
+++ b/core/shipped.json
@@ -28,6 +28,7 @@
"survey_client",
"systemtags",
"templateeditor",
+ "twofactor_backupcodes",
"theming",
"updatenotification",
"user_external",
@@ -39,6 +40,7 @@
"files",
"dav",
"federatedfilesharing",
+ "twofactor_backupcodes",
"workflowengine"
]
}
diff --git a/core/templates/twofactorselectchallenge.php b/core/templates/twofactorselectchallenge.php
index 1948499e604..5cfba8bcb47 100644
--- a/core/templates/twofactorselectchallenge.php
+++ b/core/templates/twofactorselectchallenge.php
@@ -19,4 +19,12 @@
</ul>
</p>
</div>
-<a class="two-factor-cancel" <?php print_unescaped($_['logout_attribute']); ?>><?php p($l->t('Cancel log in')) ?></a>
+<a class="two-factor-link" <?php print_unescaped($_['logout_attribute']); ?>><?php p($l->t('Cancel log in')) ?></a>
+<?php if (!is_null($_['backupProvider'])): ?>
+<a class="two-factor-link" href="<?php p(\OC::$server->getURLGenerator()->linkToRoute('core.TwoFactorChallenge.showChallenge',
+ [
+ 'challengeProviderId' => $_['backupProvider']->getId(),
+ 'redirect_url' => $_['redirect_url'],
+ ]
+ )) ?>"><?php p($l->t('Use backup code')) ?></a>
+<?php endif;
diff --git a/core/templates/twofactorshowchallenge.php b/core/templates/twofactorshowchallenge.php
index fb161921675..197de8a288e 100644
--- a/core/templates/twofactorshowchallenge.php
+++ b/core/templates/twofactorshowchallenge.php
@@ -16,4 +16,12 @@ $template = $_['template'];
<?php endif; ?>
<?php print_unescaped($template); ?>
</div>
-<a class="two-factor-cancel" <?php print_unescaped($_['logout_attribute']); ?>><?php p($l->t('Cancel log in')) ?></a>
+<a class="two-factor-link" <?php print_unescaped($_['logout_attribute']); ?>><?php p($l->t('Cancel log in')) ?></a>
+<?php if (!is_null($_['backupProvider'])): ?>
+<a class="two-factor-link" href="<?php p(\OC::$server->getURLGenerator()->linkToRoute('core.TwoFactorChallenge.showChallenge',
+ [
+ 'challengeProviderId' => $_['backupProvider']->getId(),
+ 'redirect_url' => $_['redirect_url'],
+ ]
+ )) ?>"><?php p($l->t('Use backup code')) ?></a>
+<?php endif;
diff --git a/lib/composer/composer/autoload_classmap.php b/lib/composer/composer/autoload_classmap.php
index 1363d96349e..2700c87ec00 100644
--- a/lib/composer/composer/autoload_classmap.php
+++ b/lib/composer/composer/autoload_classmap.php
@@ -550,6 +550,7 @@ return array(
'OC\\IntegrityCheck\\Iterator\\ExcludeFoldersByPathFilterIterator' => $baseDir . '/lib/private/IntegrityCheck/Iterator/ExcludeFoldersByPathFilterIterator.php',
'OC\\L10N\\Factory' => $baseDir . '/lib/private/L10N/Factory.php',
'OC\\L10N\\L10N' => $baseDir . '/lib/private/L10N/L10N.php',
+ 'OC\\L10N\\LanguageNotFoundException' => $baseDir . '/lib/private/L10N/LanguageNotFoundException.php',
'OC\\LargeFileHelper' => $baseDir . '/lib/private/LargeFileHelper.php',
'OC\\Lock\\AbstractLockingProvider' => $baseDir . '/lib/private/Lock/AbstractLockingProvider.php',
'OC\\Lock\\DBLockingProvider' => $baseDir . '/lib/private/Lock/DBLockingProvider.php',
diff --git a/lib/composer/composer/autoload_static.php b/lib/composer/composer/autoload_static.php
index 42b6fc83217..3797711f914 100644
--- a/lib/composer/composer/autoload_static.php
+++ b/lib/composer/composer/autoload_static.php
@@ -580,6 +580,7 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c
'OC\\IntegrityCheck\\Iterator\\ExcludeFoldersByPathFilterIterator' => __DIR__ . '/../../..' . '/lib/private/IntegrityCheck/Iterator/ExcludeFoldersByPathFilterIterator.php',
'OC\\L10N\\Factory' => __DIR__ . '/../../..' . '/lib/private/L10N/Factory.php',
'OC\\L10N\\L10N' => __DIR__ . '/../../..' . '/lib/private/L10N/L10N.php',
+ 'OC\\L10N\\LanguageNotFoundException' => __DIR__ . '/../../..' . '/lib/private/L10N/LanguageNotFoundException.php',
'OC\\LargeFileHelper' => __DIR__ . '/../../..' . '/lib/private/LargeFileHelper.php',
'OC\\Lock\\AbstractLockingProvider' => __DIR__ . '/../../..' . '/lib/private/Lock/AbstractLockingProvider.php',
'OC\\Lock\\DBLockingProvider' => __DIR__ . '/../../..' . '/lib/private/Lock/DBLockingProvider.php',
diff --git a/lib/private/Authentication/TwoFactorAuth/Manager.php b/lib/private/Authentication/TwoFactorAuth/Manager.php
index 143fe7dc927..1bea7aa3478 100644
--- a/lib/private/Authentication/TwoFactorAuth/Manager.php
+++ b/lib/private/Authentication/TwoFactorAuth/Manager.php
@@ -35,6 +35,8 @@ use OCP\IUser;
class Manager {
const SESSION_UID_KEY = 'two_factor_auth_uid';
+ const BACKUP_CODES_APP_ID = 'twofactor_backupcodes';
+ const BACKUP_CODES_PROVIDER_ID = 'backup_codes';
/** @var AppManager */
private $appManager;
@@ -93,21 +95,35 @@ class Manager {
* @return IProvider|null
*/
public function getProvider(IUser $user, $challengeProviderId) {
- $providers = $this->getProviders($user);
+ $providers = $this->getProviders($user, true);
return isset($providers[$challengeProviderId]) ? $providers[$challengeProviderId] : null;
}
/**
+ * @param IUser $user
+ * @return IProvider|null the backup provider, if enabled for the given user
+ */
+ public function getBackupProvider(IUser $user) {
+ $providers = $this->getProviders($user, true);
+ return $providers[self::BACKUP_CODES_PROVIDER_ID];
+ }
+
+ /**
* Get the list of 2FA providers for the given user
*
* @param IUser $user
+ * @param bool $includeBackupApp
* @return IProvider[]
*/
- public function getProviders(IUser $user) {
+ public function getProviders(IUser $user, $includeBackupApp = false) {
$allApps = $this->appManager->getEnabledAppsForUser($user);
$providers = [];
foreach ($allApps as $appId) {
+ if (!$includeBackupApp && $appId === self::BACKUP_CODES_APP_ID) {
+ continue;
+ }
+
$info = $this->appManager->getAppInfo($appId);
if (isset($info['two-factor-providers'])) {
$providerClasses = $info['two-factor-providers'];
diff --git a/lib/private/L10N/Factory.php b/lib/private/L10N/Factory.php
index 74f759a4e0d..91233a0c4a7 100644
--- a/lib/private/L10N/Factory.php
+++ b/lib/private/L10N/Factory.php
@@ -1,6 +1,7 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
+ * @copyright 2016 Roeland Jago Douma <roeland@famdouma.nl>
*
* @author Bart Visscher <bartv@thisnet.nl>
* @author Joas Schilling <coding@schilljs.com>
@@ -8,6 +9,7 @@
* @author Morris Jobke <hey@morrisjobke.de>
* @author Robin Appelman <robin@icewind.nl>
* @author Robin McCorkell <robin@mccorkell.me.uk>
+ * @author Roeland Jago Douma <roeland@famdouma.nl>
*
* @license AGPL-3.0
*
@@ -147,18 +149,23 @@ class Factory implements IFactory {
}
}
- $defaultLanguage = $this->config->getSystemValue('default_language', false);
-
- if ($defaultLanguage !== false && $this->languageExists($app, $defaultLanguage)) {
- return $defaultLanguage;
- }
-
- $lang = $this->setLanguageFromRequest($app);
- if ($userId !== null && $app === null && !$userLang) {
- $this->config->setUserValue($userId, 'core', 'lang', $lang);
+ try {
+ // Try to get the language from the Request
+ $lang = $this->getLanguageFromRequest($app);
+ if ($userId !== null && $app === null && !$userLang) {
+ $this->config->setUserValue($userId, 'core', 'lang', $lang);
+ }
+ return $lang;
+ } catch (LanguageNotFoundException $e) {
+ // Finding language from request failed fall back to default language
+ $defaultLanguage = $this->config->getSystemValue('default_language', false);
+ if ($defaultLanguage !== false && $this->languageExists($app, $defaultLanguage)) {
+ return $defaultLanguage;
+ }
}
- return $lang;
+ // We could not find any language so fall back to english
+ return 'en';
}
/**
@@ -227,10 +234,11 @@ class Factory implements IFactory {
}
/**
- * @param string|null $app App id or null for core
+ * @param string|null $app
* @return string
+ * @throws LanguageNotFoundException
*/
- public function setLanguageFromRequest($app = null) {
+ private function getLanguageFromRequest($app) {
$header = $this->request->getHeader('ACCEPT_LANGUAGE');
if ($header) {
$available = $this->findAvailableLanguages($app);
@@ -245,9 +253,6 @@ class Factory implements IFactory {
foreach ($available as $available_language) {
if ($preferred_language === strtolower($available_language)) {
- if ($app === null && !$this->requestLanguage) {
- $this->requestLanguage = $available_language;
- }
return $available_language;
}
}
@@ -255,19 +260,31 @@ class Factory implements IFactory {
// Fallback from de_De to de
foreach ($available as $available_language) {
if (substr($preferred_language, 0, 2) === $available_language) {
- if ($app === null && !$this->requestLanguage) {
- $this->requestLanguage = $available_language;
- }
return $available_language;
}
}
}
}
+ throw new LanguageNotFoundException();
+ }
+
+ /**
+ * @param string|null $app App id or null for core
+ * @return string
+ */
+ public function setLanguageFromRequest($app = null) {
+
+ try {
+ $requestLanguage = $this->getLanguageFromRequest($app);
+ } catch (LanguageNotFoundException $e) {
+ $requestLanguage = 'en';
+ }
+
if ($app === null && !$this->requestLanguage) {
- $this->requestLanguage = 'en';
+ $this->requestLanguage = $requestLanguage;
}
- return 'en'; // Last try: English
+ return $requestLanguage;
}
/**
diff --git a/lib/private/L10N/LanguageNotFoundException.php b/lib/private/L10N/LanguageNotFoundException.php
new file mode 100644
index 00000000000..175f6ab3483
--- /dev/null
+++ b/lib/private/L10N/LanguageNotFoundException.php
@@ -0,0 +1,26 @@
+<?php
+/**
+ *
+ * @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 OC\L10N;
+
+class LanguageNotFoundException extends \Exception {
+
+}
diff --git a/lib/private/Setup/PostgreSQL.php b/lib/private/Setup/PostgreSQL.php
index 3647d5bdcec..85755ea4218 100644
--- a/lib/private/Setup/PostgreSQL.php
+++ b/lib/private/Setup/PostgreSQL.php
@@ -143,7 +143,7 @@ class PostgreSQL extends AbstractDatabase {
try {
if ($this->userExists($connection)) {
// change the password
- $query = $connection->prepare("ALTER ROLE " . addslashes($this->dbUser) . " CREATEDB WITH PASSWORD '" . addslashes($this->dbPassword) . "'");
+ $query = $connection->prepare("ALTER ROLE " . addslashes($this->dbUser) . " WITH CREATEDB PASSWORD '" . addslashes($this->dbPassword) . "'");
} else {
// create the user
$query = $connection->prepare("CREATE USER " . addslashes($this->dbUser) . " CREATEDB PASSWORD '" . addslashes($this->dbPassword) . "'");
diff --git a/lib/public/AppFramework/Http/OCSResponse.php b/lib/public/AppFramework/Http/OCSResponse.php
index 0fb0f9678d0..cfda8ea4f75 100644
--- a/lib/public/AppFramework/Http/OCSResponse.php
+++ b/lib/public/AppFramework/Http/OCSResponse.php
@@ -29,11 +29,10 @@
namespace OCP\AppFramework\Http;
-use OCP\AppFramework\Http;
-
/**
* A renderer for OCS responses
* @since 8.1.0
+ * @deprecated 9.2.0 To implement an OCS endpoint extend the OCSController
*/
class OCSResponse extends Response {
@@ -53,6 +52,7 @@ class OCSResponse extends Response {
* @param int|string $itemscount
* @param int|string $itemsperpage
* @since 8.1.0
+ * @deprecated 9.2.0 To implement an OCS endpoint extend the OCSController
*/
public function __construct($format, $statuscode, $message,
$data=[], $itemscount='',
@@ -79,6 +79,7 @@ class OCSResponse extends Response {
/**
* @return string
* @since 8.1.0
+ * @deprecated 9.2.0 To implement an OCS endpoint extend the OCSController
*/
public function render() {
$r = new \OC_OCS_Result($this->data, $this->statuscode, $this->message);
diff --git a/tests/Core/Controller/TwoFactorChallengeControllerTest.php b/tests/Core/Controller/TwoFactorChallengeControllerTest.php
index 28f16241e2f..8a1cfb9edf1 100644
--- a/tests/Core/Controller/TwoFactorChallengeControllerTest.php
+++ b/tests/Core/Controller/TwoFactorChallengeControllerTest.php
@@ -77,9 +77,14 @@ class TwoFactorChallengeControllerTest extends TestCase {
->method('getProviders')
->with($user)
->will($this->returnValue($providers));
+ $this->twoFactorManager->expects($this->once())
+ ->method('getBackupProvider')
+ ->with($user)
+ ->will($this->returnValue('backup'));
$expected = new \OCP\AppFramework\Http\TemplateResponse('core', 'twofactorselectchallenge', [
'providers' => $providers,
+ 'backupProvider' => 'backup',
'redirect_url' => '/some/url',
'logout_attribute' => 'logoutAttribute',
], 'guest');
@@ -92,6 +97,9 @@ class TwoFactorChallengeControllerTest extends TestCase {
$provider = $this->getMockBuilder('\OCP\Authentication\TwoFactorAuth\IProvider')
->disableOriginalConstructor()
->getMock();
+ $backupProvider = $this->getMockBuilder('\OCP\Authentication\TwoFactorAuth\IProvider')
+ ->disableOriginalConstructor()
+ ->getMock();
$tmpl = $this->getMockBuilder('\OCP\Template')
->disableOriginalConstructor()
->getMock();
@@ -103,6 +111,16 @@ class TwoFactorChallengeControllerTest extends TestCase {
->method('getProvider')
->with($user, 'myprovider')
->will($this->returnValue($provider));
+ $this->twoFactorManager->expects($this->once())
+ ->method('getBackupProvider')
+ ->with($user)
+ ->will($this->returnValue($backupProvider));
+ $provider->expects($this->once())
+ ->method('getId')
+ ->will($this->returnValue('u2f'));
+ $backupProvider->expects($this->once())
+ ->method('getId')
+ ->will($this->returnValue('backup_codes'));
$this->session->expects($this->once())
->method('exists')
@@ -122,6 +140,7 @@ class TwoFactorChallengeControllerTest extends TestCase {
$expected = new \OCP\AppFramework\Http\TemplateResponse('core', 'twofactorshowchallenge', [
'error' => true,
'provider' => $provider,
+ 'backupProvider' => $backupProvider,
'logout_attribute' => 'logoutAttribute',
'template' => '<html/>',
], 'guest');
diff --git a/tests/lib/App/ManagerTest.php b/tests/lib/App/ManagerTest.php
index 80754413fc8..f3a91d4f5f4 100644
--- a/tests/lib/App/ManagerTest.php
+++ b/tests/lib/App/ManagerTest.php
@@ -306,7 +306,8 @@ class ManagerTest extends TestCase {
$this->appConfig->setValue('test1', 'enabled', 'yes');
$this->appConfig->setValue('test2', 'enabled', 'no');
$this->appConfig->setValue('test3', 'enabled', '["foo"]');
- $this->assertEquals(['dav', 'federatedfilesharing', 'files', 'test1', 'test3', 'workflowengine'], $this->manager->getInstalledApps());
+ $apps = ['dav', 'federatedfilesharing', 'files', 'test1', 'test3', 'twofactor_backupcodes', 'workflowengine'];
+ $this->assertEquals($apps, $this->manager->getInstalledApps());
}
public function testGetAppsForUser() {
@@ -320,7 +321,16 @@ class ManagerTest extends TestCase {
$this->appConfig->setValue('test2', 'enabled', 'no');
$this->appConfig->setValue('test3', 'enabled', '["foo"]');
$this->appConfig->setValue('test4', 'enabled', '["asd"]');
- $this->assertEquals(['dav', 'federatedfilesharing', 'files', 'test1', 'test3', 'workflowengine'], $this->manager->getEnabledAppsForUser($user));
+ $enabled = [
+ 'dav',
+ 'federatedfilesharing',
+ 'files',
+ 'test1',
+ 'test3',
+ 'twofactor_backupcodes',
+ 'workflowengine'
+ ];
+ $this->assertEquals($enabled, $this->manager->getEnabledAppsForUser($user));
}
public function testGetAppsNeedingUpgrade() {
@@ -338,6 +348,7 @@ class ManagerTest extends TestCase {
'test3' => ['id' => 'test3', 'version' => '1.2.4', 'requiremin' => '9.0.0'],
'test4' => ['id' => 'test4', 'version' => '3.0.0', 'requiremin' => '8.1.0'],
'testnoversion' => ['id' => 'testnoversion', 'requiremin' => '8.2.0'],
+ 'twofactor_backupcodes' => ['id' => 'twofactor_backupcodes'],
'workflowengine' => ['id' => 'workflowengine'],
];
@@ -379,6 +390,7 @@ class ManagerTest extends TestCase {
'test2' => ['id' => 'test2', 'version' => '1.0.0', 'requiremin' => '8.2.0'],
'test3' => ['id' => 'test3', 'version' => '1.2.4', 'requiremin' => '9.0.0'],
'testnoversion' => ['id' => 'testnoversion', 'requiremin' => '8.2.0'],
+ 'twofactor_backupcodes' => ['id' => 'twofactor_backupcodes'],
'workflowengine' => ['id' => 'workflowengine'],
];
diff --git a/tests/lib/AppTest.php b/tests/lib/AppTest.php
index d37b4a0f56a..64311f6e4ae 100644
--- a/tests/lib/AppTest.php
+++ b/tests/lib/AppTest.php
@@ -316,6 +316,7 @@ class AppTest extends \Test\TestCase {
'appforgroup12',
'dav',
'federatedfilesharing',
+ 'twofactor_backupcodes',
'workflowengine',
),
false
@@ -331,6 +332,7 @@ class AppTest extends \Test\TestCase {
'appforgroup2',
'dav',
'federatedfilesharing',
+ 'twofactor_backupcodes',
'workflowengine',
),
false
@@ -347,6 +349,7 @@ class AppTest extends \Test\TestCase {
'appforgroup2',
'dav',
'federatedfilesharing',
+ 'twofactor_backupcodes',
'workflowengine',
),
false
@@ -363,6 +366,7 @@ class AppTest extends \Test\TestCase {
'appforgroup2',
'dav',
'federatedfilesharing',
+ 'twofactor_backupcodes',
'workflowengine',
),
false,
@@ -379,6 +383,7 @@ class AppTest extends \Test\TestCase {
'appforgroup2',
'dav',
'federatedfilesharing',
+ 'twofactor_backupcodes',
'workflowengine',
),
true,
@@ -457,11 +462,11 @@ class AppTest extends \Test\TestCase {
);
$apps = \OC_App::getEnabledApps();
- $this->assertEquals(array('files', 'app3', 'dav', 'federatedfilesharing', 'workflowengine'), $apps);
+ $this->assertEquals(array('files', 'app3', 'dav', 'federatedfilesharing', 'twofactor_backupcodes', 'workflowengine'), $apps);
// mock should not be called again here
$apps = \OC_App::getEnabledApps();
- $this->assertEquals(array('files', 'app3', 'dav', 'federatedfilesharing', 'workflowengine'), $apps);
+ $this->assertEquals(array('files', 'app3', 'dav', 'federatedfilesharing', 'twofactor_backupcodes', 'workflowengine'), $apps);
$this->restoreAppConfig();
\OC_User::setUserId(null);
diff --git a/tests/lib/Authentication/TwoFactorAuth/ManagerTest.php b/tests/lib/Authentication/TwoFactorAuth/ManagerTest.php
index f9489150e21..83a74f2f253 100644
--- a/tests/lib/Authentication/TwoFactorAuth/ManagerTest.php
+++ b/tests/lib/Authentication/TwoFactorAuth/ManagerTest.php
@@ -22,54 +22,76 @@
namespace Test\Authentication\TwoFactorAuth;
-use Test\TestCase;
+use Exception;
+use OC;
+use OC\App\AppManager;
use OC\Authentication\TwoFactorAuth\Manager;
+use OCA\TwoFactor_BackupCodes\Provider\BackupCodesProvider;
+use OCP\Authentication\TwoFactorAuth\IProvider;
+use OCP\IConfig;
+use OCP\ISession;
+use OCP\IUser;
+use Test\TestCase;
class ManagerTest extends TestCase {
- /** @var \OCP\IUser|\PHPUnit_Framework_MockObject_MockObject */
+ /** @var IUser|PHPUnit_Framework_MockObject_MockObject */
private $user;
- /** @var \OC\App\AppManager|\PHPUnit_Framework_MockObject_MockObject */
+ /** @var AppManager|PHPUnit_Framework_MockObject_MockObject */
private $appManager;
- /** @var \OCP\ISession|\PHPUnit_Framework_MockObject_MockObject */
+ /** @var ISession|PHPUnit_Framework_MockObject_MockObject */
private $session;
/** @var Manager */
private $manager;
- /** @var \OCP\IConfig|\PHPUnit_Framework_MockObject_MockObject */
+ /** @var IConfig|PHPUnit_Framework_MockObject_MockObject */
private $config;
- /** @var \OCP\Authentication\TwoFactorAuth\IProvider|\PHPUnit_Framework_MockObject_MockObject */
+ /** @var IProvider|PHPUnit_Framework_MockObject_MockObject */
private $fakeProvider;
+ /** @var IProvider|PHPUnit_Framework_MockObject_MockObject */
+ private $backupProvider;
+
protected function setUp() {
parent::setUp();
- $this->user = $this->getMock('\OCP\IUser');
+ $this->user = $this->getMockBuilder('\OCP\IUser')->getMock();
$this->appManager = $this->getMockBuilder('\OC\App\AppManager')
->disableOriginalConstructor()
->getMock();
- $this->session = $this->getMock('\OCP\ISession');
- $this->config = $this->getMock('\OCP\IConfig');
+ $this->session = $this->getMockBuilder('\OCP\ISession')->getMock();
+ $this->config = $this->getMockBuilder('\OCP\IConfig')->getMock();
$this->manager = $this->getMockBuilder('\OC\Authentication\TwoFactorAuth\Manager')
->setConstructorArgs([$this->appManager, $this->session, $this->config])
->setMethods(['loadTwoFactorApp']) // Do not actually load the apps
->getMock();
- $this->fakeProvider = $this->getMock('\OCP\Authentication\TwoFactorAuth\IProvider');
+ $this->fakeProvider = $this->getMockBuilder('\OCP\Authentication\TwoFactorAuth\IProvider')->getMock();
$this->fakeProvider->expects($this->any())
->method('getId')
->will($this->returnValue('email'));
$this->fakeProvider->expects($this->any())
->method('isTwoFactorAuthEnabledForUser')
->will($this->returnValue(true));
- \OC::$server->registerService('\OCA\MyCustom2faApp\FakeProvider', function() {
+ OC::$server->registerService('\OCA\MyCustom2faApp\FakeProvider', function() {
return $this->fakeProvider;
});
+
+ $this->backupProvider = $this->getMockBuilder('\OCP\Authentication\TwoFactorAuth\IProvider')->getMock();
+ $this->backupProvider->expects($this->any())
+ ->method('getId')
+ ->will($this->returnValue('backup_codes'));
+ $this->backupProvider->expects($this->any())
+ ->method('isTwoFactorAuthEnabledForUser')
+ ->will($this->returnValue(true));
+ OC::$server->registerService('\OCA\TwoFactor_BackupCodes\Provider\FakeBackupCodesProvider', function () {
+ return $this->backupProvider;
+ });
}
private function prepareNoProviders() {
@@ -105,8 +127,40 @@ class ManagerTest extends TestCase {
->with('mycustom2faapp');
}
+ private function prepareProvidersWitBackupProvider() {
+ $this->appManager->expects($this->any())
+ ->method('getEnabledAppsForUser')
+ ->with($this->user)
+ ->will($this->returnValue([
+ 'mycustom2faapp',
+ 'twofactor_backupcodes',
+ ]));
+
+ $this->appManager->expects($this->exactly(2))
+ ->method('getAppInfo')
+ ->will($this->returnValueMap([
+ [
+ 'mycustom2faapp',
+ ['two-factor-providers' => [
+ '\OCA\MyCustom2faApp\FakeProvider',
+ ]
+ ]
+ ],
+ [
+ 'twofactor_backupcodes',
+ ['two-factor-providers' => [
+ '\OCA\TwoFactor_BackupCodes\Provider\FakeBackupCodesProvider',
+ ]
+ ]
+ ],
+ ]));
+
+ $this->manager->expects($this->exactly(2))
+ ->method('loadTwoFactorApp');
+ }
+
/**
- * @expectedException \Exception
+ * @expectedException Exception
* @expectedExceptionMessage Could not load two-factor auth provider \OCA\MyFaulty2faApp\DoesNotExist
*/
public function testFailHardIfProviderCanNotBeLoaded() {
@@ -150,6 +204,12 @@ class ManagerTest extends TestCase {
$this->assertSame($this->fakeProvider, $this->manager->getProvider($this->user, 'email'));
}
+ public function testGetBackupProvider() {
+ $this->prepareProvidersWitBackupProvider();
+
+ $this->assertSame($this->backupProvider, $this->manager->getBackupProvider($this->user));
+ }
+
public function testGetInvalidProvider() {
$this->prepareProviders();
diff --git a/tests/lib/L10N/FactoryTest.php b/tests/lib/L10N/FactoryTest.php
index bb72d84941c..5e422758cb4 100644
--- a/tests/lib/L10N/FactoryTest.php
+++ b/tests/lib/L10N/FactoryTest.php
@@ -9,6 +9,10 @@
namespace Test\L10N;
use OC\L10N\Factory;
+use OCP\IConfig;
+use OCP\IRequest;
+use OCP\IUser;
+use OCP\IUserSession;
use Test\TestCase;
/**
@@ -18,13 +22,13 @@ use Test\TestCase;
*/
class FactoryTest extends TestCase {
- /** @var \OCP\IConfig|\PHPUnit_Framework_MockObject_MockObject */
+ /** @var IConfig|\PHPUnit_Framework_MockObject_MockObject */
protected $config;
- /** @var \OCP\IRequest|\PHPUnit_Framework_MockObject_MockObject */
+ /** @var IRequest|\PHPUnit_Framework_MockObject_MockObject */
protected $request;
- /** @var \OCP\IUserSession|\PHPUnit_Framework_MockObject_MockObject */
+ /** @var IUserSession|\PHPUnit_Framework_MockObject_MockObject */
protected $userSession;
/** @var string */
@@ -33,17 +37,15 @@ class FactoryTest extends TestCase {
public function setUp() {
parent::setUp();
- /** @var \OCP\IConfig $request */
- $this->config = $this->getMockBuilder('OCP\IConfig')
+ $this->config = $this->getMockBuilder(IConfig::class)
->disableOriginalConstructor()
->getMock();
- /** @var \OCP\IRequest $request */
- $this->request = $this->getMockBuilder('OCP\IRequest')
+ $this->request = $this->getMockBuilder(IRequest::class)
->disableOriginalConstructor()
->getMock();
- $this->userSession = $this->getMockBuilder('\OCP\IUserSession')
+ $this->userSession = $this->getMockBuilder(IUserSession::class)
->disableOriginalConstructor()
->getMock();
@@ -56,7 +58,7 @@ class FactoryTest extends TestCase {
*/
protected function getFactory(array $methods = []) {
if (!empty($methods)) {
- return $this->getMockBuilder('OC\L10N\Factory')
+ return $this->getMockBuilder(Factory::class)
->setConstructorArgs([
$this->config,
$this->request,
@@ -111,7 +113,7 @@ class FactoryTest extends TestCase {
->method('getSystemValue')
->with('installed', false)
->willReturn(true);
- $user = $this->getMockBuilder('\OCP\IUser')
+ $user = $this->getMockBuilder(IUser::class)
->getMock();
$user->expects($this->once())
->method('getUID')
@@ -145,7 +147,7 @@ class FactoryTest extends TestCase {
->method('getSystemValue')
->with('installed', false)
->willReturn(true);
- $user = $this->getMockBuilder('\OCP\IUser')
+ $user = $this->getMockBuilder(IUser::class)
->getMock();
$user->expects($this->once())
->method('getUID')
@@ -188,7 +190,7 @@ class FactoryTest extends TestCase {
->method('getSystemValue')
->with('installed', false)
->willReturn(true);
- $user = $this->getMockBuilder('\OCP\IUser')
+ $user = $this->getMockBuilder(IUser::class)
->getMock();
$user->expects($this->once())
->method('getUID')
@@ -234,7 +236,7 @@ class FactoryTest extends TestCase {
->method('getSystemValue')
->with('installed', false)
->willReturn(true);
- $user = $this->getMockBuilder('\OCP\IUser')
+ $user = $this->getMockBuilder(IUser::class)
->getMock();
$user->expects($this->once())
->method('getUID')
@@ -460,4 +462,86 @@ class FactoryTest extends TestCase {
$fn = $factory->createPluralFunction($function);
$this->assertEquals($expected, $fn($count));
}
+
+ public function dataFindLanguage() {
+ return [
+ // Not logged in
+ [false, [], 'en'],
+ [false, ['fr'], 'fr'],
+ [false, ['de', 'fr'], 'de'],
+ [false, ['nl', 'de', 'fr'], 'de'],
+
+ [true, [], 'en'],
+ [true, ['fr'], 'fr'],
+ [true, ['de', 'fr'], 'de'],
+ [true, ['nl', 'de', 'fr'], 'nl'],
+ ];
+ }
+
+ /**
+ * @dataProvider dataFindLanguage
+ *
+ * @param bool $loggedIn
+ * @param array $availableLang
+ * @param string $expected
+ */
+ public function testFindLanguage($loggedIn, $availableLang, $expected) {
+ $userLang = 'nl';
+ $browserLang = 'de';
+ $defaultLang = 'fr';
+
+ $this->config->expects($this->any())
+ ->method('getSystemValue')
+ ->will($this->returnCallback(function($var, $default) use ($defaultLang) {
+ if ($var === 'installed') {
+ return true;
+ } else if ($var === 'default_language') {
+ return $defaultLang;
+ } else {
+ return $default;
+ }
+ }));
+
+ if ($loggedIn) {
+ $user = $this->getMockBuilder(IUser::class)
+ ->getMock();
+ $user->expects($this->any())
+ ->method('getUID')
+ ->willReturn('MyUserUid');
+ $this->userSession
+ ->expects($this->any())
+ ->method('getUser')
+ ->willReturn($user);
+ $this->config->expects($this->any())
+ ->method('getUserValue')
+ ->with('MyUserUid', 'core', 'lang', null)
+ ->willReturn($userLang);
+ } else {
+ $this->userSession
+ ->expects($this->any())
+ ->method('getUser')
+ ->willReturn(null);
+ }
+
+ $this->request->expects($this->any())
+ ->method('getHeader')
+ ->with($this->equalTo('ACCEPT_LANGUAGE'))
+ ->willReturn($browserLang);
+
+ $factory = $this->getFactory(['languageExists', 'findAvailableLanguages']);
+ $factory->expects($this->any())
+ ->method('languageExists')
+ ->will($this->returnCallback(function ($app, $lang) use ($availableLang) {
+ return in_array($lang, $availableLang);
+ }));
+ $factory->expects($this->any())
+ ->method('findAvailableLanguages')
+ ->will($this->returnCallback(function ($app) use ($availableLang) {
+ return $availableLang;
+ }));
+
+ $lang = $factory->findLanguage(null);
+ $this->assertSame($expected, $lang);
+
+ }
}
diff --git a/version.php b/version.php
index 8cf0822bc7e..01a822708e3 100644
--- a/version.php
+++ b/version.php
@@ -25,7 +25,7 @@
// We only can count up. The 4. digit is only for the internal patchlevel to trigger DB upgrades
// between betas, final and RCs. This is _not_ the public version number. Reset minor/patchlevel
// when updating major/minor version number.
-$OC_Version = array(9, 2, 0, 2);
+$OC_Version = array(9, 2, 0, 3);
// The human readable string
$OC_VersionString = '11.0 alpha';