Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
155 changes: 154 additions & 1 deletion src/js/_enqueues/admin/application-passwords.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
( function( $ ) {
var $appPassSection = $( '#application-passwords-section' ),
$newAppPassForm = $appPassSection.find( '.create-application-password' ),
$newAppPassField = $newAppPassForm.find( '.input' ),
$newAppPassField = $newAppPassForm.find( '#new_application_password_name' ),
$newAppPassExpiresField = $newAppPassForm.find( '#new_application_password_expires' ),
$newAppPassButton = $newAppPassForm.find( '.button' ),
$appPassTwrapper = $appPassSection.find( '.application-passwords-list-table-wrapper' ),
$appPassTbody = $appPassSection.find( 'tbody' ),
Expand All @@ -20,6 +21,11 @@
tmplAppPassRow = wp.template( 'application-password-row' ),
userId = $( '#user_id' ).val();

updateApplicationPasswordExpirationMinimum( $newAppPassExpiresField );
$newAppPassExpiresField.on( 'focus', function() {
updateApplicationPasswordExpirationMinimum( $newAppPassExpiresField );
} );

$newAppPassButton.on( 'click', function( e ) {
e.preventDefault();

Expand All @@ -34,13 +40,27 @@
return;
}

updateApplicationPasswordExpirationMinimum( $newAppPassExpiresField );
if ( ! $newAppPassExpiresField[0].reportValidity() ) {
return;
}

clearNotices();
$newAppPassButton.prop( 'aria-disabled', true ).addClass( 'disabled' );

var request = {
name: name
};

var expires = $newAppPassExpiresField.val();
if ( expires ) {
var expiresDate = wp.date.getDate( expires );

if ( ! isNaN( expiresDate.getTime() ) ) {
request.expires = expiresDate.toISOString();
}
}

/**
* Filters the request data used to create a new Application Password.
*
Expand All @@ -59,6 +79,7 @@
$newAppPassButton.removeProp( 'aria-disabled' ).removeClass( 'disabled' );
} ).done( function( response ) {
$newAppPassField.val( '' );
$newAppPassExpiresField.val( '' );
$newAppPassButton.prop( 'disabled', false );

$newAppPassForm.after( tmplNewAppPass( {
Expand All @@ -82,6 +103,127 @@
*/
wp.hooks.doAction( 'wp_application_passwords_created_password', response, request );
} ).fail( handleErrorResponse );
});

