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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
<?php
/**
* Jetpack Application Password Extras
*
* Extends WordPress Application Passwords to work with additional abilities
* beyond the REST API.
*
* @package jetpack
*/

if ( ! defined( 'ABSPATH' ) ) {
exit( 0 );
}

/**
* Extends Application Password functionality beyond the REST API.
*/
class Jetpack_Application_Password_Extras {

/**
* Initialize the main hooks.
*/
public static function init() {
add_filter( 'application_password_is_api_request', array( __CLASS__, 'application_password_extras' ) );
}

/**
* Allow Application Password access to additional abilities.
*
* NOTE: If expanding this to include more abilities, consider updating the
* `get_abilities` method to include new abilities.
*
* @param bool $original_value The original value of the filter.
* @return bool The new value of the filter.
*/
public static function application_password_extras( $original_value ) {
if ( $original_value ) {
return true;
}

// Allow Application Password access to admin-ajax.php
if ( is_admin() && wp_doing_ajax() ) {
return true;
}

// Allow access to post/page previews
$is_preview_request = isset( $_GET['preview'] ) && 'true' === $_GET['preview']; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
$has_post_id = isset( $_GET['p'] ) || isset( $_GET['page_id'] ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
if ( $is_preview_request && $has_post_id ) {
return true;
}

return $original_value;
}

/**
* Get the abilities that this extension provides.
*
* @return array Array of abilities with their status.
*/
public static function get_abilities() {
return array(
'admin-ajax' => true,
'post-previews' => true,
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
<?php
/**
* REST API endpoint for application password extras abilities.
*
* @package automattic/jetpack
*/

if ( ! defined( 'ABSPATH' ) ) {
exit( 0 );
}

/**
* Class WPCOM_REST_API_V2_Endpoint_Application_Password_Extras
*/
class WPCOM_REST_API_V2_Endpoint_Application_Password_Extras extends WP_REST_Controller {
/**
* Constructor.
*/
public function __construct() {
$this->namespace = 'wpcom/v2';
$this->rest_base = 'application-password-extras';
add_action( 'rest_api_init', array( $this, 'register_routes' ) );
}

/**
* Register routes.
*/
public function register_routes() {
register_rest_route(
$this->namespace,
$this->rest_base . '/abilities',
array(
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_abilities' ),
'permission_callback' => array( $this, 'get_item_permissions_check' ),
),
)
);

register_rest_route(
$this->namespace,
$this->rest_base . '/admin-ajax',
array(
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_abilities' ),
'permission_callback' => array( $this, 'get_item_permissions_check' ),
),
)
);

register_rest_route(
$this->namespace,
$this->rest_base . '/post-previews',
array(
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_abilities' ),
'permission_callback' => array( $this, 'get_item_permissions_check' ),
),
)
);
}

/**
* Checks if a given request has access to application password extras.
*
* @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_item_permissions_check( $request ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter, VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
if ( ! is_user_logged_in() ) {
return new WP_Error(
'rest_forbidden',
__( 'Sorry, you must be logged in to access this endpoint.', 'jetpack' ),
array( 'status' => rest_authorization_required_code() )
);
}

return true;
}

/**
* Retrieves the application password extras abilities.
*
* @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_abilities( $request ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter, VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
if ( ! class_exists( 'Jetpack_Application_Password_Extras' ) ) {
return new WP_Error(
'rest_application_password_extras_unavailable',
__( 'Application password extras functionality is not available.', 'jetpack' ),
array( 'status' => 503 )
);
}

return rest_ensure_response( Jetpack_Application_Password_Extras::get_abilities() );
}
}

wpcom_rest_api_v2_load_plugin( 'WPCOM_REST_API_V2_Endpoint_Application_Password_Extras' );
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: minor
Type: other

Application passwords: allow authenticating `admin-ajax` and post preview requests via application passwords.
3 changes: 3 additions & 0 deletions projects/plugins/jetpack/load-jetpack.php
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@ function jetpack_should_use_minified_assets() {
require_once JETPACK__PLUGIN_DIR . 'class-jetpack-connection-status.php';
Jetpack_Connection_Status::init();

require_once JETPACK__PLUGIN_DIR . '_inc/lib/class-jetpack-application-password-extras.php';
Jetpack_Application_Password_Extras::init();

require_once JETPACK__PLUGIN_DIR . '_inc/lib/class-jetpack-recommendations.php';

if ( is_admin() ) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
<?php
/**
* Tests for Jetpack_Application_Password_Extras class
*
* @package automattic/jetpack
*/

use PHPUnit\Framework\Attributes\CoversClass;

require_once JETPACK__PLUGIN_DIR . '/tests/php/lib/Jetpack_REST_TestCase.php';
require_once JETPACK__PLUGIN_DIR . '/_inc/lib/class-jetpack-application-password-extras.php';

/**
* Class Jetpack_Application_Password_Extras_Test
*
* @covers \Jetpack_Application_Password_Extras
*/
#[CoversClass( Jetpack_Application_Password_Extras::class )]
class Jetpack_Application_Password_Extras_Test extends Jetpack_REST_TestCase {

/**
* Mock user ID.
*
* @var int
*/
private static $user_id = 0;

/**
* Create shared database fixtures.
*
* @param WP_UnitTest_Factory $factory Fixture factory.
*/
public static function wpSetUpBeforeClass( $factory ) {
static::$user_id = $factory->user->create( array( 'role' => 'administrator' ) );
}

/**
* Setup the environment for a test.
*/
public function set_up() {
parent::set_up();
wp_set_current_user( static::$user_id );
}

/**
* Tear down the environment after a test.
*/
public function tear_down() {
parent::tear_down();
unset( $_GET['p'] );
unset( $_GET['page_id'] );
unset( $GLOBALS['wp_current_filter'] );
}

/**
* Test that init method registers the hook correctly.
*/
public function test_init_registers_hook() {
remove_all_filters( 'application_password_is_api_request' );
Jetpack_Application_Password_Extras::init();

$this->assertNotFalse(
has_filter( 'application_password_is_api_request', array( 'Jetpack_Application_Password_Extras', 'application_password_extras' ) ),
'Hook should be registered'
);
}

/**
* Test that non-matching requests preserve original false value.
*/
public function test_non_matching_request_preserves_original_false_value() {
set_current_screen( 'dashboard' );

$result = Jetpack_Application_Password_Extras::application_password_extras( false );
$this->assertFalse( $result, 'Should preserve false when not in matching context' );
}

/**
* Test that non-matching requests preserve original true value.
*/
public function test_non_matching_request_preserves_original_true_value() {
set_current_screen( 'dashboard' );

$result = Jetpack_Application_Password_Extras::application_password_extras( true );
$this->assertTrue( $result, 'Should preserve true when not in matching context' );
}

/**
* Test that admin-ajax requests are allowed.
*/
public function test_admin_ajax_request_allowed() {
set_current_screen( 'dashboard' );
add_filter( 'wp_doing_ajax', '__return_true' );

$result = Jetpack_Application_Password_Extras::application_password_extras( false );

remove_filter( 'wp_doing_ajax', '__return_true' );

$this->assertTrue( $result, 'Result should be true' );
}

/**
* Test that post preview requests are allowed.
*/
public function test_post_preview_request_allowed() {
$_GET['p'] = '123';

$result = Jetpack_Application_Password_Extras::application_password_extras( false );

$this->assertTrue( $result, 'Post preview requests should be allowed' );
}

/**
* Test that page preview requests are allowed.
*/
public function test_page_preview_request_allowed() {
$_GET['page_id'] = '456';

$result = Jetpack_Application_Password_Extras::application_password_extras( false );

$this->assertTrue( $result, 'Page preview requests should be allowed' );
}

/**
* Test multiple conditions at once.
*/
public function test_multiple_conditions_prioritize_first_match() {
set_current_screen( 'dashboard' );
add_filter( 'wp_doing_ajax', '__return_true' );
$_GET['p'] = '123';

$result = Jetpack_Application_Password_Extras::application_password_extras( false );

remove_filter( 'wp_doing_ajax', '__return_true' );

$this->assertTrue( $result, 'Should return true when any condition matches' );
}

/**
* Test that get_abilities returns all expected abilities.
*/
public function test_get_abilities_complete() {
$abilities = Jetpack_Application_Password_Extras::get_abilities();

$expected_abilities = array(
'admin-ajax' => true,
'post-previews' => true,
);

$this->assertEquals( $expected_abilities, $abilities, 'get_abilities should return all expected abilities' );
}
}
Loading
Loading