REST API: Introduce Application Passwords for API authentication.

In WordPress 4.4 the REST API was first introduced. A few releases later in WordPress 4.7, the Content API endpoints were added, paving the way for Gutenberg and countless in-site experiences. In the intervening years, numerous plugins have built on top of the REST API. Many developers shared a common frustration, the lack of external authentication to the REST API.

This commit introduces Application Passwords to allow users to connect to external applications to their WordPress website. Users can generate individual passwords for each application, allowing for easy revocation and activity monitoring. An authorization flow is introduced to make the connection flow simple for users and application developers.

Application Passwords uses Basic Authentication, and by default is only available over an SSL connection.

Props georgestephanis, kasparsd, timothyblynjacobs, afercia, akkspro, andraganescu, arippberger, aristath, austyfrosty, ayesh, batmoo, bradyvercher, brianhenryie, helen, ipstenu, jeffmatson, jeffpaul, joostdevalk, joshlevinson, kadamwhite, kjbenk, koke, michael-arestad, Otto42, pekz0r, salzano, spacedmonkey, valendesigns.
Fixes #42790.


git-svn-id: https://develop.svn.wordpress.org/trunk@49109 602fd350-edb4-49c9-b593-d223f7449a82
This commit is contained in:
TimothyBlynJacobs 2020-10-08 22:12:02 +00:00
parent 79703088c4
commit 1856d0fe2a
25 changed files with 3493 additions and 3 deletions

View File

