Skip to content

Commit

Permalink
Fix Backup code download with quotes in translations (#494)
Browse files Browse the repository at this point in the history
* Corrects the download URL when quotes are included in the translation
* Generate the download URL server-side to centralise the generation and allow for testing
* Add unit tests to verify the Backup Code options work
  • Loading branch information
dd32 authored Nov 28, 2022
1 parent 43f2e4f commit 555d886
Show file tree
Hide file tree
Showing 2 changed files with 137 additions and 16 deletions.
43 changes: 27 additions & 16 deletions providers/class-two-factor-backup-codes.php
Original file line number Diff line number Diff line change
Expand Up @@ -176,16 +176,7 @@ public function user_options( $user ) {

// Update counter.
$( '.two-factor-backup-codes-count' ).html( response.data.i18n.count );

// Build the download link.
var txt_data = 'data:application/text;charset=utf-8,' + '\n';
txt_data += response.data.i18n.title.replace( /%s/g, document.domain ) + '\n\n';

for ( i = 0; i < response.data.codes.length; i++ ) {
txt_data += i + 1 + '. ' + response.data.codes[ i ] + '\n';
}

$( '#two-factor-backup-codes-download-link' ).attr( 'href', encodeURI( txt_data ) );
$( '#two-factor-backup-codes-download-link' ).attr( 'href', response.data.download_link );
}
} );
} );
Expand Down Expand Up @@ -238,24 +229,44 @@ public function generate_codes( $user, $args = '' ) {
* @since 0.1-dev
*/
public function ajax_generate_json() {
$user = get_user_by( 'id', filter_input( INPUT_POST, 'user_id', FILTER_SANITIZE_NUMBER_INT ) );
$user_id = 0;
if ( ! empty( $_POST['user_id'] ) ) {
$user_id = absint( $_POST['user_id'] );
}

$user = get_user_by( 'id', $user_id );
check_ajax_referer( 'two-factor-backup-codes-generate-json-' . $user->ID, 'nonce' );

// Setup the return data.
$codes = $this->generate_codes( $user );
$count = self::codes_remaining_for_user( $user );
$i18n = array(
$title = sprintf(
/* translators: %s: the site's domain */
__( 'Two-Factor Backup Codes for %s', 'two-factor' ),
home_url( '/' )
);

// Generate download content.
$download_link = 'data:application/text;charset=utf-8,';
$download_link .= rawurlencode( "{$title}\r\n\r\n" );

$i = 1;
foreach ( $codes as $code ) {
$download_link .= rawurlencode( "{$i}. {$code}\r\n" );
$i++;
}

$i18n = array(
/* translators: %s: count */
'count' => esc_html( sprintf( _n( '%s unused code remaining.', '%s unused codes remaining.', $count, 'two-factor' ), $count ) ),
/* translators: %s: the site's domain */
'title' => esc_html__( 'Two-Factor Backup Codes for %s', 'two-factor' ),
);

// Send the response.
wp_send_json_success(
array(
'codes' => $codes,
'i18n' => $i18n,
'codes' => $codes,
'download_link' => $download_link,
'i18n' => $i18n,
)
);
}
Expand Down
110 changes: 110 additions & 0 deletions tests/providers/class-two-factor-backup-codes-ajax.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
<?php
/**
* Test Two Factor Backup Codes.
*
* @package Two_Factor
*/

/**
* Class Tests_Two_Factor_Backup_Codes_AJAX
*
* @package Two_Factor
* @group providers
*/
class Tests_Two_Factor_Backup_Codes_AJAX extends WP_Ajax_UnitTestCase {

/**
* Instance of our provider class.
*
* @var Two_Factor_Backup_Codes
*/
protected $provider;

/**
* Set up a test case.
*
* @see WP_UnitTestCase_Base::set_up()
*/
public function set_up() {
parent::set_up();
$this->provider = Two_Factor_Backup_Codes::get_instance();
}

/**
* Verify that the downloaded file contains the codes.
*
* @covers Two_Factor_Backup_Codes::ajax_generate_json
*/
public function test_generate_code_and_validate_in_download_file() {
$this->_setRole( 'administrator' );

$user = wp_get_current_user();
$_POST['user_id'] = $user->ID;
$_POST['nonce'] = wp_create_nonce( 'two-factor-backup-codes-generate-json-' . $user->ID );

try {
$this->_handleAjax( 'two_factor_backup_codes_generate' );
} catch ( WPAjaxDieContinueException $e ) {
unset( $e );
}

$this->assertStringContainsString( 'download_link', $this->_last_response );

$response = json_decode( $this->_last_response );

$this->assertTrue( $response->success );
$this->assertNotEmpty( $response->data->codes );
$this->assertTrue( $this->provider->validate_code( $user, $response->data->codes[0] ) );
$this->assertStringContainsString( $response->data->codes[0], $response->data->download_link );
}

/**
* Verify that a different user cannot generate codes for another.
*
* @covers Two_Factor_Backup_Codes::ajax_generate_json
*/
public function test_cannot_generate_code_for_different_user() {
$this->_setRole( 'administrator' );

$user = wp_get_current_user();
$_POST['nonce'] = wp_create_nonce( 'two-factor-backup-codes-generate-json-' . $user->ID );

// Create a new user
$user = new WP_User( self::factory()->user->create() );
$_POST['user_id'] = $user->ID;

$this->expectException( 'WPAjaxDieStopException' );
$this->expectExceptionMessage( '-1' );
$this->_handleAjax( 'two_factor_backup_codes_generate' );
}

/**
* Verify that an admin can create Backup codes for another user.
*
* @covers Two_Factor_Backup_Codes::ajax_generate_json
*/
public function test_generate_codes_for_other_users() {
$this->_setRole( 'administrator' );

$current_user = wp_get_current_user();
$user = new WP_User( self::factory()->user->create() );
$_POST['user_id'] = $user->ID;
$_POST['nonce'] = wp_create_nonce( 'two-factor-backup-codes-generate-json-' . $user->ID );

try {
$this->_handleAjax( 'two_factor_backup_codes_generate' );
} catch ( WPAjaxDieContinueException $e ) {
unset( $e );
}

$this->assertStringContainsString( 'codes', $this->_last_response );

$response = json_decode( $this->_last_response );

$this->assertTrue( $response->success );
$this->assertNotEmpty( $response->data->codes );

$this->assertFalse( $this->provider->validate_code( $current_user, $response->data->codes[0] ) );
$this->assertTrue( $this->provider->validate_code( $user, $response->data->codes[0] ) );
}
}

0 comments on commit 555d886

Please sign in to comment.