Skip to content

Commit

Permalink
Allow POSTing .zip file to plugins REST
Browse files Browse the repository at this point in the history
  • Loading branch information
BrianHenryIE committed Nov 18, 2022
1 parent 1ccf13f commit 0620867
Show file tree
Hide file tree
Showing 3 changed files with 234 additions and 3 deletions.
Expand Up @@ -1273,7 +1273,7 @@ protected function get_media_types() {
* @param array $file $_FILES array for a given file.
* @return true|WP_Error True if can upload, error for errors.
*/
protected function check_upload_size( $file ) {
public static function check_upload_size( $file ) {
if ( ! is_multisite() ) {
return true;
}
Expand Down
Expand Up @@ -330,6 +330,22 @@ static function ( $item ) {
$plugin_url = $request['url'];
}

if ( ! isset( $plugin_url ) ) {
// Get the file via $_FILES or raw data.
$files = $request->get_file_params();
$headers = $request->get_headers();

if ( ! empty( $files ) ) {
$plugin_url = $this->upload_from_file( $files, $headers );
} else {
$plugin_url = $this->upload_from_data( $request->get_body(), $headers );
}

if ( is_wp_error( $plugin_url ) ) {
return $plugin_url;
}
}

$skin = new WP_Ajax_Upgrader_Skin();
$upgrader = new Plugin_Upgrader( $skin );

Expand Down Expand Up @@ -798,15 +814,26 @@ public function validate_plugin_param( $file ) {
}

/**
* When installing a plugin via a POST create request, validate that one of either "url" or "slug" are present.
* If both are present, the plugin URL must be a wordpress.org URL.
* When installing a plugin via a POST create request, validate one of:
* * the request $_FILES is populated
* * the content type header indicated a zip file
* * one of either "url" or "slug" are present
* If both "url" and "zip" are present, the plugin URL must be a wordpress.org URL.
*
* @param WP_REST_Request $request
*
* @return bool
*/
public function validate_create_plugin_params( $request ) {

if ( ! empty( $request->get_file_params() ) ) {
return true;
}

if ( is_array( $request->get_content_type() ) && in_array( 'application/zip', $request->get_content_type(), true ) ) {
return true;
}

$xor_param_required = array( 'slug', 'url' );
$request_params = $request->get_params();
$required_params_present = array_intersect( $xor_param_required, array_keys( $request_params ) );
Expand Down Expand Up @@ -1041,4 +1068,155 @@ public function get_collection_params() {

return $query_params;
}

/**
* Handles an upload via multipart/form-data ($_FILES).
*
* Based on attachments controller.
* @see WP_REST_Attachments_Controller::upload_from_file()
*
* @uses WP_REST_Attachments_Controller::check_upload_size()
*
* @since 6.2.0
*
* @param array $files Data from the `$_FILES` superglobal.
* @param array $headers HTTP headers from the request.
* @return string|WP_Error Filepath of the uploaded plugin.
*/
protected function upload_from_file( $files, $headers ) {
if ( empty( $files ) ) {
return new WP_Error(
'rest_upload_no_data',
__( 'No data supplied.' ),
array( 'status' => 400 )
);
}

// Verify hash, if given.
if ( ! empty( $headers['content_md5'] ) ) {
$content_md5 = array_shift( $headers['content_md5'] );
$expected = trim( $content_md5 );
$actual = md5_file( $files['file']['tmp_name'] );

if ( $expected !== $actual ) {
return new WP_Error(
'rest_upload_hash_mismatch',
__( 'Content hash did not match expected.' ),
array( 'status' => 412 )
);
}
}

$size_check = WP_REST_Attachments_Controller::check_upload_size( $files['file'] );
if ( is_wp_error( $size_check ) ) {
return $size_check;
}

return $files['file']['tmp_name'];
}

/**
* Handles an upload via raw POST data.
*
* Based on attachments controller.
* @see WP_REST_Attachments_Controller::upload_from_data()
*
* @uses WP_REST_Attachments_Controller::get_filename_from_disposition()
* @uses WP_REST_Attachments_Controller::check_upload_size()
*
* @since 6.2.0
*
* @param string $data Supplied file data.
* @param array $headers HTTP headers from the request.
* @return string|WP_Error Filepath of the uploaded plugin.
*/
protected function upload_from_data( $data, $headers ) {
if ( empty( $data ) ) {
return new WP_Error(
'rest_upload_no_data',
__( 'No data supplied.' ),
array( 'status' => 400 )
);
}

if ( empty( $headers['content_type'] ) ) {
return new WP_Error(
'rest_upload_no_content_type',
__( 'No Content-Type supplied.' ),
array( 'status' => 400 )
);
}

if ( empty( $headers['content_disposition'] ) ) {
return new WP_Error(
'rest_upload_no_content_disposition',
__( 'No Content-Disposition supplied.' ),
array( 'status' => 400 )
);
}

$filename = WP_REST_Attachments_Controller::get_filename_from_disposition( $headers['content_disposition'] );

if ( empty( $filename ) ) {
return new WP_Error(
'rest_upload_invalid_disposition',
__( 'Invalid Content-Disposition supplied. Content-Disposition needs to be formatted as `attachment; filename="image.png"` or similar.' ),
array( 'status' => 400 )
);
}

if ( ! empty( $headers['content_md5'] ) ) {
$content_md5 = array_shift( $headers['content_md5'] );
$expected = trim( $content_md5 );
$actual = md5( $data );

if ( $expected !== $actual ) {
return new WP_Error(
'rest_upload_hash_mismatch',
__( 'Content hash did not match expected.' ),
array( 'status' => 412 )
);
}
}

// Get the content-type.
$type = array_shift( $headers['content_type'] );

// Include filesystem functions to get access to wp_tempnam().
require_once ABSPATH . 'wp-admin/includes/file.php';

// Save the file.
$tmpfname = wp_tempnam( $filename );

$fp = fopen( $tmpfname, 'w+' );

if ( ! $fp ) {
return new WP_Error(
'rest_upload_file_error',
__( 'Could not open file handle.' ),
array( 'status' => 500 )
);
}

fwrite( $fp, $data );
fclose( $fp );

/**
* Use $_FILES format for checking upload size limits.
*/
$file_data = array(
'error' => null,
'tmp_name' => $tmpfname,
'name' => $filename,
'type' => $type,
);

$size_check = WP_REST_Attachments_Controller::check_upload_size( $file_data );
if ( is_wp_error( $size_check ) ) {
return $size_check;
}

return $tmpfname;
}

}
53 changes: 53 additions & 0 deletions tests/phpunit/tests/rest-api/rest-plugins-controller.php
Expand Up @@ -436,6 +436,59 @@ public function test_create_item_from_url_and_activate() {
$this->assertTrue( is_plugin_active( 'link-manager/link-manager.php' ) );
}

/**
* @ticket 56221
*/
public function test_create_item_from_http_post_body_and_activate() {
wp_set_current_user( self::$super_admin );
$this->setup_plugin_download();

$test_file_path = DIR_TESTDATA . '/link-manager.zip';

$request = new WP_REST_Request( 'POST', self::BASE );
$request->set_header( 'Content-Type', 'application/zip' );
$request->set_header( 'Content-Disposition', 'attachment; filename=' . basename( $test_file_path ) );
$request->set_body( file_get_contents( $test_file_path ) );
$request->set_param( 'status', 'active' );

$response = rest_do_request( $request );
$this->assertNotWPError( $response->as_error() );
$this->assertSame( 201, $response->get_status() );
$this->assertSame( 'Link Manager', $response->get_data()['name'] );
$this->assertTrue( is_plugin_active( 'link-manager/link-manager.php' ) );
}

/**
* @ticket 56221
*/
public function test_create_item_from_http_post_files_and_activate() {
wp_set_current_user( self::$super_admin );
$this->setup_plugin_download();

$test_file_path = DIR_TESTDATA . '/link-manager.zip';

$request = new WP_REST_Request( 'POST', self::BASE );
$request->set_file_params(
array(
'file' => array(
'file' => file_get_contents( $test_file_path ),
'name' => basename( $test_file_path ),
'size' => filesize( $test_file_path ),
'tmp_name' => $test_file_path,
),
)
);
$request->set_header( 'Content-MD5', md5_file( $test_file_path ) );
$request->set_param( 'status', 'active' );

$response = rest_do_request( $request );
$this->assertNotWPError( $response->as_error() );
$this->assertSame( 201, $response->get_status() );
$this->assertSame( 'Link Manager', $response->get_data()['name'] );
$this->assertTrue( is_plugin_active( 'link-manager/link-manager.php' ) );
}


/**
* @ticket 56221
*/
Expand Down

0 comments on commit 0620867

Please sign in to comment.