diff --git a/extension.json b/extension.json
index a4733c18..a5d4b251 100644
--- a/extension.json
+++ b/extension.json
@@ -102,8 +102,14 @@
},
"ResourceModules": {
"ext.oath.totp.showqrcode.styles": {
+ "class": "MediaWiki\\ResourceLoader\\CodexModule",
"styles": [
- "totp/ext.oath.showqrcode.styles.css"
+ "totp/ext.oath.showqrcode.styles.less"
+ ],
+ "codexStyleOnly": "true",
+ "codexComponents": [
+ "CdxButton",
+ "CdxIcon"
]
}
},
diff --git a/i18n/en.json b/i18n/en.json
index 6e5a39e5..c90709a1 100644
--- a/i18n/en.json
+++ b/i18n/en.json
@@ -14,6 +14,7 @@
"oathauth-enable": "Enable two-factor authentication",
"oathauth-recoverycodes": "The following list is a list of one-time use recovery codes. These codes can only be used once, and are for emergency use when you don't have access to your device. Please write these down and keep them in a secure location. It is recommended that you mark each code as used when you have logged in using it. If you lose your device, these codes are the only way to rescue your account.\n\n'''These codes will never be shown again'''.",
"oathauth-recoverycodes-important": "This step is important! Do not skip this step!",
+ "oathauth-recoverycodes-download": "Download recovery codes",
"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 Google Authenticator).",
diff --git a/i18n/qqq.json b/i18n/qqq.json
index fe35c763..1042e79e 100644
--- a/i18n/qqq.json
+++ b/i18n/qqq.json
@@ -29,6 +29,7 @@
"oathauth-enable": "Page title on Special:OATH, when enabling OATH.\n\nSee [https://en.wikipedia.org/wiki/Two_factor_authentication two factor authentication]",
"oathauth-recoverycodes": "Plain text, found on Special:OATH while enabling OATH.",
"oathauth-recoverycodes-important": "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-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/totp/ext.oath.showqrcode.styles.css b/modules/totp/ext.oath.showqrcode.styles.css
deleted file mode 100644
index 62afabef..00000000
--- a/modules/totp/ext.oath.showqrcode.styles.css
+++ /dev/null
@@ -1,9 +0,0 @@
-kbd {
- font-family: monospace, monospace;
- white-space: nowrap;
- font-size: larger;
-}
-
-fieldset {
- page-break-inside: avoid;
-}
diff --git a/modules/totp/ext.oath.showqrcode.styles.less b/modules/totp/ext.oath.showqrcode.styles.less
new file mode 100644
index 00000000..0484f947
--- /dev/null
+++ b/modules/totp/ext.oath.showqrcode.styles.less
@@ -0,0 +1,18 @@
+@import "mediawiki.skin.variables.less";
+
+kbd {
+ font-family: monospace, monospace;
+ white-space: nowrap;
+ font-size: larger;
+}
+
+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 d79137c7..2b391026 100644
--- a/src/HTMLForm/TOTPEnableForm.php
+++ b/src/HTMLForm/TOTPEnableForm.php
@@ -105,7 +105,8 @@ class TOTPEnableForm extends OATHAuthOOUIHTMLForm {
'default' =>
'' . $this->msg( 'oathauth-recoverycodes-important' )->escaped() . '
'
. $this->msg( 'oathauth-recoverycodes' )->parse()
- . $this->createResourceList( $this->getScratchTokensForDisplay( $key ) ),
+ . $this->createResourceList( $this->getScratchTokensForDisplay( $key ) )
+ . $this->createDownloadLink( $this->getScratchTokensForDisplay( $key ) ),
'raw' => true,
'section' => 'step3',
],
@@ -134,6 +135,27 @@ class TOTPEnableForm extends OATHAuthOOUIHTMLForm {
return Html::rawElement( 'ul', [], $resourceList );
}
+ private function createDownloadLink( array $scratchTokensForDisplay ): string {
+ $icon = Html::element( 'span', [
+ 'class' => [ 'mw-oathauth-recoverycodes-download-icon', 'cdx-button__icon' ],
+ 'aria-hidden' => 'true',
+ ] );
+ return Html::rawElement(
+ 'a',
+ [
+ 'href' => 'data:text/plain;charset=utf-8,'
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=1895687
+ . rawurlencode( implode( PHP_EOL, $scratchTokensForDisplay ) ),
+ 'download' => 'recovery-codes.txt',
+ 'class' => [
+ 'mw-oathauth-recoverycodes-download',
+ 'cdx-button', 'cdx-button--fake-button', 'cdx-button--fake-button--enabled',
+ ],
+ ],
+ $icon . $this->msg( 'oathauth-recoverycodes-download' )->escaped()
+ );
+ }
+
/**
* Retrieve the current secret for display purposes
*