Support installing live branches from the manifest (#36072)

This commit is contained in:
Sam Seay 2022-12-21 13:58:10 +13:00 committed by GitHub
parent 400ace67a3
commit 4877e4b36e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 418 additions and 15 deletions

View File

@ -17,8 +17,8 @@ function register_woocommerce_admin_test_helper_rest_route( $route, $callback, $
'rest_api_init',
function() use ( $route, $callback, $additional_options ) {
$default_options = array(
'methods' => 'POST',
'callback' => $callback,
'methods' => 'POST',
'callback' => $callback,
'permission_callback' => function( $request ) {
if ( ! wc_rest_check_manager_permissions( 'settings', 'edit' ) ) {
return new \WP_Error(
@ -55,3 +55,4 @@ require 'features/features.php';
require 'rest-api-filters/rest-api-filters.php';
require 'rest-api-filters/hook.php';
require 'live-branches/manifest.php';
require 'live-branches/install.php';

View File

@ -0,0 +1,95 @@
<?php // @codingStandardsIgnoreLine I don't know why it thinks the doc comment is missing.
/**
* REST API endpoints for live branches installation.
*
* @package WC_Beta_Tester
*/
require_once __DIR__ . '/../../includes/class-wc-beta-tester-live-branches-installer.php';
register_woocommerce_admin_test_helper_rest_route(
'/live-branches/install/v1',
'install_version',
array(
'methods' => 'POST',
)
);
register_woocommerce_admin_test_helper_rest_route(
'/live-branches/deactivate/v1',
'deactivate_woocommerce',
array(
'methods' => 'GET',
)
);
register_woocommerce_admin_test_helper_rest_route(
'/live-branches/activate/v1',
'activate_version',
array(
'methods' => 'POST',
'permission_callback' => function( $request ) {
// Avoid using WC functions as core will be deactivated during this request.
$user = wp_get_current_user();
$allowed_roles = array( 'administrator' );
if ( array_intersect( $allowed_roles, $user->roles ) ) {
return true;
} else {
return new \WP_Error(
'woocommerce_rest_cannot_edit',
__( 'Sorry, you cannot perform this action', 'woocommerce' )
);
}
},
)
);
/**
* Respond to POST request to install a plugin by download url.
*
* @param Object $request - The request parameter.
*/
function install_version( $request ) {
$params = json_decode( $request->get_body() );
$download_url = $params->download_url;
$pr_name = $params->pr_name;
$version = $params->version;
$installer = new WC_Beta_Tester_Live_Branches_Installer();
$result = $installer->install( $download_url, $pr_name, $version );
if ( is_wp_error( $result ) ) {
return new WP_Error( 400, "Could not install $pr_name with error {$result->get_error_message()}", '' );
} else {
return new WP_REST_Response( wp_json_encode( array( 'ok' => true ) ), 200 );
}
}
/**
* Respond to POST request to activate a plugin by version.
*
* @param Object $request - The request parameter.
*/
function activate_version( $request ) {
$params = json_decode( $request->get_body() );
$version = $params->version;
$installer = new WC_Beta_Tester_Live_Branches_Installer();
$result = $installer->activate( $version );
if ( is_wp_error( $result ) ) {
return new WP_Error( 400, "Could not activate version: $version with error {$result->get_error_message()}", '' );
} else {
return new WP_REST_Response( wp_json_encode( array( 'ok' => true ) ), 200 );
}
}
/**
* Respond to GET request to deactivate WooCommerce.
*/
function deactivate_woocommerce() {
$installer = new WC_Beta_Tester_Live_Branches_Installer();
$installer->deactivate_woocommerce();
return new WP_REST_Response( wp_json_encode( array( 'ok' => true ) ), 200 );
}

View File

@ -1,10 +1,12 @@
<?php
<?php // @codingStandardsIgnoreLine
/**
* Register REST endpoint for fetching live branches manifest.
*
* @package WC_Beta_Tester
*/
require_once __DIR__ . '/../../includes/class-wc-beta-tester-live-branches-installer.php';
register_woocommerce_admin_test_helper_rest_route(
'/live-branches/manifest/v1',
'fetch_live_branches_manifest',
@ -17,8 +19,15 @@ register_woocommerce_admin_test_helper_rest_route(
* API endpoint to fetch the manifest of live branches.
*/
function fetch_live_branches_manifest() {
$response = wp_remote_get( 'https://betadownload.jetpack.me/woocommerce-branches.json' );
$body = wp_remote_retrieve_body( $response );
$response = wp_remote_get( 'https://betadownload.jetpack.me/woocommerce-branches.json' );
$body = wp_remote_retrieve_body( $response );
$installer = new WC_Beta_Tester_Live_Branches_Installer();
return new WP_REST_Response( json_decode( $body ), 200 );
$obj = json_decode( $body );
foreach ( $obj->pr as $key => $value ) {
$value->install_status = $installer->check_install_status( $value->version );
}
return new WP_REST_Response( $obj, 200 );
}

View File

@ -0,0 +1,4 @@
Significance: minor
Type: add
Add feature to install and activate live branches.

View File

@ -0,0 +1,159 @@
<?php
/**
* Beta Tester Plugin Live Branches feature class.
*
* @package WC_Beta_Tester
*/
defined( 'ABSPATH' ) || exit;
require_once ABSPATH . 'wp-admin/includes/plugin.php';
const LIVE_BRANCH_PLUGIN_PREFIX = 'wc_beta_tester_live_branch';
/**
* WC_Beta_Tester Live Branches Installer Class.
*/
class WC_Beta_Tester_Live_Branches_Installer {
/**
* Keep an instance of the WP Filesystem API.
*
* @var Object The WP_Filesystem API instance
*/
private $file_system;
/**
* Constructor.
*/
public function __construct() {
$this->file_system = $this->init_filesystem();
}
/**
* Initialize the WP_Filesystem API
*/
private function init_filesystem() {
require_once ABSPATH . 'wp-admin/includes/file.php';
$creds = request_filesystem_credentials( site_url() . '/wp-admin/', '', false, false, array() );
if ( ! WP_Filesystem( $creds ) ) {
return new WP_Error( 'fs_api_error', __( 'WooCommerce Beta Tester: No File System access', 'woocommerce-beta-tester' ) ); // @codingStandardsIgnoreLine.
}
global $wp_filesystem;
return $wp_filesystem;
}
/**
* Install a WooCommerce plugin version by download url.
*
* @param string $download_url The download url of the plugin version.
* @param string $pr_name The name of the associated PR.
* @param string $version The version of the plugin.
*/
public function install( $download_url, $pr_name, $version ) {
// Download the plugin.
$tmp_dir = download_url( $download_url );
if ( is_wp_error( $tmp_dir ) ) {
return new WP_Error(
'download_error',
sprintf( __( 'Error Downloading: <a href="%1$s">%1$s</a> - Error: %2$s', 'woocommerce-beta-tester' ), $download_url, $tmp_dir->get_error_message() ) // @codingStandardsIgnoreLine.
);
}
// Unzip the plugin.
$plugin_dir = str_replace( ABSPATH, $this->file_system->abspath(), WP_PLUGIN_DIR );
$plugin_path = $plugin_dir . '/' . LIVE_BRANCH_PLUGIN_PREFIX . "_$version";
$unzip_path = $plugin_dir . "/woocommerce-$version";
$unzip = unzip_file( $tmp_dir, $unzip_path );
// The plugin is nested under woocommerce-dev, so we need to move it up one level.
$this->file_system->mkdir( $plugin_path );
$this->move( $unzip_path . '/woocommerce-dev', $plugin_path );
if ( is_wp_error( $unzip ) ) {
return new WP_Error( 'unzip_error', sprintf( __( 'Error Unzipping file: Error: %1$s', 'woocommerce-beta-tester' ), $result->get_error_message() ) ); // @codingStandardsIgnoreLine.
}
// Delete the downloaded zip file.
unlink( $tmp_dir );
return true;
}
/**
* Move all files from one folder to another.
*
* @param string $from The folder to move files from.
* @param string $to The folder to move files to.
*/
private function move( $from, $to ) {
$files = scandir( $from );
$oldfolder = "$from/";
$newfolder = "$to/";
foreach ( $files as $fname ) {
if ( '.' !== $fname && '..' !== $fname ) {
$this->file_system->move( $oldfolder . $fname, $newfolder . $fname );
}
}
}
/**
* Deactivate all currently active WooCommerce plugins.
*/
public function deactivate_woocommerce() {
// First check is the regular woo plugin active.
if ( is_plugin_active( 'woocommerce/woocommerce.php' ) ) {
deactivate_plugins( 'woocommerce/woocommerce.php' );
}
// Check if any beta tester installed plugins are active.
$active_plugins = get_option( 'active_plugins' );
$active_woo_plugins = array_filter(
$active_plugins,
function( $plugin ) {
return str_contains( $plugin, LIVE_BRANCH_PLUGIN_PREFIX );
}
);
if ( ! empty( $active_woo_plugins ) ) {
deactivate_plugins( $active_woo_plugins );
}
}
/**
* Activate a beta tester installed WooCommerce plugin
*
* @param string $version The version of the plugin to activate.
*/
public function activate( $version ) {
if ( ! is_plugin_active( LIVE_BRANCH_PLUGIN_PREFIX . "_$version/woocommerce.php" ) ) {
activate_plugin( LIVE_BRANCH_PLUGIN_PREFIX . "_$version/woocommerce.php" );
}
}
/**
* Check the install status of a plugin version.
*
* @param string $version The version of the plugin to check.
*/
public function check_install_status( $version ) {
$plugin_path = WP_PLUGIN_DIR . '/' . LIVE_BRANCH_PLUGIN_PREFIX . "_$version/woocommerce.php";
if ( ! file_exists( $plugin_path ) ) {
return 'not-installed';
}
if ( is_plugin_active( LIVE_BRANCH_PLUGIN_PREFIX . "_$version/woocommerce.php" ) ) {
return 'active';
}
return 'installed';
}
}

View File

@ -7,14 +7,42 @@ import {
// @ts-ignore
__experimentalItem as Item,
Button,
Spinner,
} from '@wordpress/components';
import { useState } from 'react';
/**
* Internal dependencies
*/
import { Branch } from '../hooks/live-branches';
import { Branch, useLiveBranchInstall } from '../hooks/live-branches';
const BranchListItem = ( { branch }: { branch: Branch } ) => {
const { isError, isInProgress, installAndActivate, activate, status } =
useLiveBranchInstall(
branch.download_url,
`https://github.com/woocommerce/woocommerce/pull/${ branch.pr }`,
branch.version,
branch.install_status
);
const ActionButton = {
'not-installed': () => (
<Button variant="primary" onClick={ installAndActivate }>
Install and Activate
</Button>
),
installed: () => (
<Button variant="primary" onClick={ activate }>
Activate
</Button>
),
active: () => (
<Button variant="secondary" disabled>
Activated
</Button>
),
}[ status ];
return (
<Item>
<p>
@ -29,21 +57,27 @@ const BranchListItem = ( { branch }: { branch: Branch } ) => {
{ branch.branch }
</a>
</p>
<Button
variant="primary"
onClick={ () => console.log( 'Do install stuffs' ) }
>
Install
</Button>
{ isError && <p>Something Went Wrong!</p> }
{ isInProgress && <Spinner /> }
{ ! isError && ! isInProgress && <ActionButton /> }
</Item>
);
};
export const BranchList = ( { branches }: { branches: Branch[] } ) => {
const activeBranch = branches.find(
( branch ) => branch.install_status === 'active'
);
const nonActiveBranches = branches.filter(
( branch ) => branch.install_status !== 'active'
);
return (
<ItemGroup isSeparated>
{ /* @ts-ignore */ }
{ branches.map( ( branch ) => (
{ /* Sort the active branch if it exists to the top of the list */ }
{ activeBranch && <BranchListItem branch={ activeBranch } /> }
{ nonActiveBranches.map( ( branch ) => (
<BranchListItem key={ branch.commit } branch={ branch } />
) ) }
</ItemGroup>

View File

@ -4,6 +4,8 @@ import { useEffect, useState } from 'react';
// @ts-ignore
import { API_NAMESPACE } from '../../features/data/constants';
type PluginStatus = 'not-installed' | 'installed' | 'active';
export type Branch = {
branch: string;
commit: string;
@ -11,6 +13,7 @@ export type Branch = {
update_date: string;
version: string;
pr: number;
install_status: PluginStatus;
};
export const useLiveBranchesData = () => {
@ -39,3 +42,101 @@ export const useLiveBranchesData = () => {
return { branches, isLoading: loading };
};
export const useLiveBranchInstall = (
downloadUrl: string,
prName: string,
version: string,
status: PluginStatus
) => {
const [ isInProgress, setIsInProgress ] = useState( false );
const [ isError, setIsError ] = useState( false );
const [ pluginStatus, setPluginStatus ] = useState( status );
const activate = async () => {
setIsInProgress( true );
try {
const deactivateResult = await apiFetch< Response >( {
path: `${ API_NAMESPACE }/live-branches/deactivate/v1`,
} );
if ( deactivateResult.status >= 400 ) {
throw new Error( 'Could not deactivate' );
}
const activateResult = await apiFetch< Response >( {
path: `${ API_NAMESPACE }/live-branches/activate/v1`,
method: 'POST',
body: JSON.stringify( {
version,
} ),
} );
if ( activateResult.status >= 400 ) {
throw new Error( 'Could not activate' );
}
} catch ( e ) {
setIsError( true );
}
setPluginStatus( 'active' );
setIsInProgress( false );
};
const installAndActivate = async () => {
setIsInProgress( true );
try {
const installResult = await apiFetch< Response >( {
path: `${ API_NAMESPACE }/live-branches/install/v1`,
method: 'POST',
body: JSON.stringify( {
pr_name: prName,
download_url: downloadUrl,
version,
} ),
} );
if ( installResult.status >= 400 ) {
throw new Error( 'Could not install' );
}
setPluginStatus( 'installed' );
const deactivateResult = await apiFetch< Response >( {
path: `${ API_NAMESPACE }/live-branches/deactivate/v1`,
} );
if ( deactivateResult.status >= 400 ) {
throw new Error( 'Could not deactivate' );
}
const activateResult = await apiFetch< Response >( {
path: `${ API_NAMESPACE }/live-branches/activate/v1`,
method: 'POST',
body: JSON.stringify( {
version,
} ),
} );
if ( activateResult.status >= 400 ) {
throw new Error( 'Could not activate' );
}
setPluginStatus( 'active' );
} catch ( e ) {
setIsError( true );
}
setIsInProgress( false );
};
return {
installAndActivate,
activate,
isError,
isInProgress,
status: pluginStatus,
};
};