@ -238,6 +238,8 @@ module.exports = function(grunt) {
'admin-js': {
files: {
[ WORKING_DIR + 'wp-admin/js/accordion.js' ]: [ './src/js/_enqueues/lib/accordion.js' ],
[ WORKING_DIR + 'wp-admin/js/application-passwords.js' ]: [ './src/js/_enqueues/admin/application-passwords.js' ],
[ WORKING_DIR + 'wp-admin/js/auth-app.js' ]: [ './src/js/_enqueues/admin/auth-app.js' ],
[ WORKING_DIR + 'wp-admin/js/code-editor.js' ]: [ './src/js/_enqueues/wp/code-editor.js' ],
[ WORKING_DIR + 'wp-admin/js/color-picker.js' ]: [ './src/js/_enqueues/lib/color-picker.js' ],
[ WORKING_DIR + 'wp-admin/js/comment.js' ]: [ './src/js/_enqueues/admin/comment.js' ],
@ -796,6 +798,8 @@ module.exports = function(grunt) {
options: {
file_mappings: {
'src/wp-admin/js/accordion.js': 'src/js/_enqueues/lib/accordion.js',
'src/wp-admin/js/application-passwords.js': 'src/js/_enqueues/admin/application-passwords.js',
'src/wp-admin/js/auth-app.js': 'src/js/_enqueues/admin/auth-app.js',
'src/wp-admin/js/code-editor.js': 'src/js/_enqueues/wp/code-editor.js',
'src/wp-admin/js/color-picker.js': 'src/js/_enqueues/lib/color-picker.js',
'src/wp-admin/js/comment.js': 'src/js/_enqueues/admin/comment.js',

View File

@ -0,0 +1,196 @@
/**
* @output wp-admin/js/application-passwords.js
*/
( function( $ ) {
var $appPassSection = $( '#application-passwords-section' ),
$newAppPassForm = $appPassSection.find( '.create-application-password' ),
$newAppPassField = $newAppPassForm.find( '.input' ),
$newAppPassButton = $newAppPassForm.find( '.button' ),
$appPassTwrapper = $appPassSection.find( '.application-passwords-list-table-wrapper' ),
$appPassTbody = $appPassSection.find( 'tbody' ),
$appPassTrNoItems = $appPassTbody.find( '.no-items' ),
$removeAllBtn = $( '#revoke-all-application-passwords' ),
tmplNewAppPass = wp.template( 'new-application-password' ),
tmplAppPassRow = wp.template( 'application-password-row' ),
userId = $( '#user_id' ).val();
$newAppPassButton.click( function( e ) {
e.preventDefault();
var name = $newAppPassField.val();
if ( 0 === name.length ) {
$newAppPassField.focus();
return;
}
clearErrors();
$newAppPassField.prop( 'disabled', true );
$newAppPassButton.prop( 'disabled', true );
var request = {
name: name
};
/**
* Filters the request data used to create a new Application Password.
*
* @since 5.6.0
*
* @param {Object} request The request data.
* @param {number} userId The id of the user the password is added for.
*/
request = wp.hooks.applyFilters( 'wp_application_passwords_new_password_request', request, userId );
wp.apiRequest( {
path: '/wp/v2/users/' + userId + '/application-passwords',
method: 'POST',
data: request
} ).always( function() {
$newAppPassField.prop( 'disabled', false );
$newAppPassButton.prop( 'disabled', false );
} ).done( function( response ) {
$newAppPassField.val( '' );
$newAppPassButton.prop( 'disabled', false );
$newAppPassForm.after( tmplNewAppPass( {
name: name,
password: response.password
} ) );
$( '.new-application-password-notice' ).focus();
$appPassTbody.prepend( tmplAppPassRow( response ) );
$appPassTwrapper.show();
$appPassTrNoItems.remove();
/**
* Fires after an application password has been successfully created.
*
* @since 5.6.0
*
* @param {Object} response The response data from the REST API.
* @param {Object} request The request data used to create the password.
*/
wp.hooks.doAction( 'wp_application_passwords_created_password', response, request );
} ).fail( handleErrorResponse );
} );
$appPassTbody.on( 'click', '.delete', function( e ) {
e.preventDefault();
if ( ! window.confirm( wp.i18n.__( 'Are you sure you want to revoke this password? This action cannot be undone.' ) ) ) {
return;
}
var $submitButton = $( this ),
$tr = $submitButton.closest( 'tr' ),
uuid = $tr.data( 'uuid' );
clearErrors();
$submitButton.prop( 'disabled', true );
wp.apiRequest( {
path: '/wp/v2/users/' + userId + '/application-passwords/' + uuid,
method: 'DELETE'
} ).always( function() {
$submitButton.prop( 'disabled', false );
} ).done( function( response ) {
if ( response.deleted ) {
if ( 0 === $tr.siblings().length ) {
$appPassTwrapper.hide();
}
$tr.remove();
wp.a11y.speak( wp.i18n.__( 'Application password revoked.' ) );
}
} ).fail( handleErrorResponse );
} );
$removeAllBtn.on( 'click', function( e ) {
e.preventDefault();
if ( ! window.confirm( wp.i18n.__( 'Are you sure you want to revoke all passwords? This action cannot be undone.' ) ) ) {
return;
}
var $submitButton = $( this );
clearErrors();
$submitButton.prop( 'disabled', true );
wp.apiRequest( {
path: '/wp/v2/users/' + userId + '/application-passwords',
method: 'DELETE'
} ).always( function() {
$submitButton.prop( 'disabled', false );
} ).done( function( response ) {
if ( response.deleted ) {
$appPassTbody.children().remove();
$appPassSection.children( '.new-application-password' ).remove();
$appPassTwrapper.hide();
wp.a11y.speak( wp.i18n.__( 'All application passwords revoked.' ) );
}
} ).fail( handleErrorResponse );
} );
$( document ).on( 'click', '.new-application-password-notice .notice-dismiss', function( e ) {
e.preventDefault();
var $el = $( this ).parent();
$el.fadeTo( 100, 0, function () {
$el.slideUp( 100, function () {
$el.remove();
} );
} );
} );
// If there are no items, don't display the table yet. If there are, show it.
if ( 0 === $appPassTbody.children( 'tr' ).not( $appPassTrNoItems ).length ) {
$appPassTwrapper.hide();
}
/**
* Handles an error response from the REST API.
*
* @since 5.6.0
*
* @param {jqXHR} xhr The XHR object from the ajax call.
* @param {string} textStatus The string categorizing the ajax request's status.
* @param {string} errorThrown The HTTP status error text.
*/
function handleErrorResponse( xhr, textStatus, errorThrown ) {
var errorMessage = errorThrown;
if ( xhr.responseJSON && xhr.responseJSON.message ) {
errorMessage = xhr.responseJSON.message;
}
addError( errorMessage );
}
/**
* Displays an error message in the Application Passwords section.
*
* @since 5.6.0
*
* @param {string} message The error message to display.
*/
function addError( message ) {
var $notice = $( '<div></div>' )
.attr( 'role', 'alert' )
.addClass( 'notice notice-error' )
.append( $( '<p></p>' ).text( message ) );
$newAppPassForm.after( $notice );
}
/**
* Clears error messages from the Application Passwords section.
*
* @since 5.6.0
*/
function clearErrors() {
$( '.notice', $appPassSection ).remove();
}
}( jQuery ) );

View File

@ -0,0 +1,152 @@
/**
* @output wp-admin/js/auth-app.js
*/
/* global authApp */
( function( $, authApp ) {
var $appNameField = $( '#app_name' ),
$approveBtn = $( '#approve' ),
$rejectBtn = $( '#reject' ),
$form = $appNameField.closest( 'form' ),
context = {
userLogin: authApp.user_login,
successUrl: authApp.success,
rejectUrl: authApp.reject
};
$approveBtn.click( function( e ) {
var name = $appNameField.val();
e.preventDefault();
if ( 0 === name.length ) {
$appNameField.focus();
return;
}
$appNameField.prop( 'disabled', true );
$approveBtn.prop( 'disabled', true );
var request = {
name: name
};
/**
* Filters the request data used to Authorize an Application Password request.
*
* @since 5.6.0
*
* @param {Object} request The request data.
* @param {Object} context Context about the Application Password request.
* @param {string} context.userLogin The user's login username.
* @param {string} context.successUrl The URL the user will be redirected to after approving the request.
* @param {string} context.rejectUrl The URL the user will be redirected to after rejecting the request.
*/
request = wp.hooks.applyFilters( 'wp_application_passwords_approve_app_request', request, context );
wp.apiRequest( {
path: '/wp/v2/users/me/application-passwords',
method: 'POST',
data: request
} ).done( function( response, textStatus, jqXHR ) {
/**
* Fires when an Authorize Application Password request has been successfully approved.
*
* @since 5.6.0
*
* @param {Object} response The response from the REST API.
* @param {string} response.password The newly created password.
* @param {string} textStatus The status of the request.
* @param {jqXHR} jqXHR The underlying jqXHR object that made the request.
*/
wp.hooks.doAction( 'wp_application_passwords_approve_app_request_success', response, textStatus, jqXHR );
var raw = authApp.success,
url, message, $notice;
if ( raw ) {
url = raw + ( -1 === raw.indexOf( '?' ) ? '?' : '&' ) +
'user_login=' + encodeURIComponent( authApp.user_login ) +
'&password=' + encodeURIComponent( response.password );
window.location = url;
} else {
message = wp.i18n.sprintf(
wp.i18n.__( 'Your new password for %1$s is: %2$s.' ),
'<strong></strong>',
'<kbd></kbd>'
);
$notice = $( '<div></div>' )
.attr( 'role', 'alert' )
.attr( 'tabindex', 0 )
.addClass( 'notice notice-success notice-alt' )
.append( $( '<p></p>' ).html( message ) );
// We're using .text() to write the variables to avoid any chance of XSS.
$( 'strong', $notice ).text( name );
$( 'kbd', $notice ).text( response.password );
$form.replaceWith( $notice );
$notice.focus();
}
} ).fail( function( jqXHR, textStatus, errorThrown ) {
var errorMessage = errorThrown,
error = null;
if ( jqXHR.responseJSON ) {
error = jqXHR.responseJSON;
if ( error.message ) {
errorMessage = error.message;
}
}
var $notice = $( '<div></div>' )
.attr( 'role', 'alert' )
.addClass( 'notice notice-error' )
.append( $( '<p></p>' ).text( errorMessage ) );
$( 'h1' ).after( $notice );
$appNameField.prop( 'disabled', false );
$approveBtn.prop( 'disabled', false );
/**
* Fires when an Authorize Application Password request encountered an error when trying to approve the request.
*
* @since 5.6.0
*
* @param {Object|null} error The error from the REST API. May be null if the server did not send proper JSON.
* @param {string} textStatus The status of the request.
* @param {string} errorThrown The error message associated with the response status code.
* @param {jqXHR} jqXHR The underlying jqXHR object that made the request.
*/
wp.hooks.doAction( 'wp_application_passwords_approve_app_request_success', error, textStatus, jqXHR );
} );
} );
$rejectBtn.click( function( e ) {
e.preventDefault();
/**
* Fires when an Authorize Application Password request has been rejected by the user.
*
* @since 5.6.0
*
* @param {Object} context Context about the Application Password request.
* @param {string} context.userLogin The user's login username.
* @param {string} context.successUrl The URL the user will be redirected to after approving the request.
* @param {string} context.rejectUrl The URL the user will be redirected to after rejecting the request.
*/
wp.hooks.doAction( 'wp_application_passwords_reject_app', context );
// @todo: Make a better way to do this so it feels like less of a semi-open redirect.
window.location = authApp.reject;
} );
$form.on( 'submit', function( e ) {
e.preventDefault();
} );
}( jQuery, authApp ) );

View File

@ -0,0 +1,230 @@
<?php
/**
* Authorize Application Screen
*
* @package WordPress
* @subpackage Administration
*/
/** WordPress Administration Bootstrap */
require_once __DIR__ . '/admin.php';
$error = null;
$new_password = '';
if ( isset( $_POST['action'] ) && 'authorize_application_password' === $_POST['action'] ) {
check_admin_referer( 'authorize_application_password' );
$success_url = $_POST['success_url'];
$reject_url = $_POST['reject_url'];
$app_name = $_POST['app_name'];
$redirect = '';
if ( isset( $_POST['reject'] ) ) {
if ( $reject_url ) {
$redirect = add_query_arg( 'success', 'false', $reject_url );
} else {
$redirect = admin_url();
}
} elseif ( isset( $_POST['approve'] ) ) {
$created = WP_Application_Passwords::create_new_application_password( get_current_user_id(), array( 'name' => $app_name ) );
if ( is_wp_error( $created ) ) {
$error = $created;
} else {
list( $new_password ) = $created;
if ( $success_url ) {
$redirect = add_query_arg(
array(
'username' => urlencode( wp_get_current_user()->user_login ),
'password' => urlencode( $new_password ),
),
$success_url
);
}
}
}
if ( $redirect ) {
// Explicitly not using wp_safe_redirect b/c sends to arbitrary domain.
wp_redirect( $redirect );
exit;
}
}
$title = __( 'Authorize Application' );
$app_name = ! empty( $_REQUEST['app_name'] ) ? $_REQUEST['app_name'] : '';
$success_url = ! empty( $_REQUEST['success_url'] ) ? $_REQUEST['success_url'] : null;
$reject_url = ! empty( $_REQUEST['reject_url'] ) ? $_REQUEST['reject_url'] : $success_url;
$user = wp_get_current_user();
$request = compact( 'app_name', 'success_url', 'reject_url' );
$is_valid = wp_is_authorize_application_password_request_valid( $request, $user );
if ( is_wp_error( $is_valid ) ) {
wp_die(
__( 'The Authorize Application request is not allowed.' ) . ' ' . implode( ' ', $is_valid->get_error_messages() ),
__( 'Cannot Authorize Application' )
);
}
if ( ! wp_is_application_passwords_available_for_user( $user ) ) {
if ( wp_is_application_passwords_available() ) {
$message = __( 'Application passwords are not enabled for your account. Please contact the site administrator for assistance.' );
} else {
$message = __( 'Application passwords are not enabled.' );
}
wp_die(
$message,
__( 'Cannot Authorize Application' ),
array(
'link_text' => __( 'Go Back' ),
'link_url' => $reject_url ? add_query_arg( 'error', 'disabled', $reject_url ) : admin_url(),
)
);
}
wp_enqueue_script( 'auth-app' );
wp_localize_script(
'auth-app',
'authApp',
array(
'user_login' => $user->user_login,
'success' => $success_url,
'reject' => $reject_url ? $reject_url : admin_url(),
)
);
require_once ABSPATH . 'wp-admin/admin-header.php';
?>
<div class="wrap">
<h1><?php echo esc_html( $title ); ?></h1>
<?php if ( is_wp_error( $error ) ) : ?>
<div class="notice notice-error"><p><?php echo $error->get_error_message(); ?></p></div>
<?php endif; ?>
<div class="card js-auth-app-card">
<h2 class="title"><?php __( 'An application would like to connect to your account.' ); ?></h2>
<?php if ( $app_name ) : ?>
<p>
<?php
/* translators: Application Name */
printf( __( 'Would you like to give the application identifying itself as %s access to your account? You should only do this if you trust the app in question.' ), '<strong>' . esc_html( $app_name ) . '</strong>' );
?>
</p>
<?php else : ?>
<p><?php _e( 'Would you like to give this application access to your account? You should only do this if you trust the app in question.' ); ?></p>
<?php endif; ?>
<?php if ( $new_password ) : ?>
<div class="notice notice-success notice-alt below-h2">
<p class="password-display">
<?php
printf(
/* translators: 1: Application name, 2: Generated password. */
__( 'Your new password for %1$s is %2$s.' ),
'<strong>' . esc_html( $app_name ) . '</strong>',
'<kbd>' . esc_html( WP_Application_Passwords::chunk_password( $new_password ) ) . '</kbd>'
);
?>
</p>
</div>
<?php
/**
* Fires in the Authorize Application Password new password section.
*
* @since 5.6.0
*
* @param string $new_password The newly generated application password.
* @param array $request The array of request data. All arguments are optional and may be empty.
* @param WP_User $user The user authorizing the application.
*/
do_action( 'wp_authorize_application_password_form', $request, $user );
?>
<?php else : ?>
<form action="<?php echo esc_url( admin_url( 'authorize-application.php' ) ); ?>" method="post">
<?php wp_nonce_field( 'authorize_application_password' ); ?>
<input type="hidden" name="action" value="authorize_application_password" />
<input type="hidden" name="success_url" value="<?php echo esc_url( $success_url ); ?>" />
<input type="hidden" name="reject_url" value="<?php echo esc_url( $reject_url ); ?>" />
<label for="app_name"><?php esc_html_e( 'Application Title:' ); ?></label>
<input type="text" id="app_name" name="app_name" value="<?php echo esc_attr( $app_name ); ?>" placeholder="<?php esc_attr_e( 'Name this connection&hellip;' ); ?>" required />
<?php
/**
* Fires in the Authorize Application Password form before the submit buttons.
*
* @since 5.6.0
*
* @param array $request {
* The array of request data. All arguments are optional and may be empty.
*
* @type string $app_name The suggested name of the application.
* @type string $success_url The url the user will be redirected to after approving the application.
* @type string $reject_url The url the user will be redirected to after rejecting the application.
* }
* @param WP_User $user The user authorizing the application.
*/
do_action( 'wp_authorize_application_password_form', $request, $user );
?>
<p><?php submit_button( __( 'Yes, I approve of this connection.' ), 'primary', 'approve', false ); ?>
<br /><em>
<?php
if ( $success_url ) {
printf(
/* translators: The URL the user is being redirected to. */
__( 'You will be sent to %s' ),
'<strong><kbd>' . esc_html(
add_query_arg(
array(
'username' => $user->user_login,
'password' => '[------]',
),
$success_url
)
) . '</kbd></strong>'
);
} else {
_e( 'You will be given a password to manually enter into the application in question.' );
}
?>
</em>
</p>
<p><?php submit_button( __( 'No, I do not approve of this connection.' ), 'secondary', 'reject', false ); ?>
<br /><em>
<?php
if ( $reject_url ) {
printf(
/* translators: The URL the user is being redirected to. */
__( 'You will be sent to %s' ),
'<strong><kbd>' . esc_html(
add_query_arg(
array(
'success' => 'false',
),
$reject_url
)
) . '</kbd></strong>'
);
} else {
_e( 'You will be returned to the WordPress Dashboard, and no changes will be made.' );
}
?>
</em>
</p>
</form>
<?php endif; ?>
</div>
</div>
<?php
require_once ABSPATH . 'wp-admin/admin-footer.php';

View File

@ -756,7 +756,7 @@ ul#add-to-blog-users {
display: inline-block;
}
.form-table td fieldset p label {
.form-table td fieldset p label {
margin-top: 0 !important;
}
@ -841,6 +841,11 @@ table.form-table td .updated p {
cursor: pointer;
}
.new-application-password-notice.notice {
margin-top: 20px;
margin-bottom: 0;
}
/*------------------------------------------------------------------------------
19.0 - Tools
------------------------------------------------------------------------------*/

View File

@ -0,0 +1,248 @@
<?php
/**
* List Table API: WP_Application_Passwords_List_Table class
*
* @package WordPress
* @subpackage Administration
* @since 5.6.0
*/
/**
* Class for displaying the list of application password items.
*
* @since 5.6.0
* @access private
*
* @see WP_List_Table
*/
class WP_Application_Passwords_List_Table extends WP_List_Table {
/**
* Gets the list of columns.
*
* @since 5.6.0
*
* @return array
*/
public function get_columns() {
return array(
'name' => __( 'Name' ),
'created' => __( 'Created' ),
'last_used' => __( 'Last Used' ),
'last_ip' => __( 'Last IP' ),
'revoke' => __( 'Revoke' ),
);
}
/**
* Prepares the list of items for displaying.
*
* @since 5.6.0
*/
public function prepare_items() {
global $user_id;
$this->items = array_reverse( WP_Application_Passwords::get_user_application_passwords( $user_id ) );
}
/**
* Handles the name column output.
*
* @since 5.6.0
*
* @param array $item The current application password item.
*/
public function column_name( $item ) {
echo esc_html( $item['name'] );
}
/**
* Handles the created column output.
*
* @since 5.6.0
*
* @param array $item The current application password item.
*/
public function column_created( $item ) {
if ( empty( $item['created'] ) ) {
echo '&mdash;';
} else {
echo gmdate( get_option( 'date_format', 'r' ), $item['created'] );
}
}
/**
* Handles the last used column output.
*
* @since 5.6.0
*
* @param array $item The current application password item.
*/
public function column_last_used( $item ) {
if ( empty( $item['last_used'] ) ) {
echo '&mdash;';
} else {
echo gmdate( get_option( 'date_format', 'r' ), $item['last_used'] );
}
}
/**
* Handles the last ip column output.
*
* @since 5.6.0
*
* @param array $item The current application password item.
*/
public function column_last_ip( $item ) {
if ( empty( $item['last_ip'] ) ) {
echo '&mdash;';
} else {
echo $item['last_ip'];
}
}
/**
* Handles the revoke column output.
*
* @since 5.6.0
*/
public function column_revoke() {
submit_button( __( 'Revoke' ), 'delete', 'revoke-application-password', false );
}
/**
* Generates content for a single row of the table
*
* @since 5.6.0
*
* @param array $item The current item.
* @param string $column_name The current column name.
*/
protected function column_default( $item, $column_name ) {
/**
* Fires for each custom column in the Application Passwords list table.
*
* Custom columns are registered using the {@see 'manage_application-passwords-user_columns'} filter.
*
* @since 5.6.0
*
* @param string $column_name Name of the custom column.
* @param array $item The application password item.
*/
do_action( "manage_{$this->screen->id}_custom_column", $column_name, $item );
}
/**
* Generates custom table navigation to prevent conflicting nonces.
*
* @since 5.6.0
*
* @param string $which The location of the bulk actions: 'top' or 'bottom'.
*/
protected function display_tablenav( $which ) {
?>
<div class="tablenav <?php echo esc_attr( $which ); ?>">
<?php if ( 'bottom' === $which ) : ?>
<div class="alignright">
<?php submit_button( __( 'Revoke all application passwords' ), 'delete', 'revoke-all-application-passwords', false ); ?>
</div>
<?php endif; ?>
<div class="alignleft actions bulkactions">
<?php $this->bulk_actions( $which ); ?>
</div>
<?php
$this->extra_tablenav( $which );
$this->pagination( $which );
?>
<br class="clear" />
</div>
<?php
}
/**
* Generates content for a single row of the table.
*
* @since 5.6.0
*
* @param object $item The current item.
*/
public function single_row( $item ) {
echo '<tr data-uuid="' . esc_attr( $item['uuid'] ) . '">';
$this->single_row_columns( $item );
echo '</tr>';
}
/**
* Gets the name of the default primary column.
*
* @since 5.6.0
*
* @return string Name of the default primary column, in this case, 'name'.
*/
protected function get_default_primary_column_name() {
return 'name';
}
/**
* Prints the JavaScript template for the new row item.
*
* @since 5.6.0
*/
public function print_js_template_row() {
list( $columns, $hidden, , $primary ) = $this->get_column_info();
echo '<tr data-uuid="{{ data.uuid }}">';
foreach ( $columns as $column_name => $display_name ) {
$is_primary = $primary === $column_name;
$classes = "{$column_name} column-{$column_name}";
if ( $is_primary ) {
$classes .= ' has-row-actions column-primary';
}
if ( in_array( $column_name, $hidden, true ) ) {
$classes .= ' hidden';
}
printf( '<td class="%s" data-colname="%s">', esc_attr( $classes ), esc_attr( wp_strip_all_tags( $display_name ) ) );
switch ( $column_name ) {
case 'name':
echo '{{ data.name }}';
break;
case 'created':
echo "<# print( wp.date.dateI18n( '" . esc_js( get_option( 'date_format' ) ) . "', data.created ) ) #>";
break;
case 'last_used':
echo "<# print( data.last_used !== null ? wp.date.dateI18n( '" . esc_js( get_option( 'date_format' ) ) . "', data.last_used ) : '—' ) #>";
break;
case 'last_ip':
echo "{{ data.last_ip || '—' }}";
break;
case 'revoke':
echo $this->column_revoke();
break;
default:
/**
* Fires in the JavaScript row template for each custom column in the Application Passwords list table.
*
* Custom columns are registered using the {@see 'manage_application-passwords-user_columns'} filter.
*
* @since 5.6.0
*
* @param string $column_name Name of the custom column.
*/
do_action( "manage_{$this->screen->id}_custom_column_js_template", $column_name );
break;
}
if ( $is_primary ) {
echo '<button type="button" class="toggle-row"><span class="screen-reader-text">' . __( 'Show more details' ) . '</span></button>';
}
echo '</td>';
}
echo '</tr>';
}
}

View File

@ -33,6 +33,7 @@ function _get_list_table( $class, $args = array() ) {
'WP_Themes_List_Table' => 'themes',
'WP_Theme_Install_List_Table' => array( 'themes', 'theme-install' ),
'WP_Plugins_List_Table' => 'plugins',
'WP_Application_Passwords_List_Table' => 'application-passwords',
// Network Admin.
'WP_MS_Sites_List_Table' => 'ms-sites',

View File

@ -594,3 +594,61 @@ Please click the following link to activate your user account:
wp_specialchars_decode( translate_user_role( $role['name'] ) )
);
}
/**
* Checks if the Authorize Application Password request is valid.
*
* @since 5.6.0
*
* @param array $request {
* The array of request data. All arguments are optional and may be empty.
*
* @type string $app_name The suggested name of the application.
* @type string $success_url The url the user will be redirected to after approving the application.
* @type string $reject_url The url the user will be redirected to after rejecting the application.
* }
* @param WP_User $user The user authorizing the application.
* @return true|WP_Error True if the request is valid, a WP_Error object contains errors if not.
*/
function wp_is_authorize_application_password_request_valid( $request, $user ) {
$error = new WP_Error();
if ( ! empty( $request['success_url'] ) ) {
$scheme = wp_parse_url( $request['success_url'], PHP_URL_SCHEME );
if ( 'http' === $scheme ) {
$error->add(
'invalid_redirect_scheme',
__( 'The success url must be served over a secure connection.' )
);
}
}
if ( ! empty( $request['reject_url'] ) ) {
$scheme = wp_parse_url( $request['reject_url'], PHP_URL_SCHEME );
if ( 'http' === $scheme ) {
$error->add(
'invalid_redirect_scheme',
__( 'The rejection url must be served over a secure connection.' )
);
}
}
/**
* Fires before application password errors are returned.
*
* @since 5.6.0
*
* @param WP_Error $error The error object.
* @param array $request The array of request data.
* @param WP_User $user The user authorizing the application.
*/
do_action( 'wp_authorize_application_password_request_errors', $error, $request, $user );
if ( $error->has_errors() ) {
return $error;
}
return true;
}

View File

@ -27,6 +27,10 @@ if ( ! $user_id && IS_PROFILE_PAGE ) {
wp_enqueue_script( 'user-profile' );
if ( wp_is_application_passwords_available_for_user( $user_id ) ) {
wp_enqueue_script( 'application-passwords' );
}
if ( IS_PROFILE_PAGE ) {
$title = __( 'Profile' );
} else {
@ -702,6 +706,39 @@ endif;
</table>
<?php if ( wp_is_application_passwords_available_for_user( $user_id ) ) : ?>
<div class="application-passwords hide-if-no-js" id="application-passwords-section">
<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>
<div class="create-application-password">
<label for="new_application_password_name" class="screen-reader-text"><?php _e( 'New Application Password Name' ); ?></label>
<input type="text" size="30" id="new_application_password_name" name="new_application_password_name" placeholder="<?php esc_attr_e( 'New Application Password Name' ); ?>" class="input" />
<?php
/**
* Fires in the create Application Passwords form.
*
* @since 5.6.0
*
* @param WP_User $profileuser The current WP_User object.
*/
do_action( 'wp_create_application_password_form', $profileuser );
?>
<?php submit_button( __( 'Add New' ), 'secondary', 'do_new_application_password', false ); ?>
</div>
<div class="application-passwords-list-table-wrapper">
<?php
$application_passwords_list_table = _get_list_table( 'WP_Application_Passwords_List_Table', array( 'screen' => 'application-passwords-user' ) );
$application_passwords_list_table->prepare_items();
$application_passwords_list_table->display();
?>
</div>
</div>
<?php endif; ?>
<?php
if ( IS_PROFILE_PAGE ) {
/**
@ -787,5 +824,30 @@ endif;
document.getElementById('pass1').focus();
}
</script>
<?php if ( isset( $application_passwords_list_table ) ) : ?>
<script type="text/html" id="tmpl-new-application-password">
<div class="notice notice-success is-dismissible new-application-password-notice" role="alert" tabindex="0">
<p>
<?php
printf(
/* translators: 1: Application name, 2: Generated password. */
esc_html__( 'Your new password for %1$s is: %2$s' ),
'<strong>{{ data.name }}</strong>',
'<kbd>{{ data.password }}</kbd>'
);
?>
</p>
<p><?php esc_attr_e( 'Be sure to save this in a safe location. You will not be able to retrieve it.' ); ?></p>
<button type="button" class="notice-dismiss">
<span class="screen-reader-text"><?php __( 'Dismiss this notice.' ); ?></span>
</button>
</div>
</script>
<script type="text/html" id="tmpl-application-password-row">
<?php $application_passwords_list_table->print_js_template_row(); ?>
</script>
<?php endif; ?>
<?php
require_once ABSPATH . 'wp-admin/admin-footer.php';

View File

@ -0,0 +1,318 @@
<?php
/**
* WP_Application_Passwords class
*
* @package WordPress
* @since 5.6.0
*/
/**
* Class for displaying, modifying, and sanitizing application passwords.
*
* @package WordPress
*/
class WP_Application_Passwords {
/**
* The application passwords user meta key.
*
* @since 5.6.0
*
* @type string
*/
const USERMETA_KEY_APPLICATION_PASSWORDS = '_application_passwords';
/**
* The generated application password length.
*
* @since 5.6.0
*
* @type int
*/
const PW_LENGTH = 24;
/**
* Creates a new application password.
*
* @since 5.6.0
*
* @param int $user_id User ID.
* @param array $args Information about the application password.
* @return array|WP_Error The first key in the array is the new password, the second is its detailed information.
* A WP_Error instance is returned on error.
*/
public static function create_new_application_password( $user_id, $args = array() ) {
if ( empty( $args['name'] ) ) {
return new WP_Error( 'application_password_empty_name', __( 'An application name is required to create an application password.' ) );
}
$new_password = wp_generate_password( static::PW_LENGTH, false );
$hashed_password = wp_hash_password( $new_password );
$new_item = array(
'uuid' => wp_generate_uuid4(),
'name' => $args['name'],
'password' => $hashed_password,
'created' => time(),
'last_used' => null,
'last_ip' => null,
);
$passwords = static::get_user_application_passwords( $user_id );
$passwords[] = $new_item;
$saved = static::set_user_application_passwords( $user_id, $passwords );
if ( ! $saved ) {
return new WP_Error( 'db_error', __( 'Could not save application password.' ) );
}
/**
* Fires when an application password is created.
*
* @since 5.6.0
*
* @param int $user_id The user ID.
* @param array $new_item The details about the created password.
* @param string $new_password The unhashed generated app password.
* @param array $args Information used to create the application password.
*/
do_action( 'wp_create_application_password', $user_id, $new_item, $new_password, $args );
return array( $new_password, $new_item );
}
/**
* Gets a user's application passwords.
*
* @since 5.6.0
*
* @param int $user_id User ID.
* @return array The list of app passwords.
*/
public static function get_user_application_passwords( $user_id ) {
$passwords = get_user_meta( $user_id, static::USERMETA_KEY_APPLICATION_PASSWORDS, true );
if ( ! is_array( $passwords ) ) {
return array();
}
$save = false;
foreach ( $passwords as $i => $password ) {
if ( ! isset( $password['uuid'] ) ) {
$passwords[ $i ]['uuid'] = wp_generate_uuid4();
$save = true;
}
}
if ( $save ) {
static::set_user_application_passwords( $user_id, $passwords );
}
return $passwords;
}
/**
* Gets a user's application password with the given uuid.
*
* @since 5.6.0
*
* @param int $user_id User ID.
* @param string $uuid The password's uuid.
* @return array|null The application password if found, null otherwise.
*/
public static function get_user_application_password( $user_id, $uuid ) {
$passwords = static::get_user_application_passwords( $user_id );
foreach ( $passwords as $password ) {
if ( $password['uuid'] === $uuid ) {
return $password;
}
}
return null;
}
/**
* Updates an application password.
*
* @since 5.6.0
*
* @param int $user_id User ID.
* @param string $uuid The password's uuid.
* @param array $update Information about the application password to update.
* @return true|WP_Error True if successful, otherwise a WP_Error instance is returned on error.
*/
public static function update_application_password( $user_id, $uuid, $update = array() ) {
$passwords = static::get_user_application_passwords( $user_id );
foreach ( $passwords as &$item ) {
if ( $item['uuid'] !== $uuid ) {
continue;
}
$save = false;
if ( ! empty( $update['name'] ) && $item['name'] !== $update['name'] ) {
$item['name'] = $update['name'];
$save = true;
}
if ( $save ) {
$saved = static::set_user_application_passwords( $user_id, $passwords );
if ( ! $saved ) {
return new WP_Error( 'db_error', __( 'Could not save application password.' ) );
}
}
/**
* Fires when an application password is updated.
*
* @since 5.6.0
*
* @param int $user_id The user ID.
* @param array $item The updated app password details.
* @param array $update The information to update.
*/
do_action( 'wp_update_application_password', $user_id, $item, $update );
return true;
}
return new WP_Error( 'application_password_not_found', __( 'Could not find an application password with that id.' ) );
}
/**
* Records that an application password has been used.
*
* @since 5.6.0
*
* @param int $user_id User ID.
* @param string $uuid The password's uuid.
* @return true|WP_Error True if the usage was recorded, a WP_Error if an error occurs.
*/
public static function record_application_password_usage( $user_id, $uuid ) {
$passwords = static::get_user_application_passwords( $user_id );
foreach ( $passwords as &$password ) {
if ( $password['uuid'] !== $uuid ) {
continue;
}
// Only record activity once a day.
if ( $password['last_used'] + DAY_IN_SECONDS > time() ) {
continue;
}
$password['last_used'] = time();
$password['last_ip'] = $_SERVER['REMOTE_ADDR'];
$saved = static::set_user_application_passwords( $user_id, $passwords );
if ( ! $saved ) {
return new WP_Error( 'db_error', __( 'Could not save application password.' ) );
}
return true;
}
// Specified Application Password not found!
return new WP_Error( 'application_password_not_found', __( 'Could not find an application password with that id.' ) );
}
/**
* Deletes an application password.
*
* @since 5.6.0
*
* @param int $user_id User ID.
* @param string $uuid The password's uuid.
* @return true|WP_Error Whether the password was successfully found and deleted, a WP_Error otherwise.
*/
public static function delete_application_password( $user_id, $uuid ) {
$passwords = static::get_user_application_passwords( $user_id );
foreach ( $passwords as $key => $item ) {
if ( $item['uuid'] === $uuid ) {
unset( $passwords[ $key ] );
$saved = static::set_user_application_passwords( $user_id, $passwords );
if ( ! $saved ) {
return new WP_Error( 'db_error', __( 'Could not delete application password.' ) );
}
/**
* Fires when an application password is deleted.
*
* @since 5.6.0
*
* @param int $user_id The user ID.
* @param array $item The data about the application password.
*/
do_action( 'wp_delete_application_password', $user_id, $item );
return true;
}
}
return new WP_Error( 'application_password_not_found', __( 'Could not find an application password with that id.' ) );
}
/**
* Deletes all application passwords for the given user.
*
* @since 5.6.0
*
* @param int $user_id User ID.
* @return int|WP_Error The number of passwords that were deleted or a WP_Error on failure.
*/
public static function delete_all_application_passwords( $user_id ) {
$passwords = static::get_user_application_passwords( $user_id );
if ( $passwords ) {
$saved = static::set_user_application_passwords( $user_id, array() );
if ( ! $saved ) {
return new WP_Error( 'db_error', __( 'Could not delete application passwords.' ) );
}
foreach ( $passwords as $item ) {
/** This action is documented in wp-includes/class-wp-application-passwords.php */
do_action( 'wp_delete_application_password', $user_id, $item );
}
return count( $passwords );
}
return 0;
}
/**
* Sets a users application passwords.
*
* @since 5.6.0
*
* @param int $user_id User ID.
* @param array $passwords Application passwords.
*
* @return bool
*/
protected static function set_user_application_passwords( $user_id, $passwords ) {
return update_user_meta( $user_id, static::USERMETA_KEY_APPLICATION_PASSWORDS, $passwords );
}
/**
* Sanitizes and then splits a password into smaller chunks.
*
* @since 5.6.0
*
* @param string $raw_password The raw application password.
* @return string The chunked password.
*/
public static function chunk_password( $raw_password ) {
$raw_password = preg_replace( '/[^a-z\d]/i', '', $raw_password );
return trim( chunk_split( $raw_password, 4, ' ' ) );
}
}

View File

@ -1509,6 +1509,7 @@ class WP_Rewrite {
$rules = "<IfModule mod_rewrite.c>\n";
$rules .= "RewriteEngine On\n";
$rules .= 'RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]';
$rules .= "RewriteBase $home_root\n";
// Prevent -f checks on index.php.

View File

@ -276,6 +276,9 @@ add_action( 'auth_cookie_expired', 'rest_cookie_collect_status' );
add_action( 'auth_cookie_bad_username', 'rest_cookie_collect_status' );
add_action( 'auth_cookie_bad_hash', 'rest_cookie_collect_status' );
add_action( 'auth_cookie_valid', 'rest_cookie_collect_status' );
add_action( 'application_password_failed_authentication', 'rest_application_password_collect_status' );
add_action( 'application_password_did_authenticate', 'rest_application_password_collect_status' );
add_filter( 'rest_authentication_errors', 'rest_application_password_check_errors', 90 );
add_filter( 'rest_authentication_errors', 'rest_cookie_check_errors', 100 );
// Actions.
@ -427,9 +430,11 @@ add_filter( 'heartbeat_nopriv_send', 'wp_auth_check' );
// Default authentication filters.
add_filter( 'authenticate', 'wp_authenticate_username_password', 20, 3 );
add_filter( 'authenticate', 'wp_authenticate_email_password', 20, 3 );
add_filter( 'authenticate', 'wp_authenticate_application_password', 20, 3 );
add_filter( 'authenticate', 'wp_authenticate_spam_check', 99 );
add_filter( 'determine_current_user', 'wp_validate_auth_cookie' );
add_filter( 'determine_current_user', 'wp_validate_logged_in_cookie', 20 );
add_filter( 'determine_current_user', 'wp_validate_application_password', 20 );
// Split term updates.
add_action( 'admin_init', '_wp_check_for_scheduled_split_terms' );

View File

@ -86,6 +86,47 @@ function wp_fix_server_vars() {
$_SERVER['PHP_SELF'] = preg_replace( '/(\?.*)?$/', '', $_SERVER['REQUEST_URI'] );
$PHP_SELF = $_SERVER['PHP_SELF'];
}
wp_populate_basic_auth_from_authorization_header();
}
/**
* Populates the Basic Auth server details from the Authorization header.
*
* Some servers running in CGI or FastCGI mode don't pass the Authorization
* header on to WordPress. If it's been rewritten to the `HTTP_AUTHORIZATION` header,
* fill in the proper $_SERVER variables instead.
*
* @since 5.6.0
*/
function wp_populate_basic_auth_from_authorization_header() {
// If we don't have anything to pull from, return early.
if ( ! isset( $_SERVER['HTTP_AUTHORIZATION'] ) && ! isset( $_SERVER['REDIRECT_HTTP_AUTHORIZATION'] ) ) {
return;
}
// If either PHP_AUTH key is already set, do nothing.
if ( isset( $_SERVER['PHP_AUTH_USER'] ) || isset( $_SERVER['PHP_AUTH_PW'] ) ) {
return;
}
// From our prior conditional, one of these must be set.
$header = isset( $_SERVER['HTTP_AUTHORIZATION'] ) ? $_SERVER['HTTP_AUTHORIZATION'] : $_SERVER['REDIRECT_HTTP_AUTHORIZATION'];
// Test to make sure the pattern matches expected.
if ( ! preg_match( '%^Basic [a-z\d/+]*={0,2}$%i', $header ) ) {
return;
}
// Removing `Basic ` the token would start six characters in.
$token = substr( $header, 6 );
$userpass = base64_decode( $token );
list( $user, $pass ) = explode( ':', $userpass );
// Now shove them in the proper keys where we're expecting later on.
$_SERVER['PHP_AUTH_USER'] = $user;
$_SERVER['PHP_AUTH_PW'] = $pass;
}
/**

View File

@ -209,6 +209,7 @@ function rest_api_default_filters() {
add_filter( 'rest_post_dispatch', 'rest_filter_response_fields', 10, 3 );
add_filter( 'rest_pre_dispatch', 'rest_handle_options_request', 10, 3 );
add_filter( 'rest_index', 'rest_add_application_passwords_to_index' );
}
/**
@ -264,6 +265,10 @@ function create_initial_rest_routes() {
$controller = new WP_REST_Users_Controller;
$controller->register_routes();
// Application Passwords
$controller = new WP_REST_Application_Passwords_Controller();
$controller->register_routes();
// Comments.
$controller = new WP_REST_Comments_Controller;
$controller->register_routes();
@ -310,7 +315,6 @@ function create_initial_rest_routes() {
// Block Directory.
$controller = new WP_REST_Block_Directory_Controller();
$controller->register_routes();
}
/**
@ -1034,6 +1038,80 @@ function rest_cookie_collect_status() {
$wp_rest_auth_cookie = true;
}
/**
* Collects the status of authenticating with an application password.
*
* @since 5.6.0
*
* @global WP_User|WP_Error|null $wp_rest_application_password_status
*
* @param WP_Error $user_or_error The authenticated user or error instance.
*/
function rest_application_password_collect_status( $user_or_error ) {
global $wp_rest_application_password_status;
$wp_rest_application_password_status = $user_or_error;
}
/**
* Checks for errors when using application password-based authentication.
*
* @since 5.6.0
*
* @global WP_User|WP_Error|null $wp_rest_application_password_status
*
* @param WP_Error|null|true $result Error from another authentication handler,
* null if we should handle it, or another value if not.
* @return WP_Error|null|true WP_Error if the application password is invalid, the $result, otherwise true.
*/
function rest_application_password_check_errors( $result ) {
global $wp_rest_application_password_status;
if ( ! empty( $result ) ) {
return $result;
}
if ( is_wp_error( $wp_rest_application_password_status ) ) {
$data = $wp_rest_application_password_status->get_error_data();
if ( ! isset( $data['status'] ) ) {
$data['status'] = 401;
}
$wp_rest_application_password_status->add_data( $data );
return $wp_rest_application_password_status;
}
if ( $wp_rest_application_password_status instanceof WP_User ) {
return true;
}
return $result;
}
/**
* Adds Application Passwords info to the REST API index.
*
* @since 5.6.0
*
* @param WP_REST_Response $response The index response object.
* @return WP_REST_Response
*/
function rest_add_application_passwords_to_index( $response ) {
if ( ! wp_is_application_passwords_available() ) {
return $response;
}
$response->data['authentication']['application-passwords'] = array(
'endpoints' => array(
'authorization' => admin_url( 'authorize-application.php' ),
),
);
return $response;
}
/**
* Retrieves the avatar urls in various sizes.
*

View File

@ -223,11 +223,30 @@ class WP_REST_Server {
*
* @see WP_REST_Server::dispatch()
*
* @global WP_User $current_user The currently authenticated user.
*
* @param string $path Optional. The request route. If not set, `$_SERVER['PATH_INFO']` will be used.
* Default null.
* @return null|false Null if not served and a HEAD request, false otherwise.
*/
public function serve_request( $path = null ) {
/* @var WP_User|null $current_user */
global $current_user;
if ( $current_user instanceof WP_User && ! $current_user->exists() ) {
/*
* If there is no current user authenticated via other means, clear
* the cached lack of user, so that an authenticate check can set it
* properly.
*
* This is done because for authentications such as Application
* Passwords, we don't want it to be accepted unless the current HTTP
* request is an API request, which can't always be identified early
* enough in evaluation.
*/
$current_user = null;
}
$content_type = isset( $_GET['_jsonp'] ) ? 'application/javascript' : 'application/json';
$this->send_header( 'Content-Type', $content_type . '; charset=' . get_option( 'blog_charset' ) );
$this->send_header( 'X-Robots-Tag', 'noindex' );

View File

@ -0,0 +1,656 @@
<?php
/**
* REST API: WP_REST_Application_Passwords_Controller class
*
* @package WordPress
* @subpackage REST_API
* @since 5.6.0
*/
/**
* Core class to access a user's application passwords via the REST API.
*
* @since 5.6.0
*
* @see WP_REST_Controller
*/
class WP_REST_Application_Passwords_Controller extends WP_REST_Controller {
/**
* Application Passwords controller constructor.
*
* @since 5.6.0
*/
public function __construct() {
$this->namespace = 'wp/v2';
$this->rest_base = 'users/(?P<user_id>(?:[\d]+|me))/application-passwords';
}
/**
* Registers the REST API routes for the application passwords controller.
*
* @since 5.6.0
*/
public function register_routes() {
register_rest_route(
$this->namespace,
'/' . $this->rest_base,
array(
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_items' ),
'permission_callback' => array( $this, 'get_items_permissions_check' ),
'args' => $this->get_collection_params(),
),
array(
'methods' => WP_REST_Server::CREATABLE,
'callback' => array( $this, 'create_item' ),
'permission_callback' => array( $this, 'create_item_permissions_check' ),
'args' => $this->get_endpoint_args_for_item_schema(),
),
array(
'methods' => WP_REST_Server::DELETABLE,
'callback' => array( $this, 'delete_items' ),
'permission_callback' => array( $this, 'delete_items_permissions_check' ),
),
'schema' => array( $this, 'get_public_item_schema' ),
)
);
register_rest_route(
$this->namespace,
'/' . $this->rest_base . '/(?P<uuid>[\w\-]+)',
array(
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_item' ),
'permission_callback' => array( $this, 'get_item_permissions_check' ),
'args' => array(
'context' => $this->get_context_param( array( 'default' => 'view' ) ),
),
),
array(
'methods' => WP_REST_Server::EDITABLE,
'callback' => array( $this, 'update_item' ),
'permission_callback' => array( $this, 'update_item_permissions_check' ),
'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
),
array(
'methods' => WP_REST_Server::DELETABLE,
'callback' => array( $this, 'delete_item' ),
'permission_callback' => array( $this, 'delete_item_permissions_check' ),
),
'schema' => array( $this, 'get_public_item_schema' ),
)
);
}
/**
* Checks if a given request has access to get application passwords.
*
* @since 5.6.0
*
* @param WP_REST_Request $request Full details about the request.
* @return true|WP_Error True if the request has read access, WP_Error object otherwise.
*/
public function get_items_permissions_check( $request ) {
return $this->do_permissions_check( $request );
}
/**
* Retrieves a collection of application passwords.
*
* @since 5.6.0
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
*/
public function get_items( $request ) {
$user = $this->get_user( $request );
if ( is_wp_error( $user ) ) {
return $user;
}
$passwords = WP_Application_Passwords::get_user_application_passwords( $user->ID );
$response = array();
foreach ( $passwords as $password ) {
$response[] = $this->prepare_response_for_collection(
$this->prepare_item_for_response( $password, $request )
);
}
return new WP_REST_Response( $response );
}
/**
* Checks if a given request has access to get a specific application password.
*
* @since 5.6.0
*
* @param WP_REST_Request $request Full details about the request.
* @return true|WP_Error True if the request has read access for the item, WP_Error object otherwise.
*/
public function get_item_permissions_check( $request ) {
return $this->do_permissions_check( $request );
}
/**
* Retrieves one application password from the collection.
*
* @since 5.6.0
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
*/
public function get_item( $request ) {
$password = $this->get_application_password( $request );
if ( is_wp_error( $password ) ) {
return $password;
}
return $this->prepare_item_for_response( $password, $request );
}
/**
* Checks if a given request has access to create application passwords.
*
* @since 5.6.0
*
* @param WP_REST_Request $request Full details about the request.
* @return true|WP_Error True if the request has access to create items, WP_Error object otherwise.
*/
public function create_item_permissions_check( $request ) {
return $this->do_permissions_check( $request );
}
/**
* Creates an application password.
*
* @since 5.6.0
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
*/
public function create_item( $request ) {
$user = $this->get_user( $request );
if ( is_wp_error( $user ) ) {
return $user;
}
$prepared = $this->prepare_item_for_database( $request );
if ( is_wp_error( $prepared ) ) {
return $prepared;
}
$created = WP_Application_Passwords::create_new_application_password( $user->ID, wp_slash( (array) $prepared ) );
if ( is_wp_error( $created ) ) {
return $created;
}
$password = $created[0];
$item = WP_Application_Passwords::get_user_application_password( $user->ID, $created[1]['uuid'] );
$item['new_password'] = WP_Application_Passwords::chunk_password( $password );
$fields_update = $this->update_additional_fields_for_object( $item, $request );
if ( is_wp_error( $fields_update ) ) {
return $fields_update;
}
/**
* Fires after a single application password is completely created or updated via the REST API.
*
* @since 5.6.0
*
* @param array $item Inserted or updated password item.
* @param WP_REST_Request $request Request object.
* @param bool $creating True when creating an application password, false when updating.
*/
do_action( 'rest_after_insert_application_password', $item, $request, true );
$request->set_param( 'context', 'edit' );
$response = $this->prepare_item_for_response( $item, $request );
$response->set_status( 201 );
$response->header( 'Location', $response->get_links()['self'][0]['href'] );
return $response;
}
/**
* Checks if a given request has access to update application passwords.
*
* @since 5.6.0
*
* @param WP_REST_Request $request Full details about the request.
* @return true|WP_Error True if the request has access to create items, WP_Error object otherwise.
*/
public function update_item_permissions_check( $request ) {
return $this->do_permissions_check( $request );
}
/**
* Updates an application password.
*
* @since 5.6.0
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
*/
public function update_item( $request ) {
$user = $this->get_user( $request );
if ( is_wp_error( $user ) ) {
return $user;
}
$item = $this->get_application_password( $request );
if ( is_wp_error( $item ) ) {
return $item;
}
$prepared = $this->prepare_item_for_database( $request );
if ( is_wp_error( $prepared ) ) {
return $prepared;
}
$saved = WP_Application_Passwords::update_application_password( $user->ID, $item['uuid'], wp_slash( (array) $prepared ) );
if ( is_wp_error( $saved ) ) {
return $saved;
}
$fields_update = $this->update_additional_fields_for_object( $item, $request );
if ( is_wp_error( $fields_update ) ) {
return $fields_update;
}
$item = WP_Application_Passwords::get_user_application_password( $user->ID, $item['uuid'] );
/** This action is documented in wp-includes/rest-api/endpoints/class-wp-rest-application-passwords-controller.php */
do_action( 'rest_after_insert_application_password', $item, $request, false );
$request->set_param( 'context', 'edit' );
return $this->prepare_item_for_response( $item, $request );
}
/**
* Checks if a given request has access to delete all application passwords.
*
* @since 5.6.0
*
* @param WP_REST_Request $request Full details about the request.
* @return true|WP_Error True if the request has access to delete the item, WP_Error object otherwise.
*/
public function delete_items_permissions_check( $request ) {
return $this->do_permissions_check( $request );
}
/**
* Deletes all application passwords.
*
* @since 5.6.0
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
*/
public function delete_items( $request ) {
$user = $this->get_user( $request );
if ( is_wp_error( $user ) ) {
return $user;
}
$deleted = WP_Application_Passwords::delete_all_application_passwords( $user->ID );
if ( is_wp_error( $deleted ) ) {
return $deleted;
}
return new WP_REST_Response(
array(
'deleted' => true,
'count' => $deleted,
)
);
}
/**
* Checks if a given request has access to delete a specific application password.
*
* @since 5.6.0
*
* @param WP_REST_Request $request Full details about the request.
* @return true|WP_Error True if the request has access to delete the item, WP_Error object otherwise.
*/
public function delete_item_permissions_check( $request ) {
return $this->do_permissions_check( $request );
}
/**
* Deletes one application password.
*
* @since 5.6.0
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
*/
public function delete_item( $request ) {
$user = $this->get_user( $request );
if ( is_wp_error( $user ) ) {
return $user;
}
$password = $this->get_application_password( $request );
if ( is_wp_error( $password ) ) {
return $password;
}
$request->set_param( 'context', 'edit' );
$previous = $this->prepare_item_for_response( $password, $request );
$deleted = WP_Application_Passwords::delete_application_password( $user->ID, $password['uuid'] );
if ( is_wp_error( $deleted ) ) {
return $deleted;
}
return new WP_REST_Response(
array(
'deleted' => true,
'previous' => $previous->get_data(),
)
);
}
/**
* Performs a permissions check for the request.
*
* @since 5.6.0
*
* @param WP_REST_Request $request
* @return true|WP_Error
*/
protected function do_permissions_check( $request ) {
$user = $this->get_user( $request );
if ( is_wp_error( $user ) ) {
return $user;
}
if ( ! current_user_can( 'edit_user', $user->ID ) ) {
return new WP_Error(
'rest_cannot_manage_application_passwords',
__( 'Sorry, you are not allowed to manage application passwords for this user.' ),
array( 'status' => rest_authorization_required_code() )
);
}
return true;
}
/**
* Prepares an application password for a create or update operation.
*
* @since 5.6.0
*
* @param WP_REST_Request $request Request object.
* @return object|WP_Error The prepared item, or WP_Error object on failure.
*/
protected function prepare_item_for_database( $request ) {
$prepared = (object) array(
'name' => $request['name'],
);
/**
* Filters an application password before it is inserted via the REST API.
*
* @since 5.6.0
*
* @param stdClass $prepared An object representing a single application password prepared for inserting or updating the database.
* @param WP_REST_Request $request Request object.
*/
return apply_filters( 'rest_pre_insert_application_password', $prepared, $request );
}
/**
* Prepares the application password for the REST response.
*
* @since 5.6.0
*
* @param array $item WordPress representation of the item.
* @param WP_REST_Request $request Request object.
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
*/
public function prepare_item_for_response( $item, $request ) {
$user = $this->get_user( $request );
if ( is_wp_error( $user ) ) {
return $user;
}
$prepared = array(
'uuid' => $item['uuid'],
'name' => $item['name'],
'created' => gmdate( 'Y-m-d\TH:i:s', $item['created'] ),
'last_used' => $item['last_used'] ? gmdate( 'Y-m-d\TH:i:s', $item['last_used'] ) : null,
'last_ip' => $item['last_ip'] ? $item['last_ip'] : null,
);
if ( isset( $item['new_password'] ) ) {
$prepared['password'] = $item['new_password'];
}
$prepared = $this->add_additional_fields_to_object( $prepared, $request );
$prepared = $this->filter_response_by_context( $prepared, $request['context'] );
$response = new WP_REST_Response( $prepared );
$response->add_links( $this->prepare_links( $user, $item ) );
/**
* Filters the REST API response for an application password.
*
* @since 5.6.0
*
* @param WP_REST_Response $response The response object.
* @param array $item The application password array.
* @param WP_REST_Request $request The request object.
*/
return apply_filters( 'rest_prepare_application_password', $response, $item, $request );
}
/**
* Prepares links for the object.
*
* @since 5.6.0
*
* @param WP_User $user The requested user.
* @param array $item The application password.
* @return array The list of links.
*/
protected function prepare_links( WP_User $user, $item ) {
return array(
'self' => array(
'href' => rest_url( sprintf( '%s/users/%d/application-passwords/%s', $this->namespace, $user->ID, $item['uuid'] ) ),
),
);
}
/**
* Gets the requested user.
*
* @since 5.6.0
*
* @param WP_REST_Request $request The request object.
* @return WP_User|WP_Error The WordPress user associated with the request, or a WP_Error if none found.
*/
protected function get_user( $request ) {
if ( ! wp_is_application_passwords_available() ) {
return new WP_Error(
'application_passwords_disabled',
__( 'Application passwords are not enabled.' ),
array( 'status' => 500 )
);
}
$error = new WP_Error(
'rest_user_invalid_id',
__( 'Invalid user ID.' ),
array( 'status' => 404 )
);
$id = $request['user_id'];
if ( 'me' === $id ) {
if ( ! is_user_logged_in() ) {
return new WP_Error(
'rest_not_logged_in',
__( 'You are not currently logged in.' ),
array( 'status' => 401 )
);
}
$user = wp_get_current_user();
} else {
$id = (int) $id;
if ( $id <= 0 ) {
return $error;
}
$user = get_userdata( $id );
}
if ( empty( $user ) || ! $user->exists() ) {
return $error;
}
if ( is_multisite() && ! is_user_member_of_blog( $user->ID ) ) {
return $error;
}
if ( ! wp_is_application_passwords_available_for_user( $user ) ) {
return new WP_Error(
'application_passwords_disabled_for_user',
__( 'Application passwords are not enabled for your account. Please contact the site administrator for assistance.' ),
array( 'status' => 500 )
);
}
return $user;
}
/**
* Gets the requested application password.
*
* @since 5.6.0
*
* @param WP_REST_Request $request The request object.
* @return array|WP_Error The application password details if found, a WP_Error otherwise.
*/
protected function get_application_password( $request ) {
$user = $this->get_user( $request );
if ( is_wp_error( $user ) ) {
return $user;
}
$password = WP_Application_Passwords::get_user_application_password( $user->ID, $request['uuid'] );
if ( ! $password ) {
return new WP_Error(
'rest_application_password_not_found',
__( 'Application password not found.' ),
array( 'status' => 404 )
);
}
return $password;
}
/**
* Retrieves the query params for the collections.
*
* @since 5.6.0
*
* @return array Query parameters for the collection.
*/
public function get_collection_params() {
return array(
'context' => $this->get_context_param( array( 'default' => 'view' ) ),
);
}
/**
* Retrieves the application password's schema, conforming to JSON Schema.
*
* @since 5.6.0
*
* @return array Item schema data.
*/
public function get_item_schema() {
if ( $this->schema ) {
return $this->add_additional_fields_schema( $this->schema );
}
$this->schema = array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => 'application-password',
'type' => 'object',
'properties' => array(
'uuid' => array(
'description' => __( 'The unique identifier for the application password.' ),
'type' => 'string',
'format' => 'uuid',
'context' => array( 'view', 'edit', 'embed' ),
'readonly' => true,
),
'name' => array(
'description' => __( 'The name of the application password.' ),
'type' => 'string',
'required' => true,
'context' => array( 'view', 'edit', 'embed' ),
),
'password' => array(
'description' => __( 'The generated password. Only available after adding an application.' ),
'type' => 'string',
'context' => array( 'edit' ),
'readonly' => true,
),
'created' => array(
'description' => __( 'The GMT date the application password was created.' ),
'type' => 'string',
'format' => 'date-time',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'last_used' => array(
'description' => __( 'The GMT date the application password was last used.' ),
'type' => array( 'string', 'null' ),
'format' => 'date-time',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'last_ip' => array(
'description' => __( 'The IP address the application password was last used by.' ),
'type' => array( 'string', 'null' ),
'format' => 'ip',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
),
);
return $this->add_additional_fields_schema( $this->schema );
}
}

View File

@ -1067,6 +1067,12 @@ function wp_default_scripts( $scripts ) {
);
$scripts->set_translations( 'password-strength-meter' );
$scripts->add( 'application-passwords', "/wp-admin/js/application-passwords$suffix.js", array( 'jquery', 'wp-util', 'wp-api-request', 'wp-date', 'wp-i18n', 'wp-hooks', 'wp-a11y' ), false, 1 );
$scripts->set_translations( 'application-passwords' );
$scripts->add( 'auth-app', "/wp-admin/js/auth-app$suffix.js", array( 'jquery', 'wp-api-request', 'wp-i18n', 'wp-hooks' ), false, 1 );
$scripts->set_translations( 'auth-app' );
$scripts->add( 'user-profile', "/wp-admin/js/user-profile$suffix.js", array( 'jquery', 'password-strength-meter', 'wp-util' ), false, 1 );
$scripts->set_translations( 'user-profile' );

View File

@ -297,6 +297,176 @@ function wp_authenticate_cookie( $user, $username, $password ) {
return $user;
}
/**
* Authenticates the user using an application password.
*
* @since 5.6.0
*
* @param WP_User|WP_Error|null $input_user WP_User or WP_Error object if a previous
* callback failed authentication.
* @param string $username Username for authentication.
* @param string $password Password for authentication.
* @return WP_User|WP_Error WP_User on success, WP_Error on failure.
*/
function wp_authenticate_application_password( $input_user, $username, $password ) {
if ( $input_user instanceof WP_User ) {
return $input_user;
}
$is_api_request = ( ( defined( 'XMLRPC_REQUEST' ) && XMLRPC_REQUEST ) || ( defined( 'REST_REQUEST' ) && REST_REQUEST ) );
/**
* Filters whether this is an API request that Application Passwords can be used on.
*
* By default, Application Passwords is available for the REST API and XML-RPC.
*
* @since 5.6.0
*
* @param bool $is_api_request If this is an acceptable API request.
*/
$is_api_request = apply_filters( 'application_password_is_api_request', $is_api_request );
if ( ! $is_api_request ) {
return $input_user;
}
$error = null;
$user = get_user_by( 'login', $username );
if ( ! $user && is_email( $username ) ) {
$user = get_user_by( 'email', $username );
}
// If the login name is invalid, short circuit.
if ( ! $user ) {
if ( is_email( $username ) ) {
$error = new WP_Error(
'invalid_email',
__( 'Unknown email address. Check again or try your username.' )
);
} else {
$error = new WP_Error(
'invalid_username',
__( 'Unknown username. Check again or try your email address.' )
);
}
} elseif ( ! wp_is_application_passwords_available_for_user( $user ) ) {
$error = new WP_Error(
'application_passwords_disabled',
__( 'Application passwords are disabled for the requested user.' )
);
}
if ( $error ) {
/**
* Fires when an application password failed to authenticate the user.
*
* @since 5.6.0
*
* @param WP_Error $error The authentication error.
*/
do_action( 'application_password_failed_authentication', $error );
return $error;
}
/*
* Strip out anything non-alphanumeric. This is so passwords can be used with
* or without spaces to indicate the groupings for readability.
*
* Generated application passwords are exclusively alphanumeric.
*/
$password = preg_replace( '/[^a-z\d]/i', '', $password );
$hashed_passwords = WP_Application_Passwords::get_user_application_passwords( $user->ID );
foreach ( $hashed_passwords as $key => $item ) {
if ( ! wp_check_password( $password, $item['password'], $user->ID ) ) {
continue;
}
$error = new WP_Error();
/**
* Fires when an application password has been successfully checked as valid.
*
* This allows for plugins to add additional constraints to prevent an application password from being used.
*
* @since 5.6.0
*
* @param WP_Error $error The error object.
* @param WP_User $user The user authenticating.
* @param array $item The details about the application password.
* @param string $password The raw supplied password.
*/
do_action( 'wp_authenticate_application_password_errors', $error, $user, $item, $password );
if ( is_wp_error( $error ) && $error->has_errors() ) {
/** This action is documented in wp-includes/user.php */
do_action( 'application_password_failed_authentication', $error );
return $error;
}
WP_Application_Passwords::record_application_password_usage( $user->ID, $item['uuid'] );
/**
* Fires after an application password was used for authentication.
*
* @since 5.6.0
*
* @param WP_User $user The user who was authenticated.
* @param array $item The application password used.
*/
do_action( 'application_password_did_authenticate', $user, $item );
return $user;
}
$error = new WP_Error(
'incorrect_password',
__( 'The provided password is an invalid application password.' )
);
/** This action is documented in wp-includes/user.php */
do_action( 'application_password_failed_authentication', $error );
return $error;
}
/**
* Validates the application password credentials passed via Basic Authentication.
*
* @since 5.6.0
*
* @param int|bool $input_user User ID if one has been determined, false otherwise.
* @return int|bool The authenticated user ID if successful, false otherwise.
*/
function wp_validate_application_password( $input_user ) {
// Don't authenticate twice.
if ( ! empty( $input_user ) ) {
return $input_user;
}
if ( ! wp_is_application_passwords_available() ) {
return $input_user;
}
// Check that we're trying to authenticate
if ( ! isset( $_SERVER['PHP_AUTH_USER'] ) ) {
return $input_user;
}
$authenticated = wp_authenticate_application_password( null, $_SERVER['PHP_AUTH_USER'], $_SERVER['PHP_AUTH_PW'] );
if ( $authenticated instanceof WP_User ) {
return $authenticated->ID;
}
// If it wasn't a user what got returned, just pass on what we had received originally.
return $input_user;
}
/**
* For Multisite blogs, check if the authenticated user has been marked as a
* spammer, or if the user's primary blog has been marked as spam.
@ -3923,3 +4093,59 @@ function wp_get_user_request( $request_id ) {
return new WP_User_Request( $post );
}
/**
* Checks if Application Passwords is globally available.
*
* By default, Application Passwords is available to all sites using SSL, but this function is
* filterable to adjust its availability.
*
* @since 5.6.0
*
* @return bool
*/
function wp_is_application_passwords_available() {
/**
* Filters whether Application Passwords is available.
*
* @since 5.6.0
*
* @param bool $available True if available, false otherwise.
*/
return apply_filters( 'wp_is_application_passwords_available', is_ssl() );
}
/**
* Checks if Application Passwords is enabled for a specific user.
*
* By default all users can use Application Passwords, but this function is filterable to restrict
* availability to certain users.
*
* @since 5.6.0
*
* @param int|WP_User $user The user to check.
* @return bool
*/
function wp_is_application_passwords_available_for_user( $user ) {
if ( ! wp_is_application_passwords_available() ) {
return false;
}
if ( ! is_object( $user ) ) {
$user = get_userdata( $user );
}
if ( ! $user || ! $user->exists() ) {
return false;
}
/**
* Filters whether Application Passwords is available for a specific user.
*
* @since 5.6.0
*
* @param bool $available True if available, false otherwise.
* @param WP_User $user The user to check.
*/
return apply_filters( 'wp_is_application_passwords_available_for_user', true, $user );
}

View File

@ -1371,6 +1371,19 @@ switch ( $action ) {
$errors->add( 'updated', __( '<strong>You have successfully updated WordPress!</strong> Please log back in to see what&#8217;s new.' ), 'message' );
} elseif ( WP_Recovery_Mode_Link_Service::LOGIN_ACTION_ENTERED === $action ) {
$errors->add( 'enter_recovery_mode', __( 'Recovery Mode Initialized. Please log in to continue.' ), 'message' );
} elseif ( isset( $_GET['redirect_to'] ) && false !== strpos( $_GET['redirect_to'], 'wp-admin/authorize-application.php' ) ) {
$query_component = wp_parse_url( $_GET['redirect_to'], PHP_URL_QUERY );
parse_str( $query_component, $query );
if ( ! empty( $query['app_name'] ) ) {
/* translators: 1: Website name, 2: Application name. */
$message = sprintf( 'Please log in to %1$s to authorize %2$s to connect to your account.', get_bloginfo( 'name', 'display' ), '<strong>' . esc_html( $query['app_name'] ) . '</strong>' );
} else {
/* translators: Website name. */
$message = sprintf( 'Please log in to %s to proceed with authorization.', get_bloginfo( 'name', 'display' ) );
}
$errors->add( 'authorize_application', $message, 'message' );
}
}

View File

@ -236,6 +236,7 @@ require ABSPATH . WPINC . '/class-wp-widget-factory.php';
require ABSPATH . WPINC . '/nav-menu.php';
require ABSPATH . WPINC . '/nav-menu-template.php';
require ABSPATH . WPINC . '/admin-bar.php';
require ABSPATH . WPINC . '/class-wp-application-passwords.php';
require ABSPATH . WPINC . '/rest-api.php';
require ABSPATH . WPINC . '/rest-api/class-wp-rest-server.php';
require ABSPATH . WPINC . '/rest-api/class-wp-rest-response.php';
@ -259,6 +260,7 @@ require ABSPATH . WPINC . '/rest-api/endpoints/class-wp-rest-settings-controller
require ABSPATH . WPINC . '/rest-api/endpoints/class-wp-rest-themes-controller.php';
require ABSPATH . WPINC . '/rest-api/endpoints/class-wp-rest-plugins-controller.php';
require ABSPATH . WPINC . '/rest-api/endpoints/class-wp-rest-block-directory-controller.php';
require ABSPATH . WPINC . '/rest-api/endpoints/class-wp-rest-application-passwords-controller.php';
require ABSPATH . WPINC . '/rest-api/fields/class-wp-rest-meta-fields.php';
require ABSPATH . WPINC . '/rest-api/fields/class-wp-rest-comment-meta-fields.php';
require ABSPATH . WPINC . '/rest-api/fields/class-wp-rest-post-meta-fields.php';

View File

@ -0,0 +1,58 @@
<?php
/**
* @group admin
* @group user
*/
class Tests_Admin_IncludesUser extends WP_UnitTestCase {
/**
* @ticket 42790
* @dataProvider data_is_authorize_application_password_request_valid
* @param array $request The request data to validate.
* @param string $error_code The expected error code, empty if no error.
*/
public function test_is_authorize_application_password_request_valid( $request, $error_code ) {
$error = wp_is_authorize_application_password_request_valid( $request, get_userdata( 1 ) );
if ( $error_code ) {
$this->assertWPError( $error );
$this->assertEquals( $error_code, $error->get_error_code() );
} else {
$this->assertNotWPError( $error );
}
}
public function data_is_authorize_application_password_request_valid() {
return array(
array(
array(),
'',
),
array(
array( 'success_url' => 'http://example.org' ),
'invalid_redirect_scheme',
),
array(
array( 'reject_url' => 'http://example.org' ),
'invalid_redirect_scheme',
),
array(
array( 'success_url' => 'https://example.org' ),
'',
),
array(
array( 'reject_url' => 'https://example.org' ),
'',
),
array(
array( 'success_url' => 'wordpress://example' ),
'',
),
array(
array( 'reject_url' => 'wordpress://example' ),
'',
),
);
}
}

View File

@ -6,6 +6,10 @@
*/
class Tests_Auth extends WP_UnitTestCase {
protected $user;
/**
* @var WP_User
*/
protected static $_user;
protected static $user_id;
protected static $wp_hasher;
@ -35,6 +39,13 @@ class Tests_Auth extends WP_UnitTestCase {
wp_set_current_user( self::$user_id );
}
public function tearDown() {
parent::tearDown();
// Cleanup all the global state.
unset( $_SERVER['PHP_AUTH_USER'], $_SERVER['PHP_AUTH_PW'], $GLOBALS['wp_rest_application_password_status'] );
}
function test_auth_cookie_valid() {
$cookie = wp_generate_auth_cookie( self::$user_id, time() + 3600, 'auth' );
$this->assertSame( self::$user_id, wp_validate_auth_cookie( $cookie, 'auth' ) );
@ -414,4 +425,183 @@ class Tests_Auth extends WP_UnitTestCase {
$this->assertInstanceOf( 'WP_User', wp_signon() );
}
/**
* HTTP Auth headers are used to determine the current user.
*
* @ticket 42790
*
* @covers ::wp_validate_application_password
*/
public function test_application_password_authentication() {
$user_id = $this->factory()->user->create(
array(
'user_login' => 'http_auth_login',
'user_pass' => 'http_auth_pass', // Shouldn't be allowed for API login.
)
);
// Create a new app-only password.
list( $user_app_password ) = WP_Application_Passwords::create_new_application_password( $user_id, array( 'name' => 'phpunit' ) );
// Fake a REST API request.
add_filter( 'application_password_is_api_request', '__return_true' );
add_filter( 'wp_is_application_passwords_available', '__return_true' );
// Fake an HTTP Auth request with the regular account password first.
$_SERVER['PHP_AUTH_USER'] = 'http_auth_login';
$_SERVER['PHP_AUTH_PW'] = 'http_auth_pass';
$this->assertEquals(
0,
wp_validate_application_password( null ),
'Regular user account password should not be allowed for API authentication'
);
// Not try with an App password instead.
$_SERVER['PHP_AUTH_PW'] = $user_app_password;
$this->assertEquals(
$user_id,
wp_validate_application_password( null ),
'Application passwords should be allowed for API authentication'
);
}
/**
* @ticket 42790
*/
public function test_authenticate_application_password_respects_existing_user() {
$this->assertSame( self::$_user, wp_authenticate_application_password( self::$_user, self::$_user->user_login, 'password' ) );
}
/**
* @ticket 42790
*/
public function test_authenticate_application_password_is_rejected_if_not_api_request() {
add_filter( 'application_password_is_api_request', '__return_false' );
$this->assertNull( wp_authenticate_application_password( null, self::$_user->user_login, 'password' ) );
}
/**
* @ticket 42790
*/
public function test_authenticate_application_password_invalid_username() {
add_filter( 'application_password_is_api_request', '__return_true' );
$error = wp_authenticate_application_password( null, 'idonotexist', 'password' );
$this->assertWPError( $error );
$this->assertEquals( 'invalid_username', $error->get_error_code() );
}
/**
* @ticket 42790
*/
public function test_authenticate_application_password_invalid_email() {
add_filter( 'application_password_is_api_request', '__return_true' );
$error = wp_authenticate_application_password( null, 'idonotexist@example.org', 'password' );
$this->assertWPError( $error );
$this->assertEquals( 'invalid_email', $error->get_error_code() );
}
/**
* @ticket 42790
*/
public function test_authenticate_application_password_not_allowed() {
add_filter( 'application_password_is_api_request', '__return_true' );
add_filter( 'wp_is_application_passwords_available', '__return_false' );
$error = wp_authenticate_application_password( null, self::$_user->user_login, 'password' );
$this->assertWPError( $error );
$this->assertEquals( 'application_passwords_disabled', $error->get_error_code() );
}
/**
* @ticket 42790
*/
public function test_authenticate_application_password_not_allowed_for_user() {
add_filter( 'application_password_is_api_request', '__return_true' );
add_filter( 'wp_is_application_passwords_available', '__return_true' );
add_filter( 'wp_is_application_passwords_available_for_user', '__return_false' );
$error = wp_authenticate_application_password( null, self::$_user->user_login, 'password' );
$this->assertWPError( $error );
$this->assertEquals( 'application_passwords_disabled', $error->get_error_code() );
}
/**
* @ticket 42790
*/
public function test_authenticate_application_password_incorrect_password() {
add_filter( 'application_password_is_api_request', '__return_true' );
add_filter( 'wp_is_application_passwords_available', '__return_true' );
$error = wp_authenticate_application_password( null, self::$_user->user_login, 'password' );
$this->assertWPError( $error );
$this->assertEquals( 'incorrect_password', $error->get_error_code() );
}
/**
* @ticket 42790
*/
public function test_authenticate_application_password_custom_errors() {
add_filter( 'application_password_is_api_request', '__return_true' );
add_filter( 'wp_is_application_passwords_available', '__return_true' );
add_action(
'wp_authenticate_application_password_errors',
static function ( WP_Error $error ) {
$error->add( 'my_code', 'My Error' );
}
);
list( $password ) = WP_Application_Passwords::create_new_application_password( self::$user_id, array( 'name' => 'phpunit' ) );
$error = wp_authenticate_application_password( null, self::$_user->user_login, $password );
$this->assertWPError( $error );
$this->assertEquals( 'my_code', $error->get_error_code() );
}
/**
* @ticket 42790
*/
public function test_authenticate_application_password_by_username() {
add_filter( 'application_password_is_api_request', '__return_true' );
add_filter( 'wp_is_application_passwords_available', '__return_true' );
list( $password ) = WP_Application_Passwords::create_new_application_password( self::$user_id, array( 'name' => 'phpunit' ) );
$user = wp_authenticate_application_password( null, self::$_user->user_login, $password );
$this->assertInstanceOf( WP_User::class, $user );
$this->assertEquals( self::$user_id, $user->ID );
}
/**
* @ticket 42790
*/
public function test_authenticate_application_password_by_email() {
add_filter( 'application_password_is_api_request', '__return_true' );
add_filter( 'wp_is_application_passwords_available', '__return_true' );
list( $password ) = WP_Application_Passwords::create_new_application_password( self::$user_id, array( 'name' => 'phpunit' ) );
$user = wp_authenticate_application_password( null, self::$_user->user_email, $password );
$this->assertInstanceOf( WP_User::class, $user );
$this->assertEquals( self::$user_id, $user->ID );
}
/**
* @ticket 42790
*/
public function test_authenticate_application_password_chunked() {
add_filter( 'application_password_is_api_request', '__return_true' );
add_filter( 'wp_is_application_passwords_available', '__return_true' );
list( $password ) = WP_Application_Passwords::create_new_application_password( self::$user_id, array( 'name' => 'phpunit' ) );
$user = wp_authenticate_application_password( null, self::$_user->user_email, WP_Application_Passwords::chunk_password( $password ) );
$this->assertInstanceOf( WP_User::class, $user );
$this->assertEquals( self::$user_id, $user->ID );
}
}

View File

@ -0,0 +1,823 @@
<?php
/**
* Unit tests covering WP_REST_Application_Passwords_Controller functionality.
*
* @package WordPress
* @subpackage REST API
*/
/**
* @group restapi
*/
class WP_Test_REST_Application_Passwords_Controller extends WP_Test_REST_Controller_Testcase {
/**
* Subscriber user ID.
*
* @since 5.6.0
*
* @var int
*/
private static $subscriber_id;
/**
* Administrator user id.
*
* @since 5.6.0
*
* @var int
*/
private static $admin;
/**
* Set up class test fixtures.
*
* @since 5.6.0
*
* @param WP_UnitTest_Factory $factory WordPress unit test factory.
*/
public static function wpSetUpBeforeClass( $factory ) {
self::$subscriber_id = $factory->user->create(
array(
'role' => 'subscriber',
)
);
self::$admin = $factory->user->create(
array(
'role' => 'administrator',
)
);
if ( is_multisite() ) {
grant_super_admin( self::$admin );
}
}
/**
* Clean up test fixtures.
*
* @since 5.6.0
*/
public static function wpTearDownAfterClass() {
self::delete_user( self::$subscriber_id );
self::delete_user( self::$admin );
}
public function setUp() {
parent::setUp();
add_filter( 'wp_is_application_passwords_available', '__return_true' );
}
/**
* @ticket 42790
*/
public function test_register_routes() {
$routes = rest_get_server()->get_routes();
$this->assertArrayHasKey( '/wp/v2/users/(?P<user_id>(?:[\\d]+|me))/application-passwords', $routes );
$this->assertCount( 3, $routes['/wp/v2/users/(?P<user_id>(?:[\\d]+|me))/application-passwords'] );
$this->assertArrayHasKey( '/wp/v2/users/(?P<user_id>(?:[\\d]+|me))/application-passwords/(?P<uuid>[\\w\\-]+)', $routes );
$this->assertCount( 3, $routes['/wp/v2/users/(?P<user_id>(?:[\\d]+|me))/application-passwords/(?P<uuid>[\\w\\-]+)'] );
}
/**
* @ticket 42790
*/
public function test_context_param() {
wp_set_current_user( self::$admin );
list( , $item ) = WP_Application_Passwords::create_new_application_password( self::$admin, array( 'name' => 'App' ) );
$uuid = $item['uuid'];
// Collection.
$request = new WP_REST_Request( 'OPTIONS', '/wp/v2/users/me/application-passwords' );
$response = rest_get_server()->dispatch( $request );
$data = $response->get_data();
$this->assertSame( 'view', $data['endpoints'][0]['args']['context']['default'] );
$this->assertSame( array( 'view', 'embed', 'edit' ), $data['endpoints'][0]['args']['context']['enum'] );
// Single.
$request = new WP_REST_Request( 'OPTIONS', '/wp/v2/users/me/application-passwords/' . $uuid );
$response = rest_get_server()->dispatch( $request );
$data = $response->get_data();
$this->assertSame( 'view', $data['endpoints'][0]['args']['context']['default'] );
$this->assertSame( array( 'view', 'embed', 'edit' ), $data['endpoints'][0]['args']['context']['enum'] );
}
/**
* @ticket 42790
*/
public function test_disabled() {
wp_set_current_user( self::$admin );
add_filter( 'wp_is_application_passwords_available', '__return_false' );
$response = rest_do_request( '/wp/v2/users/me/application-passwords' );
$this->assertErrorResponse( 'application_passwords_disabled', $response, 500 );
}
/**
* @ticket 42790
*/
public function test_disabled_for_user() {
wp_set_current_user( self::$admin );
add_filter( 'wp_is_application_passwords_available_for_user', '__return_false' );
$response = rest_do_request( '/wp/v2/users/me/application-passwords' );
$this->assertErrorResponse( 'application_passwords_disabled_for_user', $response, 500 );
}
/**
* @ticket 42790
*/
public function test_get_items() {
wp_set_current_user( self::$admin );
list( , $item ) = WP_Application_Passwords::create_new_application_password( self::$admin, array( 'name' => 'App' ) );
$response = rest_do_request( '/wp/v2/users/me/application-passwords' );
$this->assertEquals( 200, $response->get_status() );
$this->assertCount( 1, $response->get_data() );
$this->check_response( $response->get_data()[0], $item );
}
/**
* @ticket 42790
*/
public function test_get_items_self_user_id_admin() {
wp_set_current_user( self::$admin );
list( , $item ) = WP_Application_Passwords::create_new_application_password( self::$admin, array( 'name' => 'App' ) );
$response = rest_do_request( sprintf( '/wp/v2/users/%d/application-passwords', self::$admin ) );
$this->assertEquals( 200, $response->get_status() );
$this->assertCount( 1, $response->get_data() );
$this->check_response( $response->get_data()[0], $item );
}
/**
* @ticket 42790
*/
public function test_get_items_self_user_id_subscriber() {
wp_set_current_user( self::$subscriber_id );
list( , $item ) = WP_Application_Passwords::create_new_application_password( self::$subscriber_id, array( 'name' => 'App' ) );
$response = rest_do_request( sprintf( '/wp/v2/users/%d/application-passwords', self::$subscriber_id ) );
$this->assertEquals( 200, $response->get_status() );
$this->assertCount( 1, $response->get_data() );
$this->check_response( $response->get_data()[0], $item );
}
/**
* @ticket 42790
*/
public function test_get_items_other_user_id() {
wp_set_current_user( self::$admin );
list( , $item ) = WP_Application_Passwords::create_new_application_password( self::$subscriber_id, array( 'name' => 'App' ) );
$response = rest_do_request( sprintf( '/wp/v2/users/%d/application-passwords', self::$subscriber_id ) );
$this->assertEquals( 200, $response->get_status() );
$this->assertCount( 1, $response->get_data() );
$this->check_response( $response->get_data()[0], $item );
}
/**
* @ticket 42790
*/
public function test_get_items_other_user_id_invalid_permission() {
wp_set_current_user( self::$subscriber_id );
$response = rest_do_request( sprintf( '/wp/v2/users/%d/application-passwords', self::$admin ) );
$this->assertErrorResponse( 'rest_cannot_manage_application_passwords', $response, 403 );
}
/**
* @ticket 42790
*/
public function test_get_items_logged_out() {
$response = rest_do_request( '/wp/v2/users/me/application-passwords' );
$this->assertErrorResponse( 'rest_not_logged_in', $response, 401 );
}
/**
* @ticket 42790
*/
public function test_get_items_invalid_user_id() {
wp_set_current_user( self::$admin );
$response = rest_do_request( '/wp/v2/users/0/application-passwords' );
$this->assertErrorResponse( 'rest_user_invalid_id', $response, 404 );
}
/**
* @ticket 42790
*/
public function test_get_item() {
wp_set_current_user( self::$admin );
list( , $item ) = WP_Application_Passwords::create_new_application_password( self::$admin, array( 'name' => 'App' ) );
$uuid = $item['uuid'];
$response = rest_do_request( '/wp/v2/users/me/application-passwords/' . $uuid );
$this->assertEquals( 200, $response->get_status() );
$this->check_response( $response->get_data(), $item );
}
/**
* @ticket 42790
*/
public function test_get_item_self_user_id_admin() {
wp_set_current_user( self::$admin );
list( , $item ) = WP_Application_Passwords::create_new_application_password( self::$admin, array( 'name' => 'App' ) );
$uuid = $item['uuid'];
$response = rest_do_request( sprintf( '/wp/v2/users/%d/application-passwords/%s', self::$admin, $uuid ) );
$this->assertEquals( 200, $response->get_status() );
$this->check_response( $response->get_data(), $item );
}
/**
* @ticket 42790
*/
public function test_get_item_self_user_id_subscriber() {
wp_set_current_user( self::$subscriber_id );
list( , $item ) = WP_Application_Passwords::create_new_application_password( self::$subscriber_id, array( 'name' => 'App' ) );
$uuid = $item['uuid'];
$response = rest_do_request( sprintf( '/wp/v2/users/%d/application-passwords/%s', self::$subscriber_id, $uuid ) );
$this->assertEquals( 200, $response->get_status() );
$this->check_response( $response->get_data(), $item );
}
/**
* @ticket 42790
*/
public function test_get_item_other_user_id() {
wp_set_current_user( self::$admin );
list( , $item ) = WP_Application_Passwords::create_new_application_password( self::$subscriber_id, array( 'name' => 'App' ) );
$uuid = $item['uuid'];
$response = rest_do_request( sprintf( '/wp/v2/users/%d/application-passwords/%s', self::$subscriber_id, $uuid ) );
$this->assertEquals( 200, $response->get_status() );
$this->check_response( $response->get_data(), $item );
}
/**
* @ticket 42790
*/
public function test_get_item_other_user_id_invalid_permission() {
wp_set_current_user( self::$subscriber_id );
list( , $item ) = WP_Application_Passwords::create_new_application_password( self::$admin, array( 'name' => 'App' ) );
$uuid = $item['uuid'];
$response = rest_do_request( sprintf( '/wp/v2/users/%d/application-passwords/%s', self::$admin, $uuid ) );
$this->assertErrorResponse( 'rest_cannot_manage_application_passwords', $response, 403 );
}
/**
* @ticket 42790
*/
public function test_get_item_logged_out() {
list( , $item ) = WP_Application_Passwords::create_new_application_password( self::$admin, array( 'name' => 'App' ) );
$uuid = $item['uuid'];
$response = rest_do_request( sprintf( '/wp/v2/users/me/application-passwords/%s', $uuid ) );
$this->assertErrorResponse( 'rest_not_logged_in', $response, 401 );
}
/**
* @ticket 42790
*/
public function test_get_item_invalid_user_id() {
wp_set_current_user( self::$admin );
list( , $item ) = WP_Application_Passwords::create_new_application_password( self::$admin, array( 'name' => 'App' ) );
$uuid = $item['uuid'];
$response = rest_do_request( '/wp/v2/users/0/application-passwords/' . $uuid );
$this->assertErrorResponse( 'rest_user_invalid_id', $response, 404 );
}
/**
* @ticket 42790
*/
public function test_get_item_invalid_password_uuid() {
wp_set_current_user( self::$admin );
$response = rest_do_request( sprintf( '/wp/v2/users/%d/application-passwords/%s', self::$admin, '123456abcdef' ) );
$this->assertErrorResponse( 'rest_application_password_not_found', $response, 404 );
}
/**
* @ticket 42790
*/
public function test_create_item() {
wp_set_current_user( self::$admin );
$request = new WP_REST_Request( 'POST', '/wp/v2/users/me/application-passwords' );
$request->set_body_params( array( 'name' => 'App' ) );
$response = rest_do_request( $request );
$this->assertEquals( 201, $response->get_status() );
$passwords = WP_Application_Passwords::get_user_application_passwords( self::$admin );
$this->assertCount( 1, $passwords );
$this->check_response( $response->get_data(), $passwords[0], true );
$this->assertEquals( 'App', $response->get_data()['name'] );
$this->assertNull( $response->get_data()['last_used'] );
$this->assertNull( $response->get_data()['last_ip'] );
}
/**
* @ticket 42790
*/
public function test_create_item_self_user_id_admin() {
wp_set_current_user( self::$admin );
$request = new WP_REST_Request( 'POST', sprintf( '/wp/v2/users/%d/application-passwords', self::$admin ) );
$request->set_body_params( array( 'name' => 'App' ) );
$response = rest_do_request( $request );
$this->assertEquals( 201, $response->get_status() );
$passwords = WP_Application_Passwords::get_user_application_passwords( self::$admin );
$this->assertCount( 1, $passwords );
$this->check_response( $response->get_data(), $passwords[0], true );
}
/**
* @ticket 42790
*/
public function test_create_item_self_user_id_subscriber() {
wp_set_current_user( self::$subscriber_id );
$request = new WP_REST_Request( 'POST', sprintf( '/wp/v2/users/%d/application-passwords', self::$subscriber_id ) );
$request->set_body_params( array( 'name' => 'App' ) );
$response = rest_do_request( $request );
$this->assertEquals( 201, $response->get_status() );
$passwords = WP_Application_Passwords::get_user_application_passwords( self::$subscriber_id );
$this->assertCount( 1, $passwords );
$this->check_response( $response->get_data(), $passwords[0], true );
}
/**
* @ticket 42790
*/
public function test_create_item_other_user_id() {
wp_set_current_user( self::$admin );
$request = new WP_REST_Request( 'POST', sprintf( '/wp/v2/users/%d/application-passwords', self::$subscriber_id ) );
$request->set_body_params( array( 'name' => 'App' ) );
$response = rest_do_request( $request );
$this->assertEquals( 201, $response->get_status() );
$passwords = WP_Application_Passwords::get_user_application_passwords( self::$subscriber_id );
$this->assertCount( 1, $passwords );
$this->check_response( $response->get_data(), $passwords[0], true );
}
/**
* @ticket 42790
*/
public function test_create_item_other_user_id_invalid_permission() {
wp_set_current_user( self::$subscriber_id );
$request = new WP_REST_Request( 'POST', sprintf( '/wp/v2/users/%d/application-passwords', self::$admin ) );
$request->set_body_params( array( 'name' => 'App' ) );
$response = rest_do_request( $request );
$this->assertErrorResponse( 'rest_cannot_manage_application_passwords', $response, 403 );
}
/**
* @ticket 42790
*/
public function test_create_item_invalid_user_id() {
wp_set_current_user( self::$admin );
$request = new WP_REST_Request( 'POST', sprintf( '/wp/v2/users/%d/application-passwords', 0 ) );
$request->set_body_params( array( 'name' => 'App' ) );
$response = rest_do_request( $request );
$this->assertErrorResponse( 'rest_user_invalid_id', $response, 404 );
}
/**
* @ticket 42790
*/
public function test_update_item() {
wp_set_current_user( self::$admin );
list( , $item ) = WP_Application_Passwords::create_new_application_password( self::$admin, array( 'name' => 'App' ) );
$uuid = $item['uuid'];
$request = new WP_REST_Request( 'PUT', '/wp/v2/users/me/application-passwords/' . $uuid );
$request->set_body_params( array( 'name' => 'New App' ) );
$response = rest_do_request( $request );
$this->assertEquals( 200, $response->get_status() );
$this->check_response( $response->get_data(), WP_Application_Passwords::get_user_application_password( self::$admin, $item['uuid'] ) );
$this->assertEquals( 'New App', $response->get_data()['name'] );
}
/**
* @ticket 42790
*/
public function test_update_item_self_user_id_admin() {
wp_set_current_user( self::$admin );
list( , $item ) = WP_Application_Passwords::create_new_application_password( self::$admin, array( 'name' => 'App' ) );
$uuid = $item['uuid'];
$request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/users/%d/application-passwords/%s', self::$admin, $uuid ) );
$request->set_body_params( array( 'name' => 'New App' ) );
$response = rest_do_request( $request );
$this->assertEquals( 200, $response->get_status() );
$this->check_response( $response->get_data(), WP_Application_Passwords::get_user_application_password( self::$admin, $item['uuid'] ) );
$this->assertEquals( 'New App', $response->get_data()['name'] );
}
/**
* @ticket 42790
*/
public function test_update_item_self_user_id_subscriber() {
wp_set_current_user( self::$subscriber_id );
list( , $item ) = WP_Application_Passwords::create_new_application_password( self::$subscriber_id, array( 'name' => 'App' ) );
$uuid = $item['uuid'];
$request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/users/%d/application-passwords/%s', self::$subscriber_id, $uuid ) );
$request->set_body_params( array( 'name' => 'New App' ) );
$response = rest_do_request( $request );
$this->assertEquals( 200, $response->get_status() );
$this->check_response( $response->get_data(), WP_Application_Passwords::get_user_application_password( self::$subscriber_id, $item['uuid'] ) );
$this->assertEquals( 'New App', $response->get_data()['name'] );
}
/**
* @ticket 42790
*/
public function test_update_item_other_user_id() {
wp_set_current_user( self::$admin );
list( , $item ) = WP_Application_Passwords::create_new_application_password( self::$subscriber_id, array( 'name' => 'App' ) );
$uuid = $item['uuid'];
$request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/users/%d/application-passwords/%s', self::$subscriber_id, $uuid ) );
$request->set_body_params( array( 'name' => 'New App' ) );
$response = rest_do_request( $request );
$this->assertEquals( 200, $response->get_status() );
$this->check_response( $response->get_data(), WP_Application_Passwords::get_user_application_password( self::$subscriber_id, $item['uuid'] ) );
$this->assertEquals( 'New App', $response->get_data()['name'] );
}
/**
* @ticket 42790
*/
public function test_update_item_other_user_id_invalid_permission() {
wp_set_current_user( self::$subscriber_id );
list( , $item ) = WP_Application_Passwords::create_new_application_password( self::$admin, array( 'name' => 'App' ) );
$uuid = $item['uuid'];
$request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/users/%d/application-passwords/%s', self::$admin, $uuid ) );
$request->set_body_params( array( 'name' => 'New App' ) );
$response = rest_do_request( $request );
$this->assertErrorResponse( 'rest_cannot_manage_application_passwords', $response, 403 );
}
/**
* @ticket 42790
*/
public function test_update_item_logged_out() {
list( , $item ) = WP_Application_Passwords::create_new_application_password( self::$admin, array( 'name' => 'App' ) );
$uuid = $item['uuid'];
$request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/users/me/application-passwords/%s', $uuid ) );
$request->set_body_params( array( 'name' => 'New App' ) );
$response = rest_do_request( $request );
$this->assertErrorResponse( 'rest_not_logged_in', $response, 401 );
}
/**
* @ticket 42790
*/
public function test_update_item_invalid_user_id() {
wp_set_current_user( self::$admin );
list( , $item ) = WP_Application_Passwords::create_new_application_password( self::$admin, array( 'name' => 'App' ) );
$uuid = $item['uuid'];
$request = new WP_REST_Request( 'PUT', '/wp/v2/users/0/application-passwords/' . $uuid );
$request->set_body_params( array( 'name' => 'New App' ) );
$response = rest_do_request( $request );
$this->assertErrorResponse( 'rest_user_invalid_id', $response, 404 );
}
/**
* @ticket 42790
*/
public function test_update_item_invalid_password_uuid() {
wp_set_current_user( self::$admin );
$request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/users/%d/application-passwords/%s', self::$admin, '123456abcdef' ) );
$request->set_body_params( array( 'name' => 'New App' ) );
$response = rest_do_request( $request );
$this->assertErrorResponse( 'rest_application_password_not_found', $response, 404 );
}
/**
* @ticket 42790
*/
public function test_delete_item() {
wp_set_current_user( self::$admin );
list( , $item ) = WP_Application_Passwords::create_new_application_password( self::$admin, array( 'name' => 'App' ) );
$uuid = $item['uuid'];
$request = new WP_REST_Request( 'DELETE', '/wp/v2/users/me/application-passwords/' . $uuid );
$response = rest_do_request( $request );
$this->assertEquals( 200, $response->get_status() );
$this->assertArrayHasKey( 'deleted', $response->get_data() );
$this->assertTrue( $response->get_data()['deleted'] );
$this->assertArrayHasKey( 'previous', $response->get_data() );
$this->check_response( $response->get_data()['previous'], $item );
$this->assertNull( WP_Application_Passwords::get_user_application_password( self::$admin, $uuid ) );
}
/**
* @ticket 42790
*/
public function test_delete_item_self_user_id_admin() {
wp_set_current_user( self::$admin );
list( , $item ) = WP_Application_Passwords::create_new_application_password( self::$admin, array( 'name' => 'App' ) );
$uuid = $item ['uuid'];
$request = new WP_REST_Request( 'DELETE', sprintf( '/wp/v2/users/%d/application-passwords/%s', self::$admin, $uuid ) );
$response = rest_do_request( $request );
$this->assertEquals( 200, $response->get_status() );
$this->check_response( $response->get_data()['previous'], $item );
}
/**
* @ticket 42790
*/
public function test_delete_item_self_user_id_subscriber() {
wp_set_current_user( self::$subscriber_id );
list( , $item ) = WP_Application_Passwords::create_new_application_password( self::$subscriber_id, array( 'name' => 'App' ) );
$uuid = $item['uuid'];
$request = new WP_REST_Request( 'DELETE', sprintf( '/wp/v2/users/%d/application-passwords/%s', self::$subscriber_id, $uuid ) );
$response = rest_do_request( $request );
$this->assertEquals( 200, $response->get_status() );
$this->check_response( $response->get_data()['previous'], $item );
}
/**
* @ticket 42790
*/
public function test_delete_item_other_user_id() {
wp_set_current_user( self::$admin );
list( , $item ) = WP_Application_Passwords::create_new_application_password( self::$subscriber_id, array( 'name' => 'App' ) );
$uuid = $item['uuid'];
$request = new WP_REST_Request( 'DELETE', sprintf( '/wp/v2/users/%d/application-passwords/%s', self::$subscriber_id, $uuid ) );
$response = rest_do_request( $request );
$this->assertEquals( 200, $response->get_status() );
$this->check_response( $response->get_data()['previous'], $item );
}
/**
* @ticket 42790
*/
public function test_delete_item_other_user_id_invalid_permission() {
wp_set_current_user( self::$subscriber_id );
list( , $item ) = WP_Application_Passwords::create_new_application_password( self::$admin, array( 'name' => 'App' ) );
$uuid = $item['uuid'];
$request = new WP_REST_Request( 'DELETE', sprintf( '/wp/v2/users/%d/application-passwords/%s', self::$admin, $uuid ) );
$response = rest_do_request( $request );
$this->assertErrorResponse( 'rest_cannot_manage_application_passwords', $response, 403 );
}
/**
* @ticket 42790
*/
public function test_delete_item_logged_out() {
list( , $item ) = WP_Application_Passwords::create_new_application_password( self::$admin, array( 'name' => 'App' ) );
$uuid = $item['uuid'];
$request = new WP_REST_Request( 'DELETE', sprintf( '/wp/v2/users/me/application-passwords/%s', $uuid ) );
$response = rest_do_request( $request );
$this->assertErrorResponse( 'rest_not_logged_in', $response, 401 );
}
/**
* @ticket 42790
*/
public function test_delete_item_invalid_user_id() {
wp_set_current_user( self::$admin );
list( , $item ) = WP_Application_Passwords::create_new_application_password( self::$admin, array( 'name' => 'App' ) );
$uuid = $item['uuid'];
$request = new WP_REST_Request( 'DELETE', '/wp/v2/users/0/application-passwords/' . $uuid );
$response = rest_do_request( $request );
$this->assertErrorResponse( 'rest_user_invalid_id', $response, 404 );
}
/**
* @ticket 42790
*/
public function test_delete_item_invalid_password_uuid() {
wp_set_current_user( self::$admin );
$request = new WP_REST_Request( 'DELETE', sprintf( '/wp/v2/users/%d/application-passwords/%s', self::$admin, '123456abcdef' ) );
$response = rest_do_request( $request );
$this->assertErrorResponse( 'rest_application_password_not_found', $response, 404 );
}
/**
* @ticket 42790
*/
public function test_delete_items() {
wp_set_current_user( self::$admin );
WP_Application_Passwords::create_new_application_password( self::$admin, array( 'name' => 'App 1' ) );
WP_Application_Passwords::create_new_application_password( self::$admin, array( 'name' => 'App 2' ) );
$request = new WP_REST_Request( 'DELETE', '/wp/v2/users/me/application-passwords' );
$response = rest_do_request( $request );
$this->assertEquals( 200, $response->get_status() );
$this->assertArrayHasKey( 'deleted', $response->get_data() );
$this->assertTrue( $response->get_data()['deleted'] );
$this->assertArrayHasKey( 'count', $response->get_data() );
$this->assertEquals( 2, $response->get_data()['count'] );
$this->assertCount( 0, WP_Application_Passwords::get_user_application_passwords( self::$admin ) );
}
/**
* @ticket 42790
*/
public function test_delete_items_self_user_id_admin() {
wp_set_current_user( self::$admin );
WP_Application_Passwords::create_new_application_password( self::$admin, array( 'name' => 'App' ) );
$request = new WP_REST_Request( 'DELETE', sprintf( '/wp/v2/users/%d/application-passwords', self::$admin ) );
$response = rest_do_request( $request );
$this->assertEquals( 200, $response->get_status() );
$this->assertCount( 0, WP_Application_Passwords::get_user_application_passwords( self::$admin ) );
}
/**
* @ticket 42790
*/
public function test_delete_items_self_user_id_subscriber() {
wp_set_current_user( self::$subscriber_id );
WP_Application_Passwords::create_new_application_password( self::$subscriber_id, array( 'name' => 'App' ) );
$request = new WP_REST_Request( 'DELETE', sprintf( '/wp/v2/users/%d/application-passwords', self::$subscriber_id ) );
$response = rest_do_request( $request );
$this->assertEquals( 200, $response->get_status() );
$this->assertCount( 0, WP_Application_Passwords::get_user_application_passwords( self::$admin ) );
}
/**
* @ticket 42790
*/
public function test_delete_items_other_user_id() {
wp_set_current_user( self::$admin );
WP_Application_Passwords::create_new_application_password( self::$subscriber_id, array( 'name' => 'App' ) );
$request = new WP_REST_Request( 'DELETE', sprintf( '/wp/v2/users/%d/application-passwords', self::$subscriber_id ) );
$response = rest_do_request( $request );
$this->assertEquals( 200, $response->get_status() );
$this->assertCount( 0, WP_Application_Passwords::get_user_application_passwords( self::$admin ) );
}
/**
* @ticket 42790
*/
public function test_delete_items_other_user_id_invalid_permission() {
wp_set_current_user( self::$subscriber_id );
$request = new WP_REST_Request( 'DELETE', sprintf( '/wp/v2/users/%d/application-passwords', self::$admin ) );
$response = rest_do_request( $request );
$this->assertErrorResponse( 'rest_cannot_manage_application_passwords', $response, 403 );
}
/**
* @ticket 42790
*/
public function test_delete_items_logged_out() {
$request = new WP_REST_Request( 'DELETE', '/wp/v2/users/me/application-passwords' );
$response = rest_do_request( $request );
$this->assertErrorResponse( 'rest_not_logged_in', $response, 401 );
}
/**
* @ticket 42790
*/
public function test_delete_items_invalid_user_id() {
wp_set_current_user( self::$admin );
$request = new WP_REST_Request( 'DELETE', '/wp/v2/users/0/application-passwords' );
$response = rest_do_request( $request );
$this->assertErrorResponse( 'rest_user_invalid_id', $response, 404 );
}
/**
* @ticket 42790
*/
public function test_prepare_item() {
wp_set_current_user( self::$admin );
list( $password, $item ) = WP_Application_Passwords::create_new_application_password( self::$admin, array( 'name' => 'App' ) );
$uuid = $item['uuid'];
$item['uuid'] = $uuid;
$item['new_password'] = $password;
$request = new WP_REST_Request( 'GET', '/wp/v2/users/me/application-passwords/' . $uuid );
$request->set_param( 'context', 'edit' );
$request->set_url_params(
array(
'user_id' => 'me',
'uuid' => $uuid,
)
);
$prepared = ( new WP_REST_Application_Passwords_Controller() )->prepare_item_for_response( $item, $request );
$this->assertNotWPError( $prepared );
$this->check_response( $prepared->get_data(), $item, true );
$request = new WP_REST_Request( 'GET', '/wp/v2/users/me/application-passwords/' . $uuid );
$request->set_param( 'context', 'view' );
$request->set_url_params(
array(
'user_id' => 'me',
'uuid' => $uuid,
)
);
$prepared = ( new WP_REST_Application_Passwords_Controller() )->prepare_item_for_response( $item, $request );
$this->assertNotWPError( $prepared );
$this->check_response( $prepared->get_data(), $item );
WP_Application_Passwords::record_application_password_usage( self::$admin, $uuid );
$item = WP_Application_Passwords::get_user_application_password( self::$admin, $uuid );
$item['uuid'] = $uuid;
$request = new WP_REST_Request( 'GET', '/wp/v2/users/me/application-passwords/' . $uuid );
$request->set_param( 'context', 'view' );
$request->set_url_params(
array(
'user_id' => 'me',
'uuid' => $uuid,
)
);
$prepared = ( new WP_REST_Application_Passwords_Controller() )->prepare_item_for_response( $item, $request );
$this->assertNotWPError( $prepared );
$this->check_response( $prepared->get_data(), $item );
}
/**
* Checks the password response matches the exepcted format.
*
* @since 5.6.0
*
* @param array $response The response data.
* @param array $item The created password item.
* @param bool $password If the password is expected.
*/
protected function check_response( $response, $item, $password = false ) {
$this->assertArrayHasKey( 'uuid', $response );
$this->assertArrayHasKey( 'name', $response );
$this->assertArrayHasKey( 'created', $response );
$this->assertArrayHasKey( 'last_used', $response );
$this->assertArrayHasKey( 'last_ip', $response );
$this->assertEquals( $item['uuid'], $response['uuid'] );
$this->assertEquals( $item['name'], $response['name'] );
$this->assertEquals( gmdate( 'Y-m-d\TH:i:s', $item['created'] ), $response['created'] );
if ( $item['last_used'] ) {
$this->assertEquals( gmdate( 'Y-m-d\TH:i:s', $item['last_used'] ), $response['last_used'] );
} else {
$this->assertNull( $response['last_used'] );
}
if ( $item['last_ip'] ) {
$this->assertEquals( $item['last_ip'], $response['last_ip'] );
} else {
$this->assertNull( $response['last_ip'] );
}
if ( $password ) {
$this->assertArrayHasKey( 'password', $response );
} else {
$this->assertArrayNotHasKey( 'password', $response );
}
}
/**
* @ticket 42790
*/
public function test_get_item_schema() {
$request = new WP_REST_Request( 'OPTIONS', '/wp/v2/users/me/application-passwords' );
$response = rest_get_server()->dispatch( $request );
$data = $response->get_data();
$properties = $data['schema']['properties'];
$this->assertCount( 6, $properties );
$this->assertArrayHasKey( 'uuid', $properties );
$this->assertArrayHasKey( 'name', $properties );
$this->assertArrayHasKey( 'password', $properties );
$this->assertArrayHasKey( 'created', $properties );
$this->assertArrayHasKey( 'last_used', $properties );
$this->assertArrayHasKey( 'last_ip', $properties );
}
}

View File

@ -118,6 +118,8 @@ class WP_Test_REST_Schema_Initialization extends WP_Test_REST_TestCase {
'/wp/v2/users',
'/wp/v2/users/(?P<id>[\\d]+)',
'/wp/v2/users/me',
'/wp/v2/users/(?P<user_id>(?:[\\d]+|me))/application-passwords',
'/wp/v2/users/(?P<user_id>(?:[\\d]+|me))/application-passwords/(?P<uuid>[\\w\\-]+)',
'/wp/v2/comments',
'/wp/v2/comments/(?P<id>[\\d]+)',
'/wp/v2/search',
@ -132,7 +134,7 @@ class WP_Test_REST_Schema_Initialization extends WP_Test_REST_TestCase {
'/wp/v2/block-directory/search',
);
$this->assertSame( $expected_routes, $routes );
$this->assertSameSets( $expected_routes, $routes );
}
private function is_builtin_route( $route ) {

View File

@ -3985,6 +3985,102 @@ mockedApiResponse.Schema = {
"self": "http://example.org/index.php?rest_route=/wp/v2/users/me"
}
},
"/wp/v2/users/(?P<user_id>(?:[\\d]+|me))/application-passwords": {
"namespace": "wp/v2",
"methods": [
"GET",
"POST",
"DELETE"
],
"endpoints": [
{
"methods": [
"GET"
],
"args": {
"context": {
"required": false,
"default": "view",
"enum": [
"view",
"embed",
"edit"
],
"description": "Scope under which the request is made; determines fields present in response.",
"type": "string"
}
}
},
{
"methods": [
"POST"
],
"args": {
"name": {
"required": true,
"description": "The name of the application password.",
"type": "string"
}
}
},
{
"methods": [
"DELETE"
],
"args": []
}
]
},
"/wp/v2/users/(?P<user_id>(?:[\\d]+|me))/application-passwords/(?P<uuid>[\\w\\-]+)": {
"namespace": "wp/v2",
"methods": [
"GET",
"POST",
"PUT",
"PATCH",
"DELETE"
],
"endpoints": [
{
"methods": [
"GET"
],
"args": {
"context": {
"required": false,
"default": "view",
"enum": [
"view",
"embed",
"edit"
],
"description": "Scope under which the request is made; determines fields present in response.",
"type": "string"
}
}
},
{
"methods": [
"POST",
"PUT",
"PATCH"
],
"args": {
"name": {
"required": false,
"description": "The name of the application password.",
"type": "string"
}
}
},
{
"methods": [
"DELETE"
],
"args": []
}
]
},
"/wp/v2/comments": {
"namespace": "wp/v2",
"methods": [