Onboarding: Add theme install and activation endpoints (https://github.com/woocommerce/woocommerce-admin/pull/3482)

* Add API endpoint to install themes

* Add API endpoint to activate a theme

* Add an array of allowed themes and sanitize slugs

* Limit allowed themes to already installed or free themes

* Add theme installation tests

* Add tests for theme activation

* Add onboarding theme installation and activation tests

* Remove unused argument in onboarding plugins API
This commit is contained in:
Joshua T Flowers 2019-12-30 15:47:15 +08:00 committed by GitHub
parent 44748bb16e
commit 3da148156e
6 changed files with 496 additions and 3 deletions

View File

@ -81,6 +81,7 @@ class Init {
'Automattic\WooCommerce\Admin\API\OnboardingProfile',
'Automattic\WooCommerce\Admin\API\OnboardingPlugins',
'Automattic\WooCommerce\Admin\API\OnboardingTasks',
'Automattic\WooCommerce\Admin\API\OnboardingThemes',
)
);
}

View File

@ -254,10 +254,9 @@ class OnboardingPlugins extends \WC_REST_Data_Controller {
/**
* Returns a list of active plugins.
*
* @param WP_REST_Request $request Full details about the request.
* @return array Active plugins
*/
public function active_plugins( $request ) {
public function active_plugins() {
$plugins = Onboarding::get_active_plugins();
return( array(
'plugins' => array_values( $plugins ),

View File

@ -0,0 +1,223 @@
<?php
/**
* REST API Onboarding Themes Controller
*
* Handles requests to install and activate themes.
*
* @package WooCommerce Admin/API
*/
namespace Automattic\WooCommerce\Admin\API;
use Automattic\WooCommerce\Admin\Features\Onboarding;
defined( 'ABSPATH' ) || exit;
/**
* Onboarding Themes Controller.
*
* @package WooCommerce Admin/API
* @extends WC_REST_Data_Controller
*/
class OnboardingThemes extends \WC_REST_Data_Controller {
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'wc-admin';
/**
* Route base.
*
* @var string
*/
protected $rest_base = 'onboarding/themes';
/**
* Register routes.
*/
public function register_routes() {
register_rest_route(
$this->namespace,
'/' . $this->rest_base . '/install',
array(
array(
'methods' => \WP_REST_Server::EDITABLE,
'callback' => array( $this, 'install_theme' ),
'permission_callback' => array( $this, 'update_item_permissions_check' ),
),
'schema' => array( $this, 'get_item_schema' ),
)
);
register_rest_route(
$this->namespace,
'/' . $this->rest_base . '/activate',
array(
array(
'methods' => \WP_REST_Server::EDITABLE,
'callback' => array( $this, 'activate_theme' ),
'permission_callback' => array( $this, 'update_item_permissions_check' ),
),
'schema' => array( $this, 'get_item_schema' ),
)
);
}
/**
* Check if a given request has access to manage themes.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|boolean
*/
public function update_item_permissions_check( $request ) {
if ( ! current_user_can( 'install_themes' ) ) {
return new \WP_Error( 'woocommerce_rest_cannot_update', __( 'Sorry, you cannot manage themes.', 'woocommerce-admin' ), array( 'status' => rest_authorization_required_code() ) );
}
return true;
}
/**
* Installs the requested theme.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|array Theme installation status.
*/
public function install_theme( $request ) {
$allowed_themes = Onboarding::get_allowed_themes();
$theme = sanitize_title_with_dashes( $request['theme'] );
if ( ! in_array( $theme, $allowed_themes, true ) ) {
return new \WP_Error( 'woocommerce_rest_invalid_theme', __( 'Invalid theme.', 'woocommerce-admin' ), 404 );
}
$slug = sanitize_key( $theme );
$installed_themes = wp_get_themes();
if ( in_array( $slug, array_keys( $installed_themes ), true ) ) {
return( array(
'slug' => $slug,
'name' => $installed_themes[ $slug ]->get( 'Name' ),
'status' => 'success',
) );
}
include_once ABSPATH . '/wp-admin/includes/admin.php';
include_once ABSPATH . '/wp-admin/includes/theme-install.php';
include_once ABSPATH . '/wp-admin/includes/theme.php';
include_once ABSPATH . '/wp-admin/includes/class-wp-upgrader.php';
include_once ABSPATH . '/wp-admin/includes/class-theme-upgrader.php';
$api = themes_api(
'theme_information',
array(
'slug' => $slug,
'fields' => array(
'sections' => false,
),
)
);
if ( is_wp_error( $api ) ) {
return new \WP_Error(
'woocommerce_rest_theme_install',
sprintf(
/* translators: %s: theme slug (example: woocommerce-services) */
__( 'The requested theme `%s` could not be installed. Theme API call failed.', 'woocommerce-admin' ),
$slug
),
500
);
}
$upgrader = new \Theme_Upgrader( new \Automatic_Upgrader_Skin() );
$result = $upgrader->install( $api->download_link );
if ( is_wp_error( $result ) || is_null( $result ) ) {
return new \WP_Error(
'woocommerce_rest_theme_install',
sprintf(
/* translators: %s: theme slug (example: woocommerce-services) */
__( 'The requested theme `%s` could not be installed.', 'woocommerce-admin' ),
$slug
),
500
);
}
return array(
'slug' => $slug,
'name' => $api->name,
'status' => 'success',
);
}
/**
* Activate the requested theme.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|array Theme activation status.
*/
public function activate_theme( $request ) {
$allowed_themes = Onboarding::get_allowed_themes();
$theme = sanitize_title_with_dashes( $request['theme'] );
if ( ! in_array( $theme, $allowed_themes, true ) ) {
return new \WP_Error( 'woocommerce_rest_invalid_theme', __( 'Invalid theme.', 'woocommerce-admin' ), 404 );
}
require_once ABSPATH . 'wp-admin/includes/theme.php';
$installed_themes = wp_get_themes();
if ( ! in_array( $theme, array_keys( $installed_themes ), true ) ) {
/* translators: %s: theme slug (example: woocommerce-services) */
return new \WP_Error( 'woocommerce_rest_invalid_theme', sprintf( __( 'Invalid theme %s.', 'woocommerce-admin' ), $slug ), 404 );
}
$result = switch_theme( $theme );
if ( ! is_null( $result ) ) {
return new \WP_Error( 'woocommerce_rest_invalid_theme', sprintf( __( 'The requested theme could not be activated.', 'woocommerce-admin' ), $slug ), 500 );
}
return( array(
'slug' => $theme,
'status' => 'success',
) );
}
/**
* Get the schema, conforming to JSON Schema.
*
* @return array
*/
public function get_item_schema() {
$schema = array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => 'onboarding_theme',
'type' => 'object',
'properties' => array(
'slug' => array(
'description' => __( 'Theme slug.', 'woocommerce-admin' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'name' => array(
'description' => __( 'Theme name.', 'woocommerce-admin' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'status' => array(
'description' => __( 'Theme status.', 'woocommerce-admin' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
),
);
return $this->add_additional_fields_schema( $schema );
}
}

View File

@ -196,10 +196,11 @@ class Onboarding {
);
foreach ( $theme_data->products as $theme ) {
$slug = sanitize_title( $theme->slug );
$slug = sanitize_title_with_dashes( $theme->slug );
$themes[ $slug ] = (array) $theme;
$themes[ $slug ]['is_installed'] = false;
$themes[ $slug ]['has_woocommerce_support'] = true;
$themes[ $slug ]['slug'] = $slug;
}
}
@ -497,6 +498,7 @@ class Onboarding {
)
);
}
/**
* Get a list of active plugins, relevent to the onboarding wizard.
*
@ -515,6 +517,26 @@ class Onboarding {
return $active_plugins;
}
/**
* Gets an array of themes that can be installed & activated via the onboarding wizard.
*
* @return array
*/
public static function get_allowed_themes() {
$allowed_themes = array();
$themes = self::get_themes();
foreach ( $themes as $theme ) {
$price = preg_replace( '/&#?[a-z0-9]+;/i', '', $theme['price'] );
if ( $theme['is_installed'] || '0.00' === $price ) {
$allowed_themes[] = $theme['slug'];
}
}
return apply_filters( 'woocommerce_admin_onboarding_themes_whitelist', $allowed_themes );
}
/**
* Let the app know that we will be showing the onboarding route, so wp-admin elements should be hidden while loading.
*

View File

@ -0,0 +1,123 @@
<?php
/**
* Onboarding Plugins REST API Test
*
* @package WooCommerce Admin\Tests\API
*/
use \Automattic\WooCommerce\Admin\API\OnboardingPlugins;
use \Automattic\WooCommerce\Admin\Features\Onboarding;
/**
* WC Tests API Onboarding Plugins
*/
class WC_Tests_API_Onboarding_Plugins extends WC_REST_Unit_Test_Case {
/**
* Endpoints.
*
* @var string
*/
protected $endpoint = '/wc-admin/onboarding/plugins';
/**
* Setup test data. Called before every test.
*/
public function setUp() {
parent::setUp();
$this->user = $this->factory->user->create(
array(
'role' => 'administrator',
)
);
}
/**
* Test that installation without permission is unauthorized.
*/
public function test_install_without_permission() {
$response = $this->server->dispatch( new WP_REST_Request( 'POST', $this->endpoint . '/install' ) );
$this->assertEquals( 401, $response->get_status() );
}
/**
* Test that installing a valid plugin works.
*/
public function test_install_plugin() {
wp_set_current_user( $this->user );
$request = new WP_REST_Request( 'POST', $this->endpoint . '/install' );
$request->set_query_params(
array(
'plugin' => 'facebook-for-woocommerce',
)
);
$response = $this->server->dispatch( $request );
$data = $response->get_data();
$plugins = get_plugins();
$this->assertEquals( 200, $response->get_status() );
$this->assertEquals( 'facebook-for-woocommerce', $data['slug'] );
$this->assertEquals( 'success', $data['status'] );
$this->assertArrayHasKey( 'facebook-for-woocommerce/facebook-for-woocommerce.php', $plugins );
}
/**
* Test that installing an invalid plugin fails.
*/
public function test_install_invalid_plugin() {
wp_set_current_user( $this->user );
$request = new WP_REST_Request( 'POST', $this->endpoint . '/install' );
$request->set_query_params(
array(
'plugin' => 'invalid-plugin-name',
)
);
$response = $this->server->dispatch( $request );
$data = $response->get_data();
$this->assertEquals( 'woocommerce_rest_invalid_plugin', $data['code'] );
}
/**
* Test that activating a valid plugin works.
*/
public function test_activate_plugin() {
wp_set_current_user( $this->user );
$request = new WP_REST_Request( 'POST', $this->endpoint . '/activate' );
$request->set_query_params(
array(
'plugins' => 'facebook-for-woocommerce',
)
);
$response = $this->server->dispatch( $request );
$data = $response->get_data();
$active_plugins = Onboarding::get_active_plugins();
$this->assertEquals( 200, $response->get_status() );
$this->assertContains( 'facebook-for-woocommerce', $data['activatedPlugins'] );
$this->assertEquals( 'success', $data['status'] );
$this->assertContains( 'facebook-for-woocommerce', $active_plugins );
}
/**
* Test that activating an invalid plugin fails.
*/
public function test_activate_invalid_plugin() {
wp_set_current_user( $this->user );
$request = new WP_REST_Request( 'POST', $this->endpoint . '/activate' );
$request->set_query_params(
array(
'plugins' => 'invalid-plugin-name',
)
);
$response = $this->server->dispatch( $request );
$data = $response->get_data();
$this->assertEquals( 'woocommerce_rest_invalid_plugins', $data['code'] );
}
}

View File

@ -0,0 +1,125 @@
<?php
/**
* Onboarding Themes REST API Test
*
* @package WooCommerce Admin\Tests\API
*/
use \Automattic\WooCommerce\Admin\API\OnboardingThemes;
use \Automattic\WooCommerce\Admin\Features\Onboarding;
/**
* WC Tests API Onboarding Themes
*/
class WC_Tests_API_Onboarding_Themes extends WC_REST_Unit_Test_Case {
/**
* Endpoints.
*
* @var string
*/
protected $endpoint = '/wc-admin/onboarding/themes';
/**
* Setup test data. Called before every test.
*/
public function setUp() {
parent::setUp();
$this->user = $this->factory->user->create(
array(
'role' => 'administrator',
)
);
}
/**
* Test that installation without permission is unauthorized.
*/
public function test_install_without_permission() {
$response = $this->server->dispatch( new WP_REST_Request( 'POST', $this->endpoint . '/install' ) );
$this->assertEquals( 401, $response->get_status() );
}
/**
* Test that installing a valid theme works.
*/
public function test_install_theme() {
wp_set_current_user( $this->user );
$request = new WP_REST_Request( 'POST', $this->endpoint . '/install' );
$request->set_query_params(
array(
'theme' => 'storefront',
)
);
$response = $this->server->dispatch( $request );
$data = $response->get_data();
$themes = wp_get_themes();
$this->assertEquals( 200, $response->get_status() );
$this->assertEquals( 'storefront', $data['slug'] );
$this->assertEquals( 'success', $data['status'] );
$this->assertArrayHasKey( 'storefront', $themes );
}
/**
* Test that installing an invalid theme fails.
*/
public function test_install_invalid_theme() {
wp_set_current_user( $this->user );
$request = new WP_REST_Request( 'POST', $this->endpoint . '/install' );
$request->set_query_params(
array(
'theme' => 'invalid-theme-name',
)
);
$response = $this->server->dispatch( $request );
$data = $response->get_data();
$this->assertEquals( 'woocommerce_rest_invalid_theme', $data['code'] );
}
/**
* Test that activating a valid theme works.
*/
public function test_activate_theme() {
wp_set_current_user( $this->user );
$previous_theme = get_stylesheet();
$request = new WP_REST_Request( 'POST', $this->endpoint . '/activate' );
$request->set_query_params(
array(
'theme' => 'storefront',
)
);
$response = $this->server->dispatch( $request );
$data = $response->get_data();
$active_theme = get_stylesheet();
$this->assertEquals( 200, $response->get_status() );
$this->assertEquals( 'storefront', $data['slug'] );
$this->assertEquals( 'success', $data['status'] );
$this->assertNotEquals( 'storefront', $previous_theme );
$this->assertEquals( 'storefront', $active_theme );
}
/**
* Test that activating an invalid theme fails.
*/
public function test_activate_invalid_theme() {
wp_set_current_user( $this->user );
$request = new WP_REST_Request( 'POST', $this->endpoint . '/activate' );
$request->set_query_params(
array(
'theme' => 'invalid-theme-name',
)
);
$response = $this->server->dispatch( $request );
$data = $response->get_data();
$this->assertEquals( 'woocommerce_rest_invalid_theme', $data['code'] );
}
}