diff --git a/.eslintrc.json b/.eslintrc.json
index 96060c6b..c498c3d2 100644
--- a/.eslintrc.json
+++ b/.eslintrc.json
@@ -1,7 +1,7 @@
{
"root": true,
"extends": [
- "wikimedia/client-es5",
+ "wikimedia/client",
"wikimedia/jquery",
"wikimedia/mediawiki"
]
diff --git a/Gruntfile.js b/Gruntfile.js
index ea9a15ab..b431894c 100644
--- a/Gruntfile.js
+++ b/Gruntfile.js
@@ -1,6 +1,6 @@
/* eslint-env node */
module.exports = function ( grunt ) {
- var conf = grunt.file.readJSON( 'extension.json' );
+ const conf = grunt.file.readJSON( 'extension.json' );
grunt.loadNpmTasks( 'grunt-banana-checker' );
grunt.loadNpmTasks( 'grunt-eslint' );
diff --git a/extension.json b/extension.json
index f72d0791..5cd44c30 100644
--- a/extension.json
+++ b/extension.json
@@ -98,16 +98,25 @@
}
},
"ResourceModules": {
- "ext.oath.totp.showqrcode.styles": {
+ "ext.oath.styles": {
"class": "MediaWiki\\ResourceLoader\\CodexModule",
"styles": [
- "totp/ext.oath.showqrcode.styles.less"
+ "totp/ext.oath.showqrcode.styles.less",
+ "recovery/ext.oauth.recovery.less"
],
"codexStyleOnly": "true",
"codexComponents": [
"CdxButton",
"CdxIcon"
]
+ },
+ "ext.oath": {
+ "packageFiles": [
+ "recovery/ext.oath.recovery.copy.js"
+ ],
+ "messages": [
+ "oathauth-recoverycodes-copy-success"
+ ]
}
},
"ResourceFileModulePaths": {
diff --git a/i18n/en.json b/i18n/en.json
index 3f824858..0838c421 100644
--- a/i18n/en.json
+++ b/i18n/en.json
@@ -16,7 +16,9 @@
"oathauth-recoverycodes-important": "This step is important! Do not skip this step!",
"oathauth-recoverycodes-neveragain": "These codes will never be shown again!",
"oathauth-recoverytokens-createdat": "Recovery tokens created: $1",
- "oathauth-recoverycodes-download": "Download recovery codes",
+ "oathauth-recoverycodes-download": "Download",
+ "oathauth-recoverycodes-copy": "Copy",
+ "oathauth-recoverycodes-copy-success": "Recovery codes were copied to your clipboard!",
"oathauth-disable": "Disable two-factor authentication",
"oathauth-validatedoath": "Validated two-factor credentials. Two-factor authentication will now be enforced.",
"oathauth-noscratchforvalidation": "You cannot use a recovery code to confirm two-factor authentication. Recovery codes are for backup and incidental use only. Please use a code from your two-factor authentication application (such as [https://authy.com/ Authy], [https://freeotp.github.io/ FreeOTP], [https://support.google.com/accounts/answer/1066447 Google Authenticator], [https://www.microsoft.com/en/security/mobile-authenticator-app Microsoft Authenticator] or [https://1password.com/ 1Password]).",
diff --git a/i18n/qqq.json b/i18n/qqq.json
index fbd03e48..eefa8814 100644
--- a/i18n/qqq.json
+++ b/i18n/qqq.json
@@ -32,6 +32,8 @@
"oathauth-recoverycodes-neveragain": "Plain text, found on Special:OATH while enabling OATH.",
"oathauth-recoverytokens-createdat": "Plain text, found on Special:OATH while enabling OATH.",
"oathauth-recoverycodes-download": "Plain text, text of the download link on Special:OATH while enabling OATH.",
+ "oathauth-recoverycodes-copy": "Plain text, label of the button on Special:OATH to copy the recovery codes",
+ "oathauth-recoverycodes-copy-success": "Notification bubble presented on Special:OATH after copying recovery codes",
"oathauth-disable": "Page title on Special:OATH while disabling OATH.\n\nSee [https://en.wikipedia.org/wiki/Two_factor_authentication two factor authentication]",
"oathauth-validatedoath": "Plain text found on Special:OATH after a token has been validated.\n\nSee [https://en.wikipedia.org/wiki/Two_factor_authentication two factor authentication]",
"oathauth-noscratchforvalidation": "Plain text found on Special:OATH if the user used the incorrect type of token while enabling OATH.\n\nSee [https://en.wikipedia.org/wiki/Two_factor_authentication two factor authentication]",
diff --git a/modules/recovery/ext.oath.recovery.copy.js b/modules/recovery/ext.oath.recovery.copy.js
new file mode 100644
index 00000000..08f1da3a
--- /dev/null
+++ b/modules/recovery/ext.oath.recovery.copy.js
@@ -0,0 +1,40 @@
+/**
+ * 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, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ */
+
+class CopyButton {
+
+ static attach() {
+ // eslint-disable-next-line no-jquery/no-global-selector
+ $( '.mw-oathauth-recoverycodes-copy-button' )
+ .addClass( 'clipboard-api-supported' )
+ .on( 'click', ( e ) => {
+ e.preventDefault();
+ // eslint-disable-next-line compat/compat
+ navigator.clipboard.writeText( mw.config.get( 'oathauth-recoverycodes' ) ).then( () => {
+ mw.notify( mw.msg( 'oathauth-recoverycodes-copy-success' ), {
+ type: 'success',
+ tag: 'recoverycodes'
+ } );
+ } );
+ } );
+ }
+}
+
+if ( navigator.clipboard && navigator.clipboard.writeText ) {
+ // navigator.clipboard() is not supported in Safari 11.1, iOS Safari 11.3-11.4
+ $( CopyButton.attach );
+}
diff --git a/modules/recovery/ext.oauth.recovery.less b/modules/recovery/ext.oauth.recovery.less
new file mode 100644
index 00000000..4060b6c9
--- /dev/null
+++ b/modules/recovery/ext.oauth.recovery.less
@@ -0,0 +1,21 @@
+@import 'mediawiki.skin.variables.less';
+
+.mw-oathauth-recoverycodes-download-icon {
+ .cdx-mixin-css-icon( @cdx-icon-download, @param-is-button-icon: true );
+}
+
+.mw-oathauth-recoverycodes-copy-button {
+ &.cdx-button {
+ margin-right: @spacing-horizontal-button;
+ margin-inline-end: @spacing-horizontal-button;
+ }
+
+ .cdx-button__icon {
+ .cdx-mixin-css-icon( @cdx-icon-copy, @param-is-button-icon: true );
+ }
+
+ .client-nojs &,
+ &:not( .clipboard-api-supported ) {
+ display: none;
+ }
+}
diff --git a/modules/totp/ext.oath.showqrcode.styles.less b/modules/totp/ext.oath.showqrcode.styles.less
index ef5ba958..62afabef 100644
--- a/modules/totp/ext.oath.showqrcode.styles.less
+++ b/modules/totp/ext.oath.showqrcode.styles.less
@@ -1,5 +1,3 @@
-@import 'mediawiki.skin.variables.less';
-
kbd {
font-family: monospace, monospace;
white-space: nowrap;
@@ -9,11 +7,3 @@ kbd {
fieldset {
page-break-inside: avoid;
}
-
-.cdx-button.mw-oathauth-recoverycodes-download {
- margin-top: 1em;
-}
-
-.mw-oathauth-recoverycodes-download-icon {
- .cdx-mixin-css-icon( @cdx-icon-download, @param-is-button-icon: true );
-}
diff --git a/src/HTMLForm/TOTPEnableForm.php b/src/HTMLForm/TOTPEnableForm.php
index ea6b65ac..b04a90ac 100644
--- a/src/HTMLForm/TOTPEnableForm.php
+++ b/src/HTMLForm/TOTPEnableForm.php
@@ -20,7 +20,9 @@ class TOTPEnableForm extends OATHAuthOOUIHTMLForm {
* @return string
*/
public function getHTML( $submitResult ) {
- $this->getOutput()->addModuleStyles( 'ext.oath.totp.showqrcode.styles' );
+ $out = $this->getOutput();
+ $out->addModuleStyles( 'ext.oath.styles' );
+ $out->addModules( 'ext.oath' );
return parent::getHTML( $submitResult );
}
@@ -69,6 +71,8 @@ class TOTPEnableForm extends OATHAuthOOUIHTMLForm {
->build();
$now = wfTimestampNow();
+ $recoveryCodes = $this->getScratchTokensForDisplay( $key );
+ $this->getOutput()->addJsConfigVars( 'oathauth-recoverycodes', $this->createTextList( $recoveryCodes ) );
// messages used: oathauth-step1, oathauth-step2, oathauth-step3, oathauth-step4
return [
@@ -115,9 +119,10 @@ class TOTPEnableForm extends OATHAuthOOUIHTMLForm {
. $this->msg( 'word-separator' )->escaped()
. $this->msg( 'parentheses' )->rawParams( wfTimestamp( TS_ISO_8601, $now ) )->escaped()
) . '
' .
- $this->createResourceList( $this->getScratchTokensForDisplay( $key ) ) . '
' .
+ $this->createResourceList( $recoveryCodes ) . '
' .
'' . $this->msg( 'oathauth-recoverycodes-neveragain' )->escaped() . '
' .
- $this->createDownloadLink( $this->getScratchTokensForDisplay( $key ) ),
+ $this->createCopyButton() .
+ $this->createDownloadLink( $recoveryCodes ),
'raw' => true,
'section' => 'step3',
],
@@ -146,6 +151,15 @@ class TOTPEnableForm extends OATHAuthOOUIHTMLForm {
return Html::rawElement( 'ul', [], $resourceList );
}
+ /**
+ * @param array $items
+ *
+ * @return string
+ */
+ private function createTextList( $items ) {
+ return "* " . implode( "\n* ", $items );
+ }
+
private function createDownloadLink( array $scratchTokensForDisplay ): string {
$icon = Html::element( 'span', [
'class' => [ 'mw-oathauth-recoverycodes-download-icon', 'cdx-button__icon' ],
@@ -167,6 +181,16 @@ class TOTPEnableForm extends OATHAuthOOUIHTMLForm {
);
}
+ private function createCopyButton(): string {
+ return Html::rawElement( 'button', [
+ 'class' => 'cdx-button mw-oathauth-recoverycodes-copy-button'
+ ], Html::element( 'span', [
+ 'class' => 'cdx-button__icon',
+ 'aria-hidden' => 'true',
+ ] ) . $this->msg( 'oathauth-recoverycodes-copy' )->escaped()
+ );
+ }
+
/**
* Retrieve the current secret for display purposes
*