/**
* Handles the inline editing of an application password expiration date.
* * @since 7.1.0
*/
$appPassTbody.on( 'click', '.edit-expires', function( e ) {
e.preventDefault();

var $button = $( this ),
$tr = $button.closest( 'tr' ),
uuid = $tr.data( 'uuid' ),
currentExpires = $tr.data( 'expires' ),
$td = $button.closest( 'td' );

if ( $td.find( '.edit-expires-form' ).length ) {
return;
}

var $form = $( '<div class="edit-expires-form"></div>' );
var timezoneDescriptionId = 'application-password-timezone-' + uuid;
var $input = $( '<input type="datetime-local" class="edit-expires-input" step="1" />' )
.attr( 'aria-label', wp.i18n.__( 'Expiration date and time' ) )
.attr( 'aria-describedby', timezoneDescriptionId );

if ( currentExpires ) {
$input.val( wp.date.date( 'Y-m-d\\TH:i:s', currentExpires + 'Z' ) );
}
var initialExpires = $input.val();
$input.on( 'focus', function() {
updateApplicationPasswordExpirationMinimum( $input );
} );

var $buttonContainer = $( '<div class="edit-expires-button-group"></div>' );
var $saveBtn = $( '<button type="button" class="button button-small button-primary">' + wp.i18n.__( 'Save' ) + '</button>' );
var $cancelBtn = $( '<button type="button" class="button button-small">' + wp.i18n.__( 'Cancel' ) + '</button>' );
var $removeBtn = $( '<button type="button" class="button button-small button-link button-link-delete"></button>' ).text( wp.i18n.__( 'Remove expiry' ) );

$buttonContainer.append( $saveBtn ).append( $cancelBtn );
if ( currentExpires ) {
$buttonContainer.append( $removeBtn );
}
$form.append( $input )
.append( $( '<p class="description"></p>' ).attr( 'id', timezoneDescriptionId ).text(
/* translators: %s: Site timezone. */
wp.i18n.sprintf( wp.i18n.__( 'Timezone: %s' ), $appPassSection.data( 'timezone' ) )
) )
.append( $buttonContainer );

$td.append( $form );
$button.hide();
$input.trigger( 'focus' );

// Close form on Escape key.
$input.on( 'keydown', function( e ) {
if ( 27 === e.which ) {
$cancelBtn.trigger( 'click' );
}
if ( 13 === e.which ) {
e.preventDefault();
$saveBtn.trigger( 'click' );
}
});

$cancelBtn.on( 'click', function() {
if ( $cancelBtn.prop( 'disabled' ) ) {
return;
}

$form.remove();
$button.show().trigger( 'focus' );
} );

$removeBtn.on( 'click', function() {
$input.val( '' ).trigger( 'focus' );
$saveBtn.trigger( 'click' );
} );

$saveBtn.on( 'click', function() {
if ( $saveBtn.prop( 'disabled' ) ) {
return;
}

updateApplicationPasswordExpirationMinimum( $input );
var newExpires = $input.val();

// An existing expired value may be retained, but a newly selected time must be in the future.
if ( ! ( currentExpires && newExpires === initialExpires ) && ! $input[0].reportValidity() ) {
return;
}

var expiresDate = newExpires ? wp.date.getDate( newExpires ) : null;
var requestData = {
expires: ( expiresDate && ! isNaN( expiresDate.getTime() ) ) ? expiresDate.toISOString() : null
};

if ( currentExpires && newExpires === initialExpires ) {
requestData.expires = currentExpires + 'Z';
}

clearNotices();
$saveBtn.prop( 'disabled', true );
$cancelBtn.prop( 'disabled', true );
$removeBtn.prop( 'disabled', true );

wp.apiRequest( {
path: '/wp/v2/users/' + userId + '/application-passwords/' + uuid + '?_locale=user',
method: 'PUT',
data: JSON.stringify( requestData ),
contentType: 'application/json'
} ).always( function() {
$saveBtn.prop( 'disabled', false );
$cancelBtn.prop( 'disabled', false );
$removeBtn.prop( 'disabled', false );
} ).done( function( response ) {
var $newRow = $( tmplAppPassRow( response ) );
$tr.replaceWith( $newRow );
$newRow.find( '.edit-expires' ).trigger( 'focus' );
addNotice( wp.i18n.__( 'Application password expiration updated.' ), 'success' );
} ).fail( handleErrorResponse );
} );
} );

$appPassTbody.on( 'click', '.delete', function( e ) {
Expand Down Expand Up @@ -186,6 +328,17 @@
addNotice( errorMessage, 'error' );
}

/**
* Updates the earliest expiration allowed in an application password input.
*
* @since 7.2.0
*
* @param {jQuery} $input The expiration input.
*/
function updateApplicationPasswordExpirationMinimum( $input ) {
$input.attr( 'min', wp.date.date( 'Y-m-d\\TH:i:s', new Date( Date.now() + 1000 ) ) );
}

/**
* Displays a message in the Application Passwords section.
*
Expand Down
23 changes: 23 additions & 0 deletions src/wp-admin/css/forms.css
Original file line number Diff line number Diff line change
Expand Up @@ -1064,6 +1064,29 @@ table.form-table td .updated p {
max-width: 20em;
}

.edit-expires-form {
margin-top: 8px;
width: 100%;
}

.edit-expires-input {
display: block;
width: 100%;
max-width: 20em;
margin-bottom: 5px;
}

.edit-expires-button-group {
display: flex;
flex-wrap: wrap;
gap: 4px;
justify-content: flex-start;
}

.column-expires {
vertical-align: top;
}

.auth-app-card.card {
max-width: 768px;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ public function get_columns() {
'created' => __( 'Created' ),
'last_used' => __( 'Last Used' ),
'last_ip' => __( 'Last IP' ),
'expires' => __( 'Expires' ),
'revoke' => __( 'Revoke' ),
);
}
Expand Down Expand Up @@ -101,6 +102,35 @@ public function column_last_ip( $item ) {
}
}

/**
* Handles the expires column output.
*
* @since 7.1.0
*
* @param array $item The current application password item.
*/
public function column_expires( $item ) {
if ( empty( $item['expires'] ) ) {
echo '&mdash;';
} else {
$date = wp_date( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), $item['expires'] );
if ( time() > $item['expires'] ) {
printf(
'%s',
/* translators: %s: Expiration date and time for the Application Password. */
sprintf( esc_html__( 'Expired on %s' ), esc_html( $date ) )
);
} else {
echo esc_html( $date );
}
}
printf(
'<br><button type="button" class="button-link edit-expires" aria-label="%s">%s</button>',
esc_attr__( 'Edit Expiration Date and Time' ),
esc_html__( 'Edit Expiry' )
);
}

/**
* Handles the revoke column output.
*
Expand Down Expand Up @@ -178,7 +208,8 @@ protected function display_tablenav( $which ) {
* @param array $item The current item.
*/
public function single_row( $item ) {
echo '<tr data-uuid="' . esc_attr( $item['uuid'] ) . '">';
$expires = ! empty( $item['expires'] ) ? gmdate( 'Y-m-d\TH:i:s', $item['expires'] ) : '';
echo '<tr data-uuid="' . esc_attr( $item['uuid'] ) . '" data-expires="' . esc_attr( $expires ) . '">';
$this->single_row_columns( $item );
echo '</tr>';
}
Expand All @@ -202,7 +233,7 @@ protected function get_default_primary_column_name() {
public function print_js_template_row() {
list( $columns, $hidden, , $primary ) = $this->get_column_info();

echo '<tr data-uuid="{{ data.uuid }}">';
echo '<tr data-uuid="{{ data.uuid }}" data-expires="{{ data.expires || \'\' }}">';

foreach ( $columns as $column_name => $display_name ) {
$is_primary = $primary === $column_name;
Expand Down Expand Up @@ -237,6 +268,26 @@ public function print_js_template_row() {
case 'last_ip':
echo "{{ data.last_ip || '—' }}";
break;
case 'expires':
?>
<# if ( data.expires ) { #>
<# var expiresDate = new Date( data.expires + 'Z' ); #>
<# var isExpired = new Date().getTime() > expiresDate.getTime(); #>
<# var formattedDate = wp.date.dateI18n( <?php echo wp_json_encode( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ) ); ?>, expiresDate ); #>
<# if ( isExpired ) { #>
<?php
/* translators: %s: Expiration date and time for the Application Password. */
printf( esc_html__( 'Expired on %s' ), '{{ formattedDate }}' );
?>
<# } else { #>
{{ formattedDate }}
<# } #>
<# } else { #>
—
<# } #>
<br><button type="button" class="button-link edit-expires" aria-label="<?php esc_attr_e( 'Edit Expiration Date and Time' ); ?>"><?php esc_html_e( 'Edit Expiry' ); ?></button>
<?php
break;
case 'revoke':
printf(
'<button type="button" class="button delete" aria-label="%1$s">%2$s</button>',
Expand Down
14 changes: 13 additions & 1 deletion src/wp-admin/user-edit.php
Original file line number Diff line number Diff line change
Expand Up @@ -811,7 +811,7 @@
</table>

<?php if ( wp_is_application_passwords_available_for_user( $user_id ) || ! wp_is_application_passwords_supported() ) : ?>
<div class="application-passwords hide-if-no-js" id="application-passwords-section">
<div class="application-passwords hide-if-no-js" id="application-passwords-section" data-timezone="<?php echo esc_attr( wp_timezone_string() ); ?>">
<h2><?php _e( 'Application Passwords' ); ?></h2>
<p><?php _e( 'Application passwords allow authentication via non-interactive systems, such as XML-RPC or the REST API, without providing your actual password. Application passwords can be easily revoked. They cannot be used for traditional logins to your website.' ); ?></p>
<?php if ( wp_is_application_passwords_available_for_user( $user_id ) ) : ?>
Expand Down Expand Up @@ -860,6 +860,12 @@
<p class="description" id="new_application_password_name_desc"><?php _e( 'Required to create an Application Password, but not to update the user.' ); ?></p>
</div>

<div class="form-field">
<label for="new_application_password_expires"><?php _e( 'Expires on' ); ?></label>
<input type="datetime-local" id="new_application_password_expires" name="new_application_password_expires" class="input ltr" step="1" min="<?php echo esc_attr( wp_date( 'Y-m-d\TH:i:s', time() + 1 ) ); ?>" aria-describedby="new_application_password_expires_desc application-passwords-timezone" />
<p class="description" id="new_application_password_expires_desc"><?php _e( 'Optional. Set an expiration date and time for this password.' ); ?></p>
</div>

<?php
/**
* Fires in the create Application Passwords form.
Expand All @@ -871,6 +877,12 @@
do_action( 'wp_create_application_password_form', $profile_user );
?>

<p class="description" id="application-passwords-timezone">
<?php
/* translators: %s: Site timezone. */
printf( esc_html__( 'Timezone: %s' ), esc_html( wp_timezone_string() ) );
?>
</p>
<button type="button" name="do_new_application_password" id="do_new_application_password" class="button button-secondary"><?php _e( 'Add Application Password' ); ?></button>
</div>
<?php
Expand Down
Loading
Loading