plugin updates

This commit is contained in:
Tony Volpe
2024-09-17 10:43:54 -04:00
parent 44b413346f
commit b7c8882c8c
1359 changed files with 58219 additions and 11364 deletions

View File

@@ -0,0 +1,11 @@
<?php
/**
* Plugin Name: Blueprint
* Plugin URI: https://woocommerce.com/
* Description: An empty blueprint definition file to setup wp-env test env.
* Version: 0.0.1
* Author: Automattic
* Author URI: https://woocommerce.com
* Requires at least: 6.4
* Requires PHP: 7.4
*/

View File

@@ -0,0 +1,23 @@
<?php
namespace Automattic\WooCommerce\Blueprint;
use Automattic\WooCommerce\Blueprint\Exporters\ExportInstallPluginSteps;
use Automattic\WooCommerce\Blueprint\Exporters\ExportInstallThemeSteps;
/**
* Built-in exporters.
*/
class BuiltInExporters {
/**
* Get all built-in exporters.
*
* @return array List of all built-in exporters.
*/
public function get_all() {
return array(
new ExportInstallPluginSteps(),
new ExportInstallThemeSteps(),
);
}
}

View File

@@ -0,0 +1,88 @@
<?php
namespace Automattic\WooCommerce\Blueprint;
use Automattic\WooCommerce\Blueprint\Importers\ImportActivatePlugin;
use Automattic\WooCommerce\Blueprint\Importers\ImportActivateTheme;
use Automattic\WooCommerce\Blueprint\Importers\ImportDeactivatePlugin;
use Automattic\WooCommerce\Blueprint\Importers\ImportDeletePlugin;
use Automattic\WooCommerce\Blueprint\Importers\ImportInstallPlugin;
use Automattic\WooCommerce\Blueprint\Importers\ImportInstallTheme;
use Automattic\WooCommerce\Blueprint\Importers\ImportSetSiteOptions;
use Automattic\WooCommerce\Blueprint\ResourceStorages\LocalPluginResourceStorage;
use Automattic\WooCommerce\Blueprint\ResourceStorages\LocalThemeResourceStorage;
use Automattic\WooCommerce\Blueprint\ResourceStorages\OrgPluginResourceStorage;
use Automattic\WooCommerce\Blueprint\ResourceStorages\OrgThemeResourceStorage;
use Automattic\WooCommerce\Blueprint\Schemas\JsonSchema;
use Automattic\WooCommerce\Blueprint\Schemas\ZipSchema;
/**
* Class BuiltInStepProcessors
*
* @package Automattic\WooCommerce\Blueprint
*/
class BuiltInStepProcessors {
/**
* The schema used for validation and processing.
*
* @var JsonSchema The schema used for validation and processing.
*/
private JsonSchema $schema;
/**
* BuiltInStepProcessors constructor.
*
* @param JsonSchema $schema The schema used for validation and processing.
*/
public function __construct( JsonSchema $schema ) {
$this->schema = $schema;
}
/**
* Returns an array of all step processors.
*
* @return array The array of step processors.
*/
public function get_all() {
return array(
$this->create_install_plugins_processor(),
$this->create_install_themes_processor(),
new ImportSetSiteOptions(),
new ImportDeletePlugin(),
new ImportActivatePlugin(),
new ImportActivateTheme(),
new ImportDeactivatePlugin(),
);
}
/**
* Creates the processor for installing plugins.
*
* @return ImportInstallPlugin The processor for installing plugins.
*/
private function create_install_plugins_processor() {
$storages = new ResourceStorages();
$storages->add_storage( new OrgPluginResourceStorage() );
if ( $this->schema instanceof ZipSchema ) {
$storages->add_storage( new LocalPluginResourceStorage( $this->schema->get_unzipped_path() ) );
}
return new ImportInstallPlugin( $storages );
}
/**
* Creates the processor for installing themes.
*
* @return ImportInstallTheme The processor for installing themes.
*/
private function create_install_themes_processor() {
$storage = new ResourceStorages();
$storage->add_storage( new OrgThemeResourceStorage() );
if ( $this->schema instanceof ZipSchema ) {
$storage->add_storage( new LocalThemeResourceStorage( $this->schema->get_unzipped_path() ) );
}
return new ImportInstallTheme( $storage );
}
}

View File

@@ -0,0 +1,95 @@
<?php
namespace Automattic\WooCommerce\Blueprint;
use Automattic\WooCommerce\Blueprint\Cli\ExportCli;
use Automattic\WooCommerce\Blueprint\Cli\ImportCli;
$autoload_path = __DIR__ . '/../vendor/autoload.php';
if ( file_exists( $autoload_path ) ) {
require_once $autoload_path;
}
/**
* Class Cli.
*
* This class is included and execute from WC_CLI(class-wc-cli.php) to register
* WP CLI commands.
*/
class Cli {
/**
* Register WP CLI commands.
*
* @return void
*/
public static function register_commands() {
\WP_CLI::add_command(
'wc blueprint import',
function ( $args, $assoc_args ) {
$import = new ImportCli( $args[0] );
$import->run( $assoc_args );
},
array(
'synopsis' => array(
array(
'type' => 'positional',
'name' => 'schema-path',
'optional' => false,
),
array(
'type' => 'assoc',
'name' => 'show-messages',
'optional' => true,
'options' => array( 'all', 'error', 'info', 'debug' ),
),
),
'when' => 'after_wp_load',
)
);
\WP_CLI::add_command(
'wc blueprint export',
function ( $args, $assoc_args ) {
$export = new ExportCli( $args[0] );
$steps = array();
$format = $assoc_args['format'] ?? 'json';
if ( isset( $assoc_args['steps'] ) ) {
$steps = array_map(
function ( $step ) {
return trim( $step );
},
explode( ',', $assoc_args['steps'] )
);
}
$export->run(
array(
'steps' => $steps,
'format' => $format,
)
);
},
array(
'synopsis' => array(
array(
'type' => 'positional',
'name' => 'save-to',
'optional' => false,
),
array(
'type' => 'assoc',
'name' => 'steps',
'optional' => true,
),
array(
'type' => 'assoc',
'name' => 'format',
'optional' => true,
'default' => 'json',
'options' => array( 'json', 'zip' ),
),
),
'when' => 'after_wp_load',
)
);
}
}

View File

@@ -0,0 +1,61 @@
<?php
namespace Automattic\WooCommerce\Blueprint\Cli;
use Automattic\WooCommerce\Blueprint\ExportSchema;
use Automattic\WooCommerce\Blueprint\ZipExportedSchema;
/**
* Class ExportCli
*
* This class handles the CLI commands for exporting schemas.
*
* @package Automattic\WooCommerce\Blueprint\Cli
*/
class ExportCli {
/**
* The path where the exported schema will be saved.
*
* @var string The path where the exported schema will be saved.
*/
private string $save_to;
/**
* ExportCli constructor.
*
* @param string $save_to The path where the exported schema will be saved.
*/
public function __construct( $save_to ) {
$this->save_to = $save_to;
}
/**
* Run the export process.
*
* @param array $args The arguments for the export process.
*/
public function run( $args = array() ) {
$export_as_zip = isset( $args['format'] ) && 'zip' === $args['format'];
if ( ! isset( $args['steps'] ) ) {
$args['steps'] = array();
}
$exporter = new ExportSchema();
$schema = $exporter->export( $args['steps'], $export_as_zip );
if ( $export_as_zip ) {
$zip_exported_schema = new ZipExportedSchema( $schema );
$this->save_to = $zip_exported_schema->zip();
\WP_CLI::success( "Exported zip to {$this->save_to}" );
} else {
// phpcs:ignore
$save = file_put_contents( $this->save_to, json_encode( $schema, JSON_PRETTY_PRINT ) );
if ( false === $save ) {
\WP_CLI::error( "Failed to save to {$this->save_to}" );
} else {
\WP_CLI::success( "Exported JSON to {$this->save_to}" );
}
}
}
}

View File

@@ -0,0 +1,52 @@
<?php
namespace Automattic\WooCommerce\Blueprint\Cli;
use Automattic\WooCommerce\Blueprint\CliResultFormatter;
use Automattic\WooCommerce\Blueprint\ImportSchema;
/**
* Class ImportCli
*/
class ImportCli {
/**
* Schema path
*
* @var string $schema_path The path to the schema file.
*/
private $schema_path;
/**
* ImportCli constructor.
*
* @param string $schema_path The path to the schema file.
*/
public function __construct( $schema_path ) {
$this->schema_path = $schema_path;
}
/**
* Run the import process.
*
* @param array $optional_args Optional arguments.
*
* @return void
*/
public function run( $optional_args ) {
$blueprint = ImportSchema::create_from_file( $this->schema_path );
$results = $blueprint->import();
$result_formatter = new CliResultFormatter( $results );
$is_success = $result_formatter->is_success();
if ( isset( $optional_args['show-messages'] ) ) {
$result_formatter->format( $optional_args['show-messages'] );
}
if ( $is_success ) {
\WP_CLI::success( "$this->schema_path imported successfully" );
} else {
\WP_CLI::error( "Failed to import $this->schema_path. Run with --show-messages=all to debug" );
}
}
}

View File

@@ -0,0 +1,104 @@
<?php
namespace Automattic\WooCommerce\Blueprint;
use Automattic\WooCommerce\Blueprint\Exporters\StepExporter;
use Automattic\WooCommerce\Blueprint\Exporters\ExportInstallPluginSteps;
use Automattic\WooCommerce\Blueprint\Exporters\ExportInstallThemeSteps;
use Automattic\WooCommerce\Blueprint\Exporters\HasAlias;
/**
* Class ExportSchema
*
* Handles the export schema functionality for WooCommerce.
*
* @package Automattic\WooCommerce\Blueprint
*/
class ExportSchema {
use UseWPFunctions;
/**
* Step exporters.
*
* @var StepExporter[] Array of step exporters.
*/
protected array $exporters = array();
/**
* ExportSchema constructor.
*
* @param StepExporter[] $exporters Array of step exporters.
*/
public function __construct( $exporters = array() ) {
$this->exporters = $exporters;
}
/**
* Export the schema steps.
*
* @param string[] $steps Array of step names to export, optional.
* @param bool $zip Whether to export as a ZIP file, optional.
*
* @return array The exported schema array.
*/
public function export( $steps = array(), $zip = false ) {
$schema = array(
'landingPage' => $this->wp_apply_filters( 'wooblueprint_export_landingpage', '/' ),
'steps' => array(),
);
$built_in_exporters = ( new BuiltInExporters() )->get_all();
/**
* Filters the step exporters.
*
* Allows adding/removing custom step exporters.
*
* @param StepExporter[] $exporters Array of step exporters.
*
* @since 0.0.1
*/
$exporters = $this->wp_apply_filters( 'wooblueprint_exporters', array_merge( $this->exporters, $built_in_exporters ) );
// Filter out any exporters that are not in the list of steps to export.
if ( count( $steps ) ) {
foreach ( $exporters as $key => $exporter ) {
$name = $exporter->get_step_name();
$alias = $exporter instanceof HasAlias ? $exporter->get_alias() : $name;
if ( ! in_array( $name, $steps, true ) && ! in_array( $alias, $steps, true ) ) {
unset( $exporters[ $key ] );
}
}
}
if ( $zip ) {
$exporters = array_map(
function ( $exporter ) {
if ( $exporter instanceof ExportInstallPluginSteps ) {
$exporter->include_private_plugins( true );
}
return $exporter;
},
$exporters
);
}
/**
* StepExporter.
*
* @var StepExporter $exporter
*/
foreach ( $exporters as $exporter ) {
$step = $exporter->export();
if ( is_array( $step ) ) {
foreach ( $step as $_step ) {
$schema['steps'][] = $_step->get_json_array();
}
} else {
$schema['steps'][] = $step->get_json_array();
}
}
return $schema;
}
}

View File

@@ -0,0 +1,93 @@
<?php
namespace Automattic\WooCommerce\Blueprint\Exporters;
use Automattic\WooCommerce\Blueprint\Steps\InstallPlugin;
use Automattic\WooCommerce\Blueprint\UseWPFunctions;
/**
* Class ExportInstallPluginSteps
*
* @package Automattic\WooCommerce\Blueprint\Exporters
*/
class ExportInstallPluginSteps implements StepExporter {
use UseWPFunctions;
/**
* Whether to include private plugins in the export.
*
* @var bool Whether to include private plugins in the export.
*/
private bool $include_private_plugins = false;
/**
* Set whether to include private plugins in the export.
*
* @param bool $boolean Whether to include private plugins.
*/
public function include_private_plugins( bool $boolean ) {
$this->include_private_plugins = $boolean;
}
/**
* Export the steps required to install plugins.
*
* @return array The array of InstallPlugin steps.
*/
public function export() {
$plugins = $this->wp_get_plugins();
// @todo temporary fix for JN site -- it includes WooCommerce as a custom plugin
// since JN sites are using a different slug.
$exclude = array( 'WooCommerce Beta Tester' );
$steps = array();
foreach ( $plugins as $path => $plugin ) {
if ( in_array( $plugin['Name'], $exclude, true ) ) {
continue;
}
// skip inactive plugins for now.
if ( ! $this->wp_is_plugin_active( $path ) ) {
continue;
}
$slug = dirname( $path );
// single-file plugin.
if ( '.' === $slug ) {
$slug = pathinfo( $path )['filename'];
}
$info = $this->wp_plugins_api(
'plugin_information',
array(
'slug' => $slug,
'fields' => array(
'sections' => false,
),
)
);
$has_download_link = isset( $info->download_link );
if ( false === $this->include_private_plugins && ! $has_download_link ) {
continue;
}
$resource = $has_download_link ? 'wordpress.org/plugins' : 'self/plugins';
$steps[] = new InstallPlugin(
$slug,
$resource,
array(
'activate' => true,
)
);
}
return $steps;
}
/**
* Get the name of the step.
*
* @return string The step name.
*/
public function get_step_name() {
return InstallPlugin::get_step_name();
}
}

View File

@@ -0,0 +1,63 @@
<?php
namespace Automattic\WooCommerce\Blueprint\Exporters;
use Automattic\WooCommerce\Blueprint\Steps\InstallTheme;
use Automattic\WooCommerce\Blueprint\UseWPFunctions;
/**
* Class ExportInstallThemeSteps
*
* Exporter for the InstallTheme step.
*
* @package Automattic\WooCommerce\Blueprint\Exporters
*/
class ExportInstallThemeSteps implements StepExporter {
use UseWPFunctions;
/**
* Export the steps.
*
* @return array
*/
public function export() {
$steps = array();
$themes = $this->wp_get_themes();
$active_theme = $this->wp_get_theme();
foreach ( $themes as $slug => $theme ) {
// Check if the theme is active.
$is_active = $theme->get( 'Name' ) === $active_theme->get( 'Name' );
$info = $this->wp_themes_api(
'theme_information',
array(
'slug' => $slug,
'fields' => array(
'sections' => false,
),
)
);
if ( isset( $info->download_link ) ) {
$steps[] = new InstallTheme(
$slug,
'wordpress.org/themes',
array(
'activate' => $is_active,
)
);
}
}
return $steps;
}
/**
* Get the step name.
*
* @return string
*/
public function get_step_name() {
return InstallTheme::get_step_name();
}
}

View File

@@ -0,0 +1,38 @@
<?php
namespace Automattic\WooCommerce\Blueprint\Exporters;
/**
* Allows a step to have an alias.
*
* An alias is useful for selective export.
*
* Let's say you have three exporters and all of them use `setSiteOptions` step.
*
* Step A: Exports options from WooCommerce -> Settings
* Step B: Exports options for the core profiler selection.
* Step C: Exports options for the task list.
*
* You also have a UI where a client can select which steps to export. In this case, we have three checkboxes.
*
* [ ] WooCommerce Settings
* [ ] WooCommerce Core Profiler
* [ ] WooCommerce Task List
*
* Without alias, the client would see three `setSiteOptions` steps and it's not possible
* to distinguish between them from the ExportSchema class.
*
* With alias, you can give each step a unique alias while keeping the step name the same.
*
* @todo Link to an example class that uses this interface.
*
* Interface HasAlias
*/
interface HasAlias {
/**
* Get the alias for the step.
*
* @return string The alias for the step.
*/
public function get_alias();
}

View File

@@ -0,0 +1,27 @@
<?php
namespace Automattic\WooCommerce\Blueprint\Exporters;
use Automattic\WooCommerce\Blueprint\Steps\Step;
/**
* Interface StepExporter
*
* A Step Exporter is responsible collecting data needed for a Step object and exporting it.
* Refer to the Step class for the data needed as each step may require different data.
*/
interface StepExporter {
/**
* Collect data needed for a Step object and export it.
*
* @return Step
*/
public function export();
/**
* Returns the name of the step class it exports.
*
* @return string
*/
public function get_step_name();
}

View File

@@ -0,0 +1,191 @@
<?php
namespace Automattic\WooCommerce\Blueprint;
use Automattic\WooCommerce\Blueprint\Schemas\JsonSchema;
use Automattic\WooCommerce\Blueprint\Schemas\ZipSchema;
use Opis\JsonSchema\Errors\ErrorFormatter;
use Opis\JsonSchema\Validator;
/**
* Class ImportSchema
*
* Handles the import schema functionality for WooCommerce.
*
* @package Automattic\WooCommerce\Blueprint
*/
class ImportSchema {
use UseWPFunctions;
/**
* JsonSchema object.
*
* @var JsonSchema The schema instance.
*/
private JsonSchema $schema;
/**
* Validator object.
*
* @var Validator The JSON schema validator instance.
*/
private Validator $validator;
/**
* Built-in step processors.
*
* @var BuiltInStepProcessors The built-in step processors instance.
*/
private BuiltInStepProcessors $builtin_step_processors;
/**
* ImportSchema constructor.
*
* @param JsonSchema $schema The schema instance.
* @param Validator|null $validator The validator instance, optional.
*/
public function __construct( JsonSchema $schema, Validator $validator = null ) {
$this->schema = $schema;
if ( null === $validator ) {
$validator = new Validator();
}
$this->validator = $validator;
$this->builtin_step_processors = new BuiltInStepProcessors( $schema );
}
/**
* Get the schema.
*
* @return JsonSchema The schema.
*/
public function get_schema() {
return $this->schema;
}
/**
* Create an ImportSchema instance from a file.
*
* @param string $file The file path.
* @return ImportSchema The created ImportSchema instance.
*/
public static function create_from_file( $file ) {
// @todo check for mime type
// @todo check for allowed types -- json or zip
$path_info = pathinfo( $file );
$is_zip = 'zip' === $path_info['extension'];
return $is_zip ? self::create_from_zip( $file ) : self::create_from_json( $file );
}
/**
* Create an ImportSchema instance from a JSON file.
*
* @param string $json_path The JSON file path.
* @return ImportSchema The created ImportSchema instance.
*/
public static function create_from_json( $json_path ) {
return new self( new JsonSchema( $json_path ) );
}
/**
* Create an ImportSchema instance from a ZIP file.
*
* @param string $zip_path The ZIP file path.
*
* @return ImportSchema The created ImportSchema instance.
*/
public static function create_from_zip( $zip_path ) {
return new self( new ZipSchema( $zip_path ) );
}
/**
* Import the schema steps.
*
* @return StepProcessorResult[]
*/
public function import() {
$results = array();
$result = StepProcessorResult::success( 'ImportSchema' );
$results[] = $result;
$step_processors = $this->builtin_step_processors->get_all();
/**
* Filters the step processors.
*
* Allows adding/removing custom step processors.
*
* @param StepProcessor[] $step_processors The step processors.
*
* @since 0.0.1
*/
$step_processors = $this->wp_apply_filters( 'wooblueprint_importers', $step_processors );
$indexed_step_processors = Util::index_array(
$step_processors,
function ( $key, $step_processor ) {
return $step_processor->get_step_class()::get_step_name();
}
);
// validate steps before processing.
$this->validate_step_schemas( $indexed_step_processors, $result );
if ( count( $result->get_messages( 'error' ) ) !== 0 ) {
return $results;
}
foreach ( $this->schema->get_steps() as $step_schema ) {
$step_processor = $indexed_step_processors[ $step_schema->step ] ?? null;
if ( ! $step_processor instanceof StepProcessor ) {
$result->add_error( "Unable to create a step processor for {$step_schema->step}" );
continue;
}
$results[] = $step_processor->process( $step_schema );
}
return $results;
}
/**
* Validate the step schemas.
*
* @param array $indexed_step_processors Array of step processors indexed by step name.
* @param StepProcessorResult $result The result object to add messages to.
*
* @return void
*/
protected function validate_step_schemas( array $indexed_step_processors, StepProcessorResult $result ) {
$step_schemas = array_map(
function ( $step_processor ) {
return $step_processor->get_step_class()::get_schema();
},
$indexed_step_processors
);
foreach ( $this->schema->get_steps() as $step_json ) {
$step_schema = $step_schemas[ $step_json->step ] ?? null;
if ( ! $step_schema ) {
$result->add_info( "No schema found for step $step_json->step" );
continue;
}
// phpcs:ignore
$validate = $this->validator->validate( $step_json, json_encode( $step_schema ) );
if ( ! $validate->isValid() ) {
$result->add_error( "Schema validation failed for step {$step_json->step}" );
$errors = ( new ErrorFormatter() )->format( $validate->error() );
$formatted_errors = array();
foreach ( $errors as $value ) {
$formatted_errors[] = implode( "\n", $value );
}
$result->add_error( implode( "\n", $formatted_errors ) );
}
}
}
}

View File

@@ -0,0 +1,47 @@
<?php
namespace Automattic\WooCommerce\Blueprint\Importers;
use Automattic\WooCommerce\Blueprint\StepProcessor;
use Automattic\WooCommerce\Blueprint\StepProcessorResult;
use Automattic\WooCommerce\Blueprint\Steps\ActivatePlugin;
use Automattic\WooCommerce\Blueprint\UsePluginHelpers;
/**
* Class ImportActivatePlugin
*/
class ImportActivatePlugin implements StepProcessor {
use UsePluginHelpers;
/**
* Process the schema.
*
* @param object $schema The schema to process.
*
* @return StepProcessorResult
*/
public function process( $schema ): StepProcessorResult {
$result = StepProcessorResult::success( ActivatePlugin::get_step_name() );
// phpcs:ignore
$name = $schema->pluginName;
$activate = $this->activate_plugin_by_slug( $name );
if ( $activate ) {
$result->add_info( "Activated {$name}." );
} else {
$result->add_error( "Unable to activate {$name}." );
}
return $result;
}
/**
* Get the step class.
*
* @return string
*/
public function get_step_class(): string {
return ActivatePlugin::class;
}
}

View File

@@ -0,0 +1,47 @@
<?php
namespace Automattic\WooCommerce\Blueprint\Importers;
use Automattic\WooCommerce\Blueprint\StepProcessor;
use Automattic\WooCommerce\Blueprint\StepProcessorResult;
use Automattic\WooCommerce\Blueprint\Steps\ActivatePlugin;
use Automattic\WooCommerce\Blueprint\Steps\ActivateTheme;
use Automattic\WooCommerce\Blueprint\UsePluginHelpers;
use Automattic\WooCommerce\Blueprint\UseWPFunctions;
/**
* Class ImportActivateTheme
*
* @package Automattic\WooCommerce\Blueprint\Importers
*/
class ImportActivateTheme implements StepProcessor {
use UsePluginHelpers;
use UseWPFunctions;
/**
* Process the step.
*
* @param object $schema The schema for the step.
*
* @return StepProcessorResult
*/
public function process( $schema ): StepProcessorResult {
$result = StepProcessorResult::success( ActivateTheme::get_step_name() );
// phpcs:ignore
$name = $schema->themeName;
$switch = $this->wp_switch_theme( $name );
$switch && $result->add_debug( "Switched theme to '{$name}'." );
return $result;
}
/**
* Returns the class name of the step this processor handles.
*
* @return string The class name of the step this processor handles.
*/
public function get_step_class(): string {
return ActivateTheme::class;
}
}

View File

@@ -0,0 +1,42 @@
<?php
namespace Automattic\WooCommerce\Blueprint\Importers;
use Automattic\WooCommerce\Blueprint\StepProcessor;
use Automattic\WooCommerce\Blueprint\StepProcessorResult;
use Automattic\WooCommerce\Blueprint\Steps\DeactivatePlugin;
use Automattic\WooCommerce\Blueprint\UsePluginHelpers;
/**
* Class ImportDeactivatePlugin
*/
class ImportDeactivatePlugin implements StepProcessor {
use UsePluginHelpers;
/**
* Process the step.
*
* @param object $schema The schema to process.
*
* @return StepProcessorResult
*/
public function process( $schema ): StepProcessorResult {
$result = StepProcessorResult::success( DeactivatePlugin::get_step_name() );
// phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
$name = $schema->pluginName;
$this->deactivate_plugin_by_slug( $name );
$result->add_info( "Deactivated {$name}." );
return $result;
}
/**
* Get the step class.
*
* @return string
*/
public function get_step_class(): string {
return DeactivatePlugin::class;
}
}

View File

@@ -0,0 +1,47 @@
<?php
namespace Automattic\WooCommerce\Blueprint\Importers;
use Automattic\WooCommerce\Blueprint\StepProcessor;
use Automattic\WooCommerce\Blueprint\StepProcessorResult;
use Automattic\WooCommerce\Blueprint\Steps\DeletePlugin;
use Automattic\WooCommerce\Blueprint\UsePluginHelpers;
/**
* Class ImportDeletePlugin
*/
class ImportDeletePlugin implements StepProcessor {
use UsePluginHelpers;
/**
* Process the schema.
*
* @param object $schema The schema to process.
*
* @return StepProcessorResult
*/
public function process( $schema ): StepProcessorResult {
$result = StepProcessorResult::success( DeletePlugin::get_step_name() );
// phpcs:ignore
$name = $schema->pluginName;
$delete = $this->delete_plugin_by_slug( $name );
if ( $delete ) {
$result->add_info( "Deleted {$name}." );
} else {
$result->add_error( "Unable to delete {$name}." );
}
return $result;
}
/**
* Get the step class.
*
* @return string
*/
public function get_step_class(): string {
return DeletePlugin::class;
}
}

View File

@@ -0,0 +1,156 @@
<?php
namespace Automattic\WooCommerce\Blueprint\Importers;
use Automattic\WooCommerce\Blueprint\ResourceStorages;
use Automattic\WooCommerce\Blueprint\StepProcessor;
use Automattic\WooCommerce\Blueprint\StepProcessorResult;
use Automattic\WooCommerce\Blueprint\Steps\InstallPlugin;
use Automattic\WooCommerce\Blueprint\UseWPFunctions;
use Plugin_Upgrader;
/**
* Class ImportInstallPlugin
*
* Handles the installation and activation of plugins based on a schema.
*/
class ImportInstallPlugin implements StepProcessor {
use UseWPFunctions;
/**
* Resource storage instance for handling plugin files.
*
* @var ResourceStorages
*/
private ResourceStorages $storage;
/**
* Array of paths to installed plugins.
*
* @var array
*/
private array $installed_plugin_paths = array();
/**
* Constructor.
*
* @param ResourceStorages $storage Resource storage instance.
*/
public function __construct( ResourceStorages $storage ) {
$this->storage = $storage;
}
/**
* Processes the schema to install and optionally activate a plugin.
*
* @param object $schema Schema object containing plugin information.
* @return StepProcessorResult Result of the processing.
*/
public function process( $schema ): StepProcessorResult {
$result = StepProcessorResult::success( InstallPlugin::get_step_name() );
$installed_plugins = $this->get_installed_plugins_paths();
// phpcs:ignore
$plugin = $schema->pluginZipFile;
if ( isset( $installed_plugins[ $plugin->slug ] ) ) {
$result->add_info( "Skipped installing {$plugin->slug}. It is already installed." );
return $result;
}
if ( $this->storage->is_supported_resource( $plugin->resource ) === false ) {
$result->add_error( "Invalid resource type for {$plugin->slug}." );
return $result;
}
$downloaded_path = $this->storage->download( $plugin->slug, $plugin->resource );
if ( ! $downloaded_path ) {
$result->add_error( "Unable to download {$plugin->slug} with {$plugin->resource} resource type." );
return $result;
}
$install = $this->install( $downloaded_path );
$install && $result->add_info( "Installed {$plugin->slug}." );
if ( isset( $plugin->options, $plugin->options->activate ) && true === $plugin->options->activate ) {
$activate = $this->activate( $plugin->slug );
if ( $activate instanceof \WP_Error ) {
$result->add_error( "Failed to activate {$plugin->slug}." );
}
if ( null === $activate ) {
$result->add_info( "Activated {$plugin->slug}." );
}
}
return $result;
}
/**
* Installs a plugin from the given local path.
*
* @param string $local_plugin_path Path to the local plugin file.
* @return bool True on success, false on failure.
*/
protected function install( $local_plugin_path ) {
if ( ! class_exists( 'Plugin_Upgrader' ) ) {
include_once ABSPATH . '/wp-admin/includes/class-wp-upgrader.php';
include_once ABSPATH . '/wp-admin/includes/class-plugin-upgrader.php';
}
$upgrader = new \Plugin_Upgrader( new \Automatic_Upgrader_Skin() );
return $upgrader->install( $local_plugin_path );
}
/**
* Activates an installed plugin by its slug.
*
* @param string $slug Plugin slug.
* @return \WP_Error|null WP_Error on failure, null on success.
*/
protected function activate( $slug ) {
if ( empty( $this->installed_plugin_paths ) ) {
$this->installed_plugin_paths = $this->get_installed_plugins_paths();
}
$path = $this->installed_plugin_paths[ $slug ] ?? false;
if ( ! $path ) {
return new \WP_Error( 'plugin_not_installed', "Plugin {$slug} is not installed." );
}
return $this->wp_activate_plugin( $path );
}
/**
* Retrieves an array of installed plugins and their paths.
*
* @return array Array of installed plugins and their paths.
*/
protected function get_installed_plugins_paths() {
if ( ! function_exists( 'get_plugins' ) ) {
require_once ABSPATH . 'wp-admin/includes/plugin.php';
}
$plugins = get_plugins();
$installed_plugins = array();
foreach ( $plugins as $path => $plugin ) {
$path_parts = explode( '/', $path );
$slug = $path_parts[0];
$installed_plugins[ $slug ] = $path;
}
return $installed_plugins;
}
/**
* Returns the class name of the step being processed.
*
* @return string Class name of the step.
*/
public function get_step_class(): string {
return InstallPlugin::class;
}
}

View File

@@ -0,0 +1,120 @@
<?php
namespace Automattic\WooCommerce\Blueprint\Importers;
use Automattic\WooCommerce\Blueprint\ResourceStorages;
use Automattic\WooCommerce\Blueprint\StepProcessor;
use Automattic\WooCommerce\Blueprint\StepProcessorResult;
use Automattic\WooCommerce\Blueprint\Steps\InstallTheme;
use Automattic\WooCommerce\Blueprint\UseWPFunctions;
use Plugin_Upgrader;
/**
* Class ImportInstallTheme
*
* This class handles the import process for installing themes.
*
* @package Automattic\WooCommerce\Blueprint\Importers
*/
class ImportInstallTheme implements StepProcessor {
use UseWPFunctions;
/**
* Collection of resource storages.
*
* @var ResourceStorages The resource storage used for downloading themes.
*/
private ResourceStorages $storage;
/**
* The result of the step processing.
*
* @var StepProcessorResult The result of the step processing.
*/
private StepProcessorResult $result;
/**
* ImportInstallTheme constructor.
*
* @param ResourceStorages $storage The resource storage used for downloading themes.
*/
public function __construct( ResourceStorages $storage ) {
$this->result = StepProcessorResult::success( InstallTheme::get_step_name() );
$this->storage = $storage;
}
/**
* Process the schema to install the theme.
*
* @param object $schema The schema containing theme installation details.
*
* @return StepProcessorResult The result of the step processing.
*/
public function process( $schema ): StepProcessorResult {
$installed_themes = $this->wp_get_themes();
// phpcs:ignore
$theme = $schema->themeZipFile;
if ( isset( $installed_themes[ $theme->slug ] ) ) {
$this->result->add_info( "Skipped installing {$theme->slug}. It is already installed." );
return $this->result;
}
if ( $this->storage->is_supported_resource( $theme->resource ) === false ) {
$this->result->add_error( "Invalid resource type for {$theme->slug}" );
return $this->result;
}
$downloaded_path = $this->storage->download( $theme->slug, $theme->resource );
if ( ! $downloaded_path ) {
$this->result->add_error( "Unable to download {$theme->slug} with {$theme->resource} resource type." );
return $this->result;
}
$this->result->add_debug( "'$theme->slug' has been downloaded in $downloaded_path" );
$install = $this->install( $downloaded_path );
if ( $install ) {
$this->result->add_debug( "Theme '$theme->slug' installed successfully." );
} else {
$this->result->add_error( "Failed to install theme '$theme->slug'." );
}
$theme_switch = true === $theme->activate && $this->wp_switch_theme( $theme->slug );
if ( $theme_switch ) {
$this->result->add_info( "Switched theme to '$theme->slug'." );
} else {
$this->result->add_error( "Failed to switch theme to '$theme->slug'." );
}
return $this->result;
}
/**
* Install the theme from the local plugin path.
*
* @param string $local_plugin_path The local path of the plugin to be installed.
*
* @return bool True if the installation was successful, false otherwise.
*/
protected function install( $local_plugin_path ) {
$unzip_result = $this->wp_unzip_file( $local_plugin_path, $this->wp_get_theme_root() );
if ( $this->is_wp_error( $unzip_result ) ) {
return false;
}
return true;
}
/**
* Get the class name of the step.
*
* @return string The class name of the step.
*/
public function get_step_class(): string {
return InstallTheme::class;
}
}

View File

@@ -0,0 +1,57 @@
<?php
namespace Automattic\WooCommerce\Blueprint\Importers;
use Automattic\WooCommerce\Blueprint\StepProcessor;
use Automattic\WooCommerce\Blueprint\StepProcessorResult;
use Automattic\WooCommerce\Blueprint\Steps\SetSiteOptions;
use Automattic\WooCommerce\Blueprint\UseWPFunctions;
/**
* Class ImportSetSiteOptions
*
* Importer for the SetSiteOptions step.
*
* @package Automattic\WooCommerce\Blueprint\Importers
*/
class ImportSetSiteOptions implements StepProcessor {
use UseWPFunctions;
/**
* Process the step.
*
* @param object $schema The schema to process.
*
* @return StepProcessorResult
*/
public function process( $schema ): StepProcessorResult {
$result = StepProcessorResult::success( SetSiteOptions::get_step_name() );
foreach ( $schema->options as $key => $value ) {
if ( is_object( $value ) ) {
$value = (array) $value;
}
$updated = $this->wp_update_option( $key, $value );
if ( $updated ) {
$result->add_info( "{$key} has been updated" );
} else {
$current_value = $this->wp_get_option( $key );
if ( $current_value === $value ) {
$result->add_info( "{$key} has not been updated because the current value is already up to date." );
}
}
}
return $result;
}
/**
* Get the step class.
*
* @return string
*/
public function get_step_class(): string {
return SetSiteOptions::class;
}
}

View File

@@ -0,0 +1,68 @@
<?php
namespace Automattic\WooCommerce\Blueprint;
use Automattic\WooCommerce\Blueprint\ResourceStorages\ResourceStorage;
/**
* Class ResourceStorages
*/
class ResourceStorages {
/**
* Storage collection.
*
* @var ResourceStorages[]
*/
protected array $storages = array();
/**
* Add a downloader.
*
* @param ResourceStorage $downloader The downloader to add.
*
* @return void
*/
public function add_storage( ResourceStorage $downloader ) {
$supported_resource = $downloader->get_supported_resource();
if ( ! isset( $this->storages[ $supported_resource ] ) ) {
$this->storages[ $supported_resource ] = array();
}
$this->storages[ $supported_resource ][] = $downloader;
}
/**
* Check if the resource is supported.
*
* @param string $resource The resource to check.
*
* @return bool
*/
// phpcs:ignore
public function is_supported_resource( $resource ) {
return isset( $this->storages[ $resource ] );
}
/**
* Download the resource.
*
* @param string $slug The slug of the resource to download.
* @param string $resource The resource to download.
*
* @return false|string
*/
// phpcs:ignore
public function download( $slug, $resource ) {
if ( ! isset( $this->storages[ $resource ] ) ) {
return false;
}
$storages = $this->storages[ $resource ];
foreach ( $storages as $storage ) {
// phpcs:ignore
if ( $found = $storage->download( $slug ) ) {
return $found;
}
}
return false;
}
}

View File

@@ -0,0 +1,58 @@
<?php
namespace Automattic\WooCommerce\Blueprint\ResourceStorages;
/**
* Class LocalPluginResourceStorage
*/
class LocalPluginResourceStorage implements ResourceStorage {
/**
* Paths to the directories containing the plugins.
*
* @var array The paths to the directories containing the plugins.
*/
protected array $paths = array();
/**
* Suffix of the plugin files.
*
* @var string The suffix of the plugin files.
*/
protected string $suffix = 'plugins';
/**
* LocalPluginResourceStorage constructor.
*
* @param string $path The path to the directory containing the plugins.
*/
public function __construct( $path ) {
$this->paths[] = $path;
}
/**
* Local plugins are already included (downloaded) in the zip file.
* Return the full path.
*
* @param string $slug The slug of the plugin to be downloaded.
*
* @return string|null
*/
public function download( $slug ): ?string {
foreach ( $this->paths as $path ) {
$full_path = $path . "/{$this->suffix}/" . $slug . '.zip';
if ( is_file( $full_path ) ) {
return $full_path;
}
}
return null;
}
/**
* Get the supported resource.
*
* @return string The supported resource.
*/
public function get_supported_resource(): string {
return 'self/plugins';
}
}

View File

@@ -0,0 +1,24 @@
<?php
namespace Automattic\WooCommerce\Blueprint\ResourceStorages;
/**
* Class LocalThemeResourceStorage
*/
class LocalThemeResourceStorage extends LocalPluginResourceStorage {
/**
* The suffix.
*
* @var string The suffix.
*/
protected string $suffix = 'themes';
/**
* Get the supported resource.
*
* @return string The supported resource.
*/
public function get_supported_resource(): string {
return 'self/themes';
}
}

View File

@@ -0,0 +1,76 @@
<?php
namespace Automattic\WooCommerce\Blueprint\ResourceStorages;
use Automattic\WooCommerce\Blueprint\UseWPFunctions;
/**
* Class OrgPluginResourceStorage
*
* This class handles the storage and downloading of plugins from wordpress.org.
*
* @package Automattic\WooCommerce\Blueprint\ResourceStorages
*/
class OrgPluginResourceStorage implements ResourceStorage {
use UseWPFunctions;
/**
* Download the plugin from wordpress.org
*
* @param string $slug The slug of the plugin to be downloaded.
*
* @return string|null The path to the downloaded plugin file, or null on failure.
*/
public function download( $slug ): ?string {
$download_link = $this->get_download_link( $slug );
if ( ! $download_link ) {
return false;
}
return $this->download_url( $download_link );
}
/**
* Download the file from the given URL.
*
* @param string $url The URL to download the file from.
*
* @return string|null The path to the downloaded file, or null on failure.
*/
protected function download_url( $url ) {
return $this->wp_download_url( $url );
}
/**
* Get the download link for a plugin from wordpress.org.
*
* @param string $slug The slug of the plugin.
*
* @return string|null The download link, or null if not found.
*/
protected function get_download_link( $slug ): ?string {
$info = $this->wp_plugins_api(
'plugin_information',
array(
'slug' => $slug,
'fields' => array(
'sections' => false,
),
)
);
if ( is_object( $info ) && isset( $info->download_link ) ) {
return $info->download_link;
}
return null;
}
/**
* Get the supported resource type.
*
* @return string The supported resource type.
*/
public function get_supported_resource(): string {
return 'wordpress.org/plugins';
}
}

View File

@@ -0,0 +1,42 @@
<?php
namespace Automattic\WooCommerce\Blueprint\ResourceStorages;
/**
* Class OrgThemeResourceStorage
*/
class OrgThemeResourceStorage extends OrgPluginResourceStorage {
/**
* Get the download link.
*
* @param string $slug The slug of the theme to be downloaded.
*
* @return string|null The download link.
*/
protected function get_download_link( $slug ): ?string {
$info = $this->wp_themes_api(
'theme_information',
array(
'slug' => $slug,
'fields' => array(
'sections' => false,
),
)
);
if ( isset( $info->download_link ) ) {
return $info->download_link;
}
return null;
}
/**
* Get the supported resource.
*
* @return string The supported resource.
*/
public function get_supported_resource(): string {
return 'wordpress.org/themes';
}
}

View File

@@ -0,0 +1,30 @@
<?php
namespace Automattic\WooCommerce\Blueprint\ResourceStorages;
/**
* Interface ResourceStorage
*
* ResourceStorage is an abstraction layer for various storages for WordPress files
* such as plugins and themes. It provides a common interface for downloading
* the files whether they are stored locally or remotely.
*
* @package Automattic\WooCommerce\Blueprint\ResourceStorages
*/
interface ResourceStorage {
/**
* Return supported resource type.
*
* @return string
*/
public function get_supported_resource(): string;
/**
* Download the resource.
*
* @param string $slug resource slug.
*
* @return string|null downloaded local path.
*/
public function download( $slug ): ?string;
}

View File

@@ -0,0 +1,66 @@
<?php
namespace Automattic\WooCommerce\Blueprint;
use function WP_CLI\Utils\format_items;
/**
* Class CliResultFormatter
*/
class CliResultFormatter {
/**
* The results to format.
*
* @var StepProcessorResult[]
*/
private array $results;
/**
* CliResultFormatter constructor.
*
* @param array $results The results to format.
*/
public function __construct( array $results ) {
$this->results = $results;
}
/**
* Format the results.
*
* @param string $message_type The message type to format.
*
* @return void
*/
public function format( $message_type = 'debug' ) {
$header = array( 'Step Processor', 'Type', 'Message' );
$items = array();
foreach ( $this->results as $result ) {
$step_name = $result->get_step_name();
foreach ( $result->get_messages( $message_type ) as $message ) {
$items[] = array(
'Step Processor' => $step_name,
'Type' => $message['type'],
'Message' => $message['message'],
);
}
}
format_items( 'table', $items, $header );
}
/**
* Check if all results are successful.
*
* @return bool True if all results are successful, false otherwise.
*/
public function is_success() {
foreach ( $this->results as $result ) {
$is_success = $result->is_success();
if ( ! $is_success ) {
return false;
}
}
return true;
}
}

View File

@@ -0,0 +1,69 @@
<?php
namespace Automattic\WooCommerce\Blueprint;
/**
* Class JsonResultFormatter
*/
class JsonResultFormatter {
/**
* The results to format.
*
* @var StepProcessorResult[]
*/
private array $results;
/**
* JsonResultFormatter constructor.
*
* @param array $results The results to format.
*/
public function __construct( array $results ) {
$this->results = $results;
}
/**
* Format the results.
*
* @param string $message_type The message type to format.
*
* @return array
*/
public function format( $message_type = 'all' ) {
$data = array(
'is_success' => $this->is_success(),
'messages' => array(),
);
foreach ( $this->results as $result ) {
$step_name = $result->get_step_name();
foreach ( $result->get_messages( $message_type ) as $message ) {
if ( ! isset( $data['messages'][ $message['type'] ] ) ) {
$data['messages'][ $message['type'] ] = array();
}
$data['messages'][ $message['type'] ][] = array(
'step' => $step_name,
'type' => $message['type'],
'message' => $message['message'],
);
}
}
return $data;
}
/**
* Check if all results are successful.
*
* @return bool True if all results are successful, false otherwise.
*/
public function is_success() {
foreach ( $this->results as $result ) {
$is_success = $result->is_success();
if ( ! $is_success ) {
return false;
}
}
return true;
}
}

View File

@@ -0,0 +1,78 @@
<?php
namespace Automattic\WooCommerce\Blueprint\Schemas;
/**
* Class JsonSchema
*/
class JsonSchema {
/**
* The schema.
*
* @var object The schema.
*/
protected $schema;
/**
* JsonSchema constructor.
*
* @param string $json_path The path to the JSON file.
* @throws \InvalidArgumentException If the JSON is invalid or missing 'steps' field.
*/
public function __construct( $json_path ) {
// phpcs:ignore
$schema = json_decode( file_get_contents( $json_path ) );
$this->schema = $schema;
if ( ! $this->validate() ) {
throw new \InvalidArgumentException( "Invalid JSON or missing 'steps' field." );
}
}
/**
* Returns the steps from the schema.
*
* @return array
*/
public function get_steps() {
return $this->schema->steps;
}
/**
* Returns steps by name.
*
* @param string $name The name of the step.
*
* @return array
*/
public function get_step( $name ) {
$steps = array();
foreach ( $this->schema->steps as $step ) {
if ( $step->step === $name ) {
$steps[] = $step;
}
}
return $steps;
}
/**
* Just makes sure that the JSON contains 'steps' field.
*
* We're going to validate 'steps' later because we can't know the exact schema
* ahead of time. 3rd party plugins can add their step processors.
*
* @return bool[
*/
public function validate() {
if ( json_last_error() !== JSON_ERROR_NONE ) {
return false;
}
if ( ! isset( $this->schema->steps ) ) {
return false;
}
return true;
}
}

View File

@@ -0,0 +1,74 @@
<?php
namespace Automattic\WooCommerce\Blueprint\Schemas;
use Automattic\WooCommerce\Blueprint\UseWPFunctions;
/**
* Class ZipSchema
*
* Handles the import schema functionality for WooCommerce.
*
* @package Automattic\WooCommerce\Blueprint
*/
class ZipSchema extends JsonSchema {
use UseWPFunctions;
/**
* Path to the unzip.
*
* @var string|mixed The path to the zip file.
*/
protected string $unzip_path;
/**
* Path to the unzipped file.
*
* @var string|mixed The path to the unzipped file.
*/
protected string $unzipped_path;
/**
* ZipSchema constructor.
*
* @param string $zip_path The path to the zip file.
* @param string $unzip_path The path to unzip the file to.
*
* @throws \Exception If the file cannot be unzipped.
*/
public function __construct( $zip_path, $unzip_path = null ) {
// Set the unzip path, defaulting to the WordPress upload directory if not provided.
$this->unzip_path = $unzip_path ?? $this->wp_upload_dir()['path'];
// Attempt to unzip the file.
$unzip_result = $this->wp_unzip_file( $zip_path, $this->unzip_path );
if ( $unzip_result instanceof \WP_Error ) {
throw new \Exception( $unzip_result->get_error_message() );
}
// Determine the name of the unzipped directory.
$unzipped_dir_name = str_replace( '.zip', '', basename( $zip_path ) );
// Define the paths to the JSON file and the unzipped directory.
$json_path = "{$this->unzip_path}/{$unzipped_dir_name}/woo-blueprint.json";
$this->unzipped_path = "{$this->unzip_path}/{$unzipped_dir_name}";
// Check if the JSON file exists in the expected location.
if ( ! file_exists( $json_path ) ) {
// Update paths if the JSON file is in the unzip root directory.
$this->unzipped_path = $this->unzip_path;
$json_path = "{$this->unzip_path}/woo-blueprint.json";
}
parent::__construct( $json_path );
}
/**
* Get the path to the unzipped file.
*
* @return mixed|string
*/
public function get_unzipped_path() {
return $this->unzipped_path;
}
}

View File

@@ -0,0 +1,24 @@
<?php
namespace Automattic\WooCommerce\Blueprint;
/**
* Interface StepProcessor
*/
interface StepProcessor {
/**
* Process the schema.
*
* @param object $schema The schema to process.
*
* @return StepProcessorResult
*/
public function process( $schema ): StepProcessorResult;
/**
* Get the step class.
*
* @return string
*/
public function get_step_class(): string;
}

View File

@@ -0,0 +1,157 @@
<?php
namespace Automattic\WooCommerce\Blueprint;
use InvalidArgumentException;
/**
* A class returned by StepProcessor classes containing result of the process and messages.
*/
class StepProcessorResult {
const MESSAGE_TYPES = array( 'error', 'info', 'debug' );
/**
* Messages
*
* @var array $messages
*/
private array $messages = array();
/**
* Indicate whether the process was success or not
*
* @var bool $success
*/
private bool $success;
/**
* Step name
*
* @var string $step_name
*/
private string $step_name;
/**
* Construct.
*
* @param bool $success Indicate whether the process was success or not.
* @param string $step_name The name of the step.
*/
public function __construct( bool $success, string $step_name ) {
$this->success = $success;
$this->step_name = $step_name;
}
/**
* Get messages.
*
* @param string $step_name The name of the step.
*
* @return void
*/
public function set_step_name( $step_name ) {
$this->step_name = $step_name;
}
/**
* Create a new instance with $success = true.
*
* @param string $stp_name The name of the step.
*
* @return StepProcessorResult
*/
public static function success( string $stp_name ): self {
return ( new self( true, $stp_name ) );
}
/**
* Add a new message.
*
* @param string $message message.
* @param string $type one of error, info.
*
* @throws InvalidArgumentException When incorrect type is given.
* @return void
*/
public function add_message( string $message, string $type = 'error' ) {
if ( ! in_array( $type, self::MESSAGE_TYPES, true ) ) {
// phpcs:ignore
throw new InvalidArgumentException( "{$type} is not allowed. Type must be one of " . implode( ',', self::MESSAGE_TYPES ) );
}
$this->messages[] = compact( 'message', 'type' );
}
/**
* Add a new error message.
*
* @param string $message message.
*
* @return void
*/
public function add_error( string $message ) {
$this->add_message( $message );
}
/**
* Add a new debug message.
*
* @param string $message message.
*
* @return void
*/
public function add_debug( string $message ) {
$this->add_message( $message, 'debug' );
}
/**
* Add a new info message.
*
* @param string $message message.
*
* @return void
*/
public function add_info( string $message ) {
$this->add_message( $message, 'info' );
}
/**
* Filter messages.
*
* @param string $type one of all, error, and info.
*
* @return array
*/
public function get_messages( string $type = 'all' ): array {
if ( 'all' === $type ) {
return $this->messages;
}
return array_filter(
$this->messages,
function ( $message ) use ( $type ) {
return $type === $message['type'];
}
);
}
/**
* Check to see if the result was success.
*
* @return bool
*/
public function is_success(): bool {
return true === $this->success && 0 === count( $this->get_messages( 'error' ) );
}
/**
* Get the name of the step.
*
* @return string The name of the step.
*/
public function get_step_name() {
return $this->step_name;
}
}

View File

@@ -0,0 +1,69 @@
<?php
namespace Automattic\WooCommerce\Blueprint\Steps;
/**
* Class ActivatePlugin
*
* @package Automattic\WooCommerce\Blueprint\Steps
*/
class ActivatePlugin extends Step {
/**
* The name of the plugin to be activated.
*
* @var string The name of the plugin to be activated.
*/
private string $plugin_name;
/**
* ActivatePlugin constructor.
*
* @param string $plugin_name The name of the plugin to be activated.
*/
public function __construct( $plugin_name ) {
$this->plugin_name = $plugin_name;
}
/**
* Returns the name of this step.
*
* @return string The step name.
*/
public static function get_step_name(): string {
return 'activatePlugin';
}
/**
* Returns the schema for the JSON representation of this step.
*
* @param int $version The version of the schema to return.
* @return array The schema array.
*/
public static function get_schema( int $version = 1 ): array {
return array(
'type' => 'object',
'properties' => array(
'step' => array(
'type' => 'string',
'enum' => array( static::get_step_name() ),
),
'pluginName' => array(
'type' => 'string',
),
),
'required' => array( 'step', 'pluginName' ),
);
}
/**
* Prepares an associative array for JSON encoding.
*
* @return array Array of data to be encoded as JSON.
*/
public function prepare_json_array(): array {
return array(
'step' => static::get_step_name(),
'pluginName' => $this->plugin_name,
);
}
}

View File

@@ -0,0 +1,69 @@
<?php
namespace Automattic\WooCommerce\Blueprint\Steps;
/**
* Class ActivateTheme
*
* @package Automattic\WooCommerce\Blueprint\Steps
*/
class ActivateTheme extends Step {
/**
* The name of the theme to be activated.
*
* @var string The name of the theme to be activated.
*/
private string $theme_name;
/**
* ActivateTheme constructor.
*
* @param string $theme_name The name of the theme to be activated.
*/
public function __construct( $theme_name ) {
$this->theme_name = $theme_name;
}
/**
* Returns the name of this step.
*
* @return string The step name.
*/
public static function get_step_name(): string {
return 'activateTheme';
}
/**
* Returns the schema for the JSON representation of this step.
*
* @param int $version The version of the schema to return.
* @return array The schema array.
*/
public static function get_schema( int $version = 1 ): array {
return array(
'type' => 'object',
'properties' => array(
'step' => array(
'type' => 'string',
'enum' => array( static::get_step_name() ),
),
'themeName' => array(
'type' => 'string',
),
),
'required' => array( 'step', 'themeName' ),
);
}
/**
* Prepares an associative array for JSON encoding.
*
* @return array Array of data to be encoded as JSON.
*/
public function prepare_json_array(): array {
return array(
'step' => static::get_step_name(),
'themeName' => $this->theme_name,
);
}
}

View File

@@ -0,0 +1,68 @@
<?php
namespace Automattic\WooCommerce\Blueprint\Steps;
/**
* Class DeactivatePlugin
*/
class DeactivatePlugin extends Step {
/**
* The plugin name.
*
* @var string $plugin_name The plugin name.
*/
private string $plugin_name;
/**
* DeactivatePlugin constructor.
*
* @param string $plugin_name string The plugin name.
*/
public function __construct( $plugin_name ) {
$this->plugin_name = $plugin_name;
}
/**
* Get the step name.
*
* @return string
*/
public static function get_step_name(): string {
return 'deactivatePlugin';
}
/**
* Get the schema for this step.
*
* @param int $version The schema version.
*
* @return array
*/
public static function get_schema( int $version = 1 ): array {
return array(
'type' => 'object',
'properties' => array(
'step' => array(
'type' => 'string',
'enum' => array( static::get_step_name() ),
),
'pluginName' => array(
'type' => 'string',
),
),
'required' => array( 'step', 'pluginName' ),
);
}
/**
* Prepare the JSON array for this step.
*
* @return array
*/
public function prepare_json_array(): array {
return array(
'step' => static::get_step_name(),
'pluginName' => $this->plugin_name,
);
}
}

View File

@@ -0,0 +1,69 @@
<?php
namespace Automattic\WooCommerce\Blueprint\Steps;
/**
* Class DeletePlugin
*
* @package Automattic\WooCommerce\Blueprint\Steps
*/
class DeletePlugin extends Step {
/**
* The name of the plugin to be deleted.
*
* @var string The name of the plugin to be deleted.
*/
private string $plugin_name;
/**
* DeletePlugin constructor.
*
* @param string $plugin_name The name of the plugin to be deleted.
*/
public function __construct( $plugin_name ) {
$this->plugin_name = $plugin_name;
}
/**
* Returns the name of this step.
*
* @return string The step name.
*/
public static function get_step_name(): string {
return 'deletePlugin';
}
/**
* Returns the schema for the JSON representation of this step.
*
* @param int $version The version of the schema to return.
* @return array The schema array.
*/
public static function get_schema( int $version = 1 ): array {
return array(
'type' => 'object',
'properties' => array(
'step' => array(
'type' => 'string',
'enum' => array( static::get_step_name() ),
),
'pluginName' => array(
'type' => 'string',
),
),
'required' => array( 'step', 'pluginName' ),
);
}
/**
* Prepares an associative array for JSON encoding.
*
* @return array Array representation of this step.
*/
public function prepare_json_array(): array {
return array(
'step' => static::get_step_name(),
'pluginName' => $this->plugin_name,
);
}
}

View File

@@ -0,0 +1,113 @@
<?php
namespace Automattic\WooCommerce\Blueprint\Steps;
/**
* Class InstallPlugin
*
* This class represents a step in the installation process of a WooCommerce plugin.
* It includes methods to prepare the data for the plugin installation step and to provide
* the schema for the JSON representation of this step.
*
* @package Automattic\WooCommerce\Blueprint\Steps
*/
class InstallPlugin extends Step {
/**
* The slug of the plugin to be installed.
*
* @var string The slug of the plugin to be installed.
*/
private string $slug;
/**
* The resource URL or path to the plugin's ZIP file.
*
* @var string The resource URL or path to the plugin's ZIP file.
*/
private string $resource;
/**
* Additional options for the plugin installation.
*
* @var array Additional options for the plugin installation.
*/
private array $options;
/**
* InstallPlugin constructor.
*
* @param string $slug The slug of the plugin to be installed.
* @param string $resource The resource URL or path to the plugin's ZIP file.
* @param array $options Additional options for the plugin installation.
*/
// phpcs:ignore
public function __construct( $slug, $resource, array $options = array() ) {
$this->slug = $slug;
$this->resource = $resource;
$this->options = $options;
}
/**
* Prepares an associative array for JSON encoding.
*
* @return array Array representing this installation step.
*/
public function prepare_json_array(): array {
return array(
'step' => static::get_step_name(),
'pluginZipFile' => array(
'resource' => $this->resource,
'slug' => $this->slug,
),
'options' => $this->options,
);
}
/**
* Returns the schema for the JSON representation of this step.
*
* @param int $version The version of the schema to return.
* @return array The schema array.
*/
public static function get_schema( int $version = 1 ): array {
return array(
'type' => 'object',
'properties' => array(
'step' => array(
'type' => 'string',
'enum' => array( static::get_step_name() ),
),
'pluginZipFile' => array(
'type' => 'object',
'properties' => array(
'resource' => array(
'type' => 'string',
),
'slug' => array(
'type' => 'string',
),
),
'required' => array( 'resource', 'slug' ),
),
'options' => array(
'type' => 'object',
'properties' => array(
'activate' => array(
'type' => 'boolean',
),
),
),
),
'required' => array( 'step', 'pluginZipFile' ),
);
}
/**
* Returns the name of this step.
*
* @return string The step name.
*/
public static function get_step_name(): string {
return 'installPlugin';
}
}

View File

@@ -0,0 +1,113 @@
<?php
namespace Automattic\WooCommerce\Blueprint\Steps;
/**
* Class InstallTheme
*
* This class represents a step in the installation process of a WooCommerce theme.
* It includes methods to prepare the data for the theme installation step and to provide
* the schema for the JSON representation of this step.
*
* @package Automattic\WooCommerce\Blueprint\Steps
*/
class InstallTheme extends Step {
/**
* The slug of the theme to be installed.
*
* @var string The slug of the theme to be installed.
*/
private string $slug;
/**
* The resource URL or path to the theme's ZIP file.
*
* @var string The resource URL or path to the theme's ZIP file.
*/
private string $resource;
/**
* Additional options for the theme installation.
*
* @var array Additional options for the theme installation.
*/
private array $options;
/**
* InstallTheme constructor.
*
* @param string $slug The slug of the theme to be installed.
* @param string $resource The resource URL or path to the theme's ZIP file.
* @param array $options Additional options for the theme installation.
*/
// phpcs:ignore
public function __construct( $slug, $resource, array $options = array() ) {
$this->slug = $slug;
$this->resource = $resource;
$this->options = $options;
}
/**
* Prepares an associative array for JSON encoding.
*
* @return array The JSON-encoded array representing this installation step.
*/
public function prepare_json_array(): array {
return array(
'step' => static::get_step_name(),
'themeZipFile' => array(
'resource' => $this->resource,
'slug' => $this->slug,
),
'options' => $this->options,
);
}
/**
* Returns the schema for the JSON representation of this step.
*
* @param int $version The version of the schema to return.
* @return array The schema array.
*/
public static function get_schema( int $version = 1 ): array {
return array(
'type' => 'object',
'properties' => array(
'step' => array(
'type' => 'string',
'enum' => array( static::get_step_name() ),
),
'themeZipFile' => array(
'type' => 'object',
'properties' => array(
'resource' => array(
'type' => 'string',
),
'slug' => array(
'type' => 'string',
),
),
'required' => array( 'resource', 'slug' ),
),
'options' => array(
'type' => 'object',
'properties' => array(
'activate' => array(
'type' => 'boolean',
),
),
),
),
'required' => array( 'step', 'themeZipFile' ),
);
}
/**
* Returns the name of this step.
*
* @return string The step name.
*/
public static function get_step_name(): string {
return 'installTheme';
}
}

View File

@@ -0,0 +1,69 @@
<?php
namespace Automattic\WooCommerce\Blueprint\Steps;
/**
* Set site options step.
*/
class SetSiteOptions extends Step {
/**
* Site options.
*
* @var array site options
*/
private array $options;
/**
* Constructor.
*
* @param array $options site options.
*/
public function __construct( array $options = array() ) {
$this->options = $options;
}
/**
* Get the name of the step.
*
* @return string step name
*/
public static function get_step_name(): string {
return 'setSiteOptions';
}
/**
* Get the schema for the step.
*
* @param int $version schema version.
*
* @return array schema for the step
*/
public static function get_schema( int $version = 1 ): array {
return array(
'type' => 'object',
'properties' => array(
'step' => array(
'type' => 'string',
'enum' => array( static::get_step_name() ),
),
'options' => array(
'type' => 'object',
'additionalProperties' => new \stdClass(),
),
),
'required' => array( 'step', 'options' ),
);
}
/**
* Prepare the step for JSON serialization.
*
* @return array array representation of the step
*/
public function prepare_json_array(): array {
return array(
'step' => static::get_step_name(),
'options' => $this->options,
);
}
}

View File

@@ -0,0 +1,68 @@
<?php
namespace Automattic\WooCommerce\Blueprint\Steps;
/**
* Abstract class Step
*
* This class defines the structure for a Step that requires arguments to perform an action.
* You can think it as a function described in JSON format.
*
* A Step should also be capable of returning formatted data that can be imported later.
* Additionally, a Step can validate data.
*/
abstract class Step {
/**
* Meta values for the step.
*
* @var array $meta_values
*/
protected array $meta_values = array();
/**
* Get the step name.
*
* @return string
*/
abstract public static function get_step_name(): string;
/**
* Get the schema for this step.
*
* @param int $version The schema version.
*
* @return array
*/
abstract public static function get_schema( int $version = 1 ): array;
/**
* Prepare the JSON array for this step.
*
* @return array The JSON array for the step.
*/
abstract public function prepare_json_array(): array;
/**
* Set meta values for the step.
*
* @param array $meta_values The meta values.
*
* @return void
*/
public function set_meta_values( array $meta_values ) {
$this->meta_values = $meta_values;
}
/**
* Get the JSON array for the step.
*
* @return mixed
*/
public function get_json_array() {
$json_array = $this->prepare_json_array();
if ( ! empty( $this->meta_values ) ) {
$json_array['meta'] = $this->meta_values;
}
return $json_array;
}
}

View File

@@ -0,0 +1,111 @@
<?php
namespace Automattic\WooCommerce\Blueprint;
trait UsePluginHelpers {
use UseWPFunctions;
/**
* Activate a plugin by its slug.
*
* Searches for the plugin with the specified slug in the installed plugins
* and activates it.
*
* @param string $slug The slug of the plugin to activate.
*
* @return bool True if the plugin was activated, false otherwise.
*/
public function activate_plugin_by_slug( $slug ) {
// Get all installed plugins.
$all_plugins = $this->wp_get_plugins();
// Loop through all plugins to find the one with the specified slug.
foreach ( $all_plugins as $plugin_path => $plugin_info ) {
// Check if the plugin path contains the slug.
if ( strpos( $plugin_path, $slug . '/' ) === 0 ) {
// Deactivate the plugin.
return $this->wp_activate_plugin( $plugin_path );
}
}
return false;
}
/**
* Check if a plugin with the specified slug is installed.
*
* @param string $slug The slug of the plugin to check.
*
* @return bool
*/
public function is_plugin_dir( $slug ) {
$all_plugins = $this->wp_get_plugins();
foreach ( $all_plugins as $plugin_file => $plugin_data ) {
// Extract the directory name from the plugin file path
$plugin_dir = explode( '/', $plugin_file )[0];
// Check for an exact match with the slug
if ( $plugin_dir === $slug ) {
return true;
}
}
return false;
}
/**
* Deactivate and delete a plugin by its slug.
*
* Searches for the plugin with the specified slug in the installed plugins,
* deactivates it if active, and then deletes it.
*
* @param string $slug The slug of the plugin to delete.
*
* @return bool True if the plugin was deleted, false otherwise.
*/
public function delete_plugin_by_slug( $slug ) {
// Get all installed plugins.
$all_plugins = $this->wp_get_plugins();
// Loop through all plugins to find the one with the specified slug.
foreach ( $all_plugins as $plugin_path => $plugin_info ) {
// Check if the plugin path contains the slug.
if ( strpos( $plugin_path, $slug . '/' ) === 0 ) {
// Deactivate the plugin.
if ( $this->deactivate_plugin_by_slug( $slug ) ) {
// Delete the plugin.
return $this->wp_delete_plugins( array( $plugin_path ) );
}
}
}
return false;
}
/**
* Deactivate a plugin by its slug.
*
* Searches for the plugin with the specified slug in the installed plugins
* and deactivates it.
*
* @param string $slug The slug of the plugin to deactivate.
*
* @return bool True if the plugin was deactivated, false otherwise.
*/
public function deactivate_plugin_by_slug( $slug ) {
// Get all installed plugins.
$all_plugins = $this->wp_get_plugins();
// Loop through all plugins to find the one with the specified slug.
foreach ( $all_plugins as $plugin_path => $plugin_info ) {
// Check if the plugin path contains the slug.
if ( strpos( $plugin_path, $slug . '/' ) === 0 ) {
// Deactivate the plugin.
deactivate_plugins( $plugin_path );
// Check if the plugin has been deactivated.
if ( ! is_plugin_active( $plugin_path ) ) {
return true;
}
}
}
return false;
}
}

View File

@@ -0,0 +1,287 @@
<?php
namespace Automattic\WooCommerce\Blueprint;
/**
* Trait UseWPFunctions
*/
trait UseWPFunctions {
/**
* Whether the filesystem has been initialized.
*
* @var bool
*/
private $filesystem_initialized = false;
/**
* Adds a filter to a specified tag.
*
* @param string $tag The name of the filter to hook the $function_to_add to.
* @param callable $function_to_add The callback to be run when the filter is applied.
* @param int $priority Optional. Used to specify the order in which the functions
* associated with a particular action are executed. Default 10.
* @param int $accepted_args Optional. The number of arguments the function accepts. Default 1.
*/
public function wp_add_filter( $tag, $function_to_add, $priority = 10, $accepted_args = 1 ) {
add_filter( $tag, $function_to_add, $priority, $accepted_args );
}
/**
* Adds an action to a specified tag.
*
* @param string $tag The name of the action to hook the $function_to_add to.
* @param callable $function_to_add The callback to be run when the action is triggered.
* @param int $priority Optional. Used to specify the order in which the functions
* associated with a particular action are executed. Default 10.
* @param int $accepted_args Optional. The number of arguments the function accepts. Default 1.
*/
public function wp_add_action( $tag, $function_to_add, $priority = 10, $accepted_args = 1 ) {
add_action( $tag, $function_to_add, $priority, $accepted_args );
}
/**
* Calls the functions added to a filter hook.
*
* @param string $tag The name of the filter hook.
* @param mixed $value The value on which the filters hooked to $tag are applied on.
* @return mixed The filtered value after all hooked functions are applied to it.
*/
// phpcs:ignore
public function wp_apply_filters( $tag, $value ) {
$args = func_get_args();
return call_user_func_array( 'apply_filters', $args );
}
/**
* Executes the functions hooked on a specific action hook.
*
* @param string $tag The name of the action to be executed.
* @param mixed ...$args Optional. Additional arguments which are passed on to the functions hooked to the action.
*/
public function wp_do_action( $tag, ...$args ) {
// phpcs:ignore
do_action( $tag, ...$args );
}
/**
* Checks if a plugin is active.
*
* @param string $plugin Path to the plugin file relative to the plugins directory.
* @return bool True if the plugin is active, false otherwise.
*/
public function wp_is_plugin_active( string $plugin ) {
if ( ! function_exists( 'is_plugin_active' ) || ! function_exists( 'get_plugins' ) ) {
require_once ABSPATH . 'wp-admin/includes/plugin.php';
}
return is_plugin_active( $plugin );
}
/**
* Retrieves plugin information from the WordPress Plugin API.
*
* @param string $action The type of information to retrieve from the API.
* @param array $args Optional. Arguments to pass to the API.
* @return object|WP_Error The API response object or WP_Error on failure.
*/
public function wp_plugins_api( $action, $args = array() ) {
if ( ! function_exists( 'plugins_api' ) ) {
require_once ABSPATH . 'wp-admin/includes/plugin-install.php';
}
return plugins_api( $action, $args );
}
/**
* Retrieves all plugins.
*
* @param string $plugin_folder Optional. Path to the plugin folder to scan.
* @return array Array of plugins.
*/
public function wp_get_plugins( string $plugin_folder = '' ) {
if ( ! function_exists( 'get_plugins' ) ) {
require_once ABSPATH . 'wp-admin/includes/plugin.php';
}
return get_plugins( $plugin_folder );
}
/**
* Retrieves all themes.
*
* @param array $args Optional. Arguments to pass to the API.
* @return array Array of themes.
*/
public function wp_get_themes( $args = array() ) {
return wp_get_themes( $args );
}
/**
* Retrieves a theme.
*
* @param string|null $stylesheet Optional. The theme's stylesheet name.
* @return WP_Theme The theme object.
*/
public function wp_get_theme( $stylesheet = null ) {
return wp_get_theme( $stylesheet );
}
/**
* Retrieves theme information from the WordPress Theme API.
*
* @param string $action The type of information to retrieve from the API.
* @param array $args Optional. Arguments to pass to the API.
* @return object|WP_Error The API response object or WP_Error on failure.
*/
public function wp_themes_api( $action, $args = array() ) {
if ( ! function_exists( 'themes_api' ) ) {
require_once ABSPATH . 'wp-admin/includes/theme.php';
}
return themes_api( $action, $args );
}
/**
* Activates a plugin.
*
* @param string $plugin Path to the plugin file relative to the plugins directory.
* @param string $redirect Optional. URL to redirect to after activation.
* @param bool $network_wide Optional. Whether to enable the plugin for all sites in the network.
* @param bool $silent Optional. Whether to prevent calling activation hooks.
* @return WP_Error|null WP_Error on failure, null on success.
*/
public function wp_activate_plugin( $plugin, $redirect = '', $network_wide = false, $silent = false ) {
if ( ! function_exists( 'activate_plugin' ) ) {
require_once ABSPATH . 'wp-admin/includes/plugin.php';
}
return activate_plugin( $plugin, $redirect, $network_wide, $silent );
}
/**
* Deletes plugins.
*
* @param array $plugins List of plugins to delete.
* @return array|WP_Error Array of results or WP_Error on failure.
*/
public function wp_delete_plugins( $plugins ) {
if ( ! function_exists( 'delete_plugins' ) ) {
require_once ABSPATH . 'wp-admin/includes/plugin.php';
}
return delete_plugins( $plugins );
}
/**
* Updates an option in the database.
*
* @param string $option Name of the option to update.
* @param mixed $value New value for the option.
* @param string|null $autoload Optional. Whether to load the option when WordPress starts up.
* @return bool True if option was updated, false otherwise.
*/
public function wp_update_option( $option, $value, $autoload = null ) {
return update_option( $option, $value, $autoload );
}
/**
* Retrieves an option from the database.
*
* @param string $option Name of the option to retrieve.
* @param mixed $default_value Optional. Default value to return if the option does not exist.
* @return mixed Value of the option or $default if the option does not exist.
*/
public function wp_get_option( $option, $default_value = false ) {
return get_option( $option, $default_value );
}
/**
* Switches the current theme.
*
* @param string $name The name of the theme to switch to.
*/
public function wp_switch_theme( $name ) {
return switch_theme( $name );
}
/**
* Initializes the WordPress filesystem.
*
* @return bool
*/
public function wp_init_filesystem() {
if ( ! $this->filesystem_initialized ) {
if ( ! class_exists( 'WP_Filesystem' ) ) {
require_once ABSPATH . 'wp-admin/includes/file.php';
}
WP_Filesystem();
$this->filesystem_initialized = true;
}
return true;
}
/**
* Unzips a file to a specified location.
*
* @param string $path Path to the ZIP file.
* @param string $to Destination directory.
* @return bool|WP_Error True on success, WP_Error on failure.
*/
public function wp_unzip_file( $path, $to ) {
$this->wp_init_filesystem();
return unzip_file( $path, $to );
}
/**
* Retrieves the upload directory information.
*
* @return array Array of upload directory information.
*/
public function wp_upload_dir() {
return \wp_upload_dir();
}
/**
* Retrieves the root directory of the current theme.
*
* @return string The root directory of the current theme.
*/
public function wp_get_theme_root() {
return \get_theme_root();
}
/**
* Checks if a variable is a WP_Error.
*
* @param mixed $thing Variable to check.
* @return bool True if the variable is a WP_Error, false otherwise.
*/
public function is_wp_error( $thing ) {
return is_wp_error( $thing );
}
/**
* Downloads a file from a URL.
*
* @param string $url The URL of the file to download.
* @return string|WP_Error The local file path on success, WP_Error on failure.
*/
public function wp_download_url( $url ) {
if ( ! function_exists( 'download_url' ) ) {
include ABSPATH . '/wp-admin/includes/file.php';
}
return download_url( $url );
}
/**
* Alias for WP_Filesystem::put_contents().
*
* @param string $file_path The path to the file to write.
* @param mixed $content The data to write to the file.
*
* @return mixed
*/
public function wp_filesystem_put_contents( $file_path, $content ) {
global $wp_filesystem;
$this->wp_init_filesystem();
return $wp_filesystem->put_contents( $file_path, $content );
}
}

View File

@@ -0,0 +1,133 @@
<?php
namespace Automattic\WooCommerce\Blueprint;
use RecursiveArrayIterator;
use RecursiveIteratorIterator;
/**
* Utility functions.
*/
class Util {
/**
* Ensure that the given path is a valid path within the WP_CONTENT_DIR.
*
* @param string $path The path to be validated.
*
* @return string
* @throws \InvalidArgumentException If the path is invalid.
*/
public static function ensure_wp_content_path( $path ) {
$path = realpath( $path );
if ( false === $path || strpos( $path, WP_CONTENT_DIR ) !== 0 ) {
throw new \InvalidArgumentException( "Invalid path: $path" );
}
return $path;
}
/**
* Convert a string from snake_case to camelCase.
*
* @param string $string The string to be converted.
*
* @return string
*/
public static function snake_to_camel( $string ) {
// Split the string by underscores
$words = explode( '_', $string );
// Capitalize the first letter of each word
$words = array_map( 'ucfirst', $words );
// Join the words back together
return implode( '', $words );
}
public static function array_flatten($array) {
return new RecursiveIteratorIterator(new RecursiveArrayIterator($array));
}
/**
* Convert a string from camelCase to snake_case.
*
* @param string $input The string to be converted.
*
* @return string
*/
public static function camel_to_snake( $input ) {
// Replace all uppercase letters with an underscore followed by the lowercase version of the letter
$pattern = '/([a-z])([A-Z])/';
$replacement = '$1_$2';
$snake = preg_replace( $pattern, $replacement, $input );
// Replace spaces with underscores
$snake = str_replace( ' ', '_', $snake );
// Convert the entire string to lowercase
return strtolower( $snake );
}
/**
* Index an array using a callback function.
*
* @param array $array The array to be indexed.
* @param callable $callback The callback function to be called for each element.
*
* @return array
*/
// phpcs:ignore
public static function index_array( $array, $callback ) {
$result = array();
foreach ( $array as $key => $value ) {
$new_key = $callback( $key, $value );
$result[ $new_key ] = $value;
}
return $result;
}
/**
* Check to see if given string is a valid WordPress plugin slug.
*
* @param string $slug The slug to be validated.
*
* @return bool
*/
public static function is_valid_wp_plugin_slug( $slug ) {
// Check if the slug only contains allowed characters.
if ( preg_match( '/^[a-z0-9-]+$/', $slug ) ) {
return true;
}
return false;
}
/**
* Recursively delete a directory.
*
* @param string $dir_path The path to the directory.
*
* @return void
* @throws \InvalidArgumentException If $dir_path is not a directory.
*/
public static function delete_dir( $dir_path ) {
if ( ! is_dir( $dir_path ) ) {
throw new \InvalidArgumentException( "$dir_path must be a directory" );
}
if ( substr( $dir_path, strlen( $dir_path ) - 1, 1 ) !== '/' ) {
$dir_path .= '/';
}
$files = glob( $dir_path . '*', GLOB_MARK );
foreach ( $files as $file ) {
if ( is_dir( $file ) ) {
static::delete_dir( $file );
} else {
// phpcs:ignore
unlink( $file );
}
}
// phpcs:ignore
rmdir( $dir_path );
}
}

View File

@@ -0,0 +1,225 @@
<?php
namespace Automattic\WooCommerce\Blueprint;
use Automattic\WooCommerce\Blueprint\Steps\InstallPlugin;
use Automattic\WooCommerce\Blueprint\Steps\InstallTheme;
/**
* Class ZipExportedSchema
*
* Handles the creation of a ZIP archive from a schema.
*/
class ZipExportedSchema {
use UsePluginHelpers;
use UseWPFunctions;
/**
* Exported schema from ExportSchema class.
*
* @var array
*/
protected array $schema;
/**
* Full path for the ZIP file.
*
* @var string
*/
protected string $destination;
/**
* Base path for working directory.
*
* @var string
*/
protected string $dir;
/**
* Unique directory name for a single session.
*
* @var string
*/
protected string $working_dir;
/**
* Array of files to be included in the ZIP archive.
*
* @var array
*/
protected array $files;
/**
* Constructor.
*
* @param array $schema Exported schema array.
* @param string|null $destination Optional. Path to the destination ZIP file.
*/
public function __construct( $schema, $destination = null ) {
$this->schema = $schema;
$this->dir = $this->get_default_destination_dir();
$this->destination = null === $destination ? $this->dir . '/woo-blueprint.zip' : Util::ensure_wp_content_path( $destination );
$this->working_dir = $this->dir . '/' . gmdate( 'Ymd' ) . '_' . time();
if ( ! class_exists( 'PclZip' ) ) {
require_once ABSPATH . 'wp-admin/includes/class-pclzip.php';
}
}
/**
* Returns the full path for a file in the working directory.
*
* @param string $filename The name of the file.
* @return string Full path to the file.
*/
protected function get_working_dir_path( $filename ) {
return $this->working_dir . '/' . $filename;
}
/**
* Creates a directory if it does not exist.
*
* @param string $dir Directory path.
*/
protected function maybe_create_dir( $dir ) {
if ( ! is_dir( $dir ) ) {
// phpcs:ignore
mkdir( $dir, 0777, true );
}
}
/**
* Creates a ZIP archive of the schema and resources.
*
* @return string Path to the created ZIP file.
* @throws \Exception If there is an error creating the ZIP archive.
*/
public function zip() {
$this->maybe_create_dir( $this->working_dir );
// Create .json file.
$this->files[] = $this->create_json_schema_file();
$this->files = array_merge( $this->files, $this->add_resource( InstallPlugin::get_step_name(), 'plugins' ) );
$this->files = array_merge( $this->files, $this->add_resource( InstallTheme::get_step_name(), 'themes' ) );
$archive = new \PclZip( $this->destination );
if ( $archive->create( $this->files, PCLZIP_OPT_REMOVE_PATH, $this->working_dir ) === 0 ) {
throw new \Exception( 'Error : ' . $archive->errorInfo( true ) );
}
$this->clean();
return $this->destination;
}
/**
* Returns the default destination directory for the ZIP file.
*
* @return string Default destination directory path.
*/
protected function get_default_destination_dir() {
return WP_CONTENT_DIR . '/uploads/blueprint';
}
/**
* Finds steps in the schema matching the given step name.
*
* @param string $step_name Name of the step to find.
* @return array|null Array of matching steps, or null if none found.
*/
protected function find_steps( $step_name ) {
$steps = array_filter(
$this->schema['steps'],
function ( $step ) use ( $step_name ) {
return $step['step'] === $step_name;
}
);
if ( count( $steps ) ) {
return $steps;
}
return null;
}
/**
* Adds resources to the list of files for the ZIP archive.
*
* @param string $step Step name to find resources for.
* @param string $type Type of resources ('plugins' or 'themes').
* @return array Array of file paths to include in the ZIP archive.
*
* @throws \Exception If there is an error creating the ZIP archive.
* @throws \InvalidArgumentException If the given slug is not a valid plugin or theme.
*/
protected function add_resource( $step, $type ) {
$steps = $this->find_steps( $step );
if ( null === $steps ) {
return array();
}
$steps = array_filter(
$steps,
// phpcs:ignore
function ( $resource ) use ( $type ) {
if ( 'plugins' === $type ) {
return 'self/plugins' === $resource['pluginZipFile']['resource'];
} elseif ( 'themes' === $type ) {
return 'self/themes' === $resource['themeZipFile']['resource'];
}
return false;
}
);
if ( count( $steps ) === 0 ) {
return array();
}
// Create 'plugins' or 'themes' directory.
$this->maybe_create_dir( $this->working_dir . '/' . $type );
$files = array();
foreach ( $steps as $step ) {
$resource = $step[ 'plugins' === $type ? 'pluginZipFile' : 'themeZipFile' ];
if ( ! $this->is_plugin_dir( $resource['slug'] ) ) {
throw new \InvalidArgumentException( 'Invalid plugin slug: ' . $resource['slug'] );
}
$destination = $this->working_dir . '/' . $type . '/' . $resource['slug'] . '.zip';
$plugin_dir = WP_CONTENT_DIR . '/' . $type . '/' . $resource['slug'];
if ( ! is_dir( $plugin_dir ) ) {
$plugin_dir = $plugin_dir . '.php';
if ( ! file_exists( $plugin_dir ) ) {
continue;
}
}
$archive = new \PclZip( $destination );
$result = $archive->create( $plugin_dir, PCLZIP_OPT_REMOVE_PATH, WP_CONTENT_DIR . '/' . $type );
if ( 0 === $result ) {
throw new \Exception( $archive->errorInfo( true ) );
}
$files[] = $destination;
}
return $files;
}
/**
* Creates a JSON file from the schema.
*
* @return string Path to the created JSON schema file.
*/
private function create_json_schema_file() {
$schema_file = $this->get_working_dir_path( 'woo-blueprint.json' );
$this->wp_filesystem_put_contents( $schema_file, json_encode( $this->schema, JSON_PRETTY_PRINT ) );
return $schema_file;
}
/**
* Cleans up the working directory by deleting it.
*/
private function clean() {
Util::delete_dir( $this->working_dir );
}
}

View File

@@ -0,0 +1,4 @@
{
"step": "activatePlugin",
"pluginName": "woocommerce"
}

View File

@@ -0,0 +1,4 @@
{
"step": "deactivatePlugin",
"pluginName": "woocommerce"
}

View File

@@ -0,0 +1,4 @@
{
"step": "deletePlugin",
"pluginName": "woocommerce"
}

View File

@@ -0,0 +1,10 @@
{
"step": "installPlugin",
"pluginZipFile": {
"resource": "wordpress.org/plugins",
"slug": "akismet"
},
"options": {
"activate": true
}
}

View File

@@ -0,0 +1,10 @@
{
"step": "installTheme",
"themeZipFile": {
"resource": "wordpress.org/themes",
"slug": "twentytwenty"
},
"options": {
"activate": true
}
}

View File

@@ -0,0 +1,6 @@
{
"step": "setSiteOptions",
"options": {
"woocommerce_allow_tracking": true
}
}

View File

@@ -0,0 +1,24 @@
<?php
namespace Automattic\WooCommerce\Blueprint\Tests;
use PHPUnit\Framework\TestCase as PHPUnitTestCase;
require_once __DIR__ . '/helpers.php';
/**
* Class TestCase
*/
abstract class TestCase extends PHPUnitTestCase {
/**
* Get the path to a fixture file.
*
* @param string $filename The filename.
*
* @return string The path to the fixture file.
*/
public function get_fixture_path( $filename ) {
return __DIR__ . '/fixtures/' . $filename;
}
}

View File

@@ -0,0 +1,96 @@
<?php
namespace Automattic\WooCommerce\Blueprint\Tests\Unit;
use Automattic\WooCommerce\Blueprint\BuiltInExporters;
use Automattic\WooCommerce\Blueprint\ExportSchema;
use Automattic\WooCommerce\Blueprint\Tests\stubs\Exporters\EmptySetSiteOptionsExporter;
use Automattic\WooCommerce\Blueprint\Tests\TestCase;
use Mockery;
use Mockery\Mock;
/**
* Class ExportSchemaTest
*/
class ExportSchemaTest extends TestCase {
/**
* Get a mock of the ExportSchema class.
*
* @param boolean $partial Whether to make the mock partial.
*
* @return ExportSchema|Mockery\MockInterface&Mockery\LegacyMockInterface
*/
public function get_mock( $partial = false ) {
$mock = Mock( ExportSchema::class );
if ( $partial ) {
$mock->makePartial();
}
return $mock;
}
/**
* Test that it uses exporters passed to the constructor
* with the built-in exporters.
*/
public function test_it_uses_exporters_passed_to_the_constructor() {
$empty_exporter = new EmptySetSiteOptionsExporter();
$mock = Mock( ExportSchema::class, array( array( $empty_exporter ) ) );
$built_in_exporters = ( new BuiltInExporters() )->get_all();
$mock->makePartial();
// Make sure wooblueprint_exporters filter passes the empty exporter + built-in exporters.
// and then return only the empty exporter to test that it is used.
// We're removing the built-in exporters as some of them make network calls.
$mock->shouldReceive( 'wp_apply_filters' )
->with( 'wooblueprint_exporters', array_merge( array( $empty_exporter ), $built_in_exporters ) )
->andReturn( array( $empty_exporter ) );
$result = $mock->export();
$this->assertCount( 1, $result['steps'] );
$this->assertEquals( 'setSiteOptions', $result['steps'][0]['step'] );
$this->assertEquals( array(), $result['steps'][0]['options'] );
}
/**
* Test that it correctly sets landingPage value from the filter.
*/
public function test_wooblueprint_export_landingpage_filter() {
$exporter = $this->get_mock( true );
$exporter->shouldReceive( 'wp_apply_filters' )
->with( 'wooblueprint_exporters', Mockery::any() )
->andReturn( array() );
$exporter->shouldReceive( 'wp_apply_filters' )
->with( 'wooblueprint_export_landingpage', Mockery::any() )
->andReturn( 'test' );
$result = $exporter->export();
$this->assertEquals( 'test', $result['landingPage'] );
}
/**
* Test that it uses the exporters from the filter.
*
* @return void
*/
public function test_wooblueprint_exporters_filter() {
}
/**
* Test that it filters out exporters that are not in the list of steps to export.
*
* @return void
*/
public function test_it_only_uses_exporters_specified_by_steps_argment() {
}
/**
* Test that it calls include_private_plugins method on ExportInstallPluginSteps when
* exporting a zip schema.
*
* @return void
*/
public function test_it_calls_include_private_plugins_for_zip_export() {
}
}

View File

@@ -0,0 +1,48 @@
<?php
namespace Automattic\WooCommerce\Blueprint\Tests\Unit\Schemas;
use Automattic\WooCommerce\Blueprint\Schemas\JsonSchema;
use Automattic\WooCommerce\Blueprint\Tests\TestCase;
/**
* Class JsonSchemaTest
*/
class JsonSchemaTest extends TestCase {
/**
* Test getting steps from a schema.
*
* @return void
*/
public function test_get_steps() {
$schema = new JsonSchema( $this->get_fixture_path( 'empty-steps.json' ) );
$steps = $schema->get_steps();
$this->assertIsArray( $steps );
$this->assertCount( 0, $steps );
}
/**
* Test getting a step from a schema.
*
* @return void
*/
public function test_get_step() {
$name = 'installPlugin';
$schema = new JsonSchema( $this->get_fixture_path( 'with-install-plugin-step.json' ) );
$steps = $schema->get_step( $name );
$this->assertIsArray( $steps );
foreach ( $steps as $step ) {
$this->assertEquals( $name, $step->step );
}
}
/**
* Test getting a step from a schema that does not exist.
*
* @return void
*/
public function test_it_throws_invalid_argument_exception_with_invalid_json() {
$this->expectException( \InvalidArgumentException::class );
new JsonSchema( $this->get_fixture_path( 'invalid-json.json' ) );
}
}

View File

@@ -0,0 +1,34 @@
<?php
namespace Automattic\WooCommerce\Blueprint\Tests\Unit\Schemas;
use Automattic\WooCommerce\Blueprint\Schemas\ZipSchema;
use Automattic\WooCommerce\Blueprint\Tests\TestCase;
/**
* Class ZipSchemaTest
*/
class ZipSchemaTest extends TestCase {
/**
* Test it throws exception on invalid zip file.
*
* @return void
* @throws \Exception If the zip file is invalid.
*/
public function test_it_throws_exception_on_unzip_failure() {
$this->expectException( \Exception::class );
new ZipSchema( $this->get_fixture_path( 'invalid-zip.zip' ) );
}
/**
* Test unzipping a zip file.
*
* @return void
* @throws \Exception If the zip file is invalid.
*/
public function test_unzip() {
$schema = new ZipSchema( $this->get_fixture_path( 'zipped-schema.zip' ) );
$unzipped_path = $schema->get_unzipped_path();
$this->assertEquals( wp_upload_dir()['path'], $unzipped_path );
}
}

View File

@@ -0,0 +1,29 @@
<?php
namespace Automattic\WooCommerce\Blueprint\Tests\Unit;
use Automattic\WooCommerce\Blueprint\Tests\TestCase;
use Automattic\WooCommerce\Blueprint\ZipExportedSchema;
/**
* Class ZipExportedSchemaTest
*/
class ZipExportedSchemaTest extends TestCase {
/**
* Test it throws exception on invalid plugin slug.
*
* @return void
* @throws \Exception If the plugin slug is invalid.
*/
public function test_it_throws_invalid_argument_exception_with_invalid_slug() {
$this->expectException( \InvalidArgumentException::class );
// phpcs:ignore
$json = json_decode( file_get_contents( $this->get_fixture_path( 'install-plugin-with-invalid-slug.json' ) ), true );
$mock = Mock( ZipExportedSchema::class, array( $json ) );
$mock->makePartial();
$mock->shouldAllowMockingProtectedMethods();
$mock->shouldReceive( 'maybe_create_dir' )->andReturn( null );
$mock->shouldReceive( 'wp_filesystem_put_contents' )->andReturn( null );
$mock->zip();
}
}

View File

@@ -0,0 +1,21 @@
<?php
/**
* PHPUnit bootstrap file.
*
* @package Starter_Plugin
*/
require_once dirname( __DIR__ ) . '/vendor/autoload.php';
$_tests_dir = getenv( 'WP_TESTS_DIR' );
// Forward custom PHPUnit Polyfills configuration to PHPUnit bootstrap file.
$_phpunit_polyfills_path = getenv( 'WP_TESTS_PHPUNIT_POLYFILLS_PATH' );
if ( false !== $_phpunit_polyfills_path ) {
define( 'WP_TESTS_PHPUNIT_POLYFILLS_PATH', $_phpunit_polyfills_path );
}
require 'vendor/yoast/phpunit-polyfills/phpunitpolyfills-autoload.php';
// Start up the WP testing environment.
require "{$_tests_dir}/includes/bootstrap.php";

View File

@@ -0,0 +1,3 @@
{
"steps": []
}

View File

@@ -0,0 +1,14 @@
{
"steps": [
{
"step": "installPlugin",
"pluginZipFile": {
"resource": "self\/plugins",
"slug": "woocommerce1"
},
"options": {
"activate": true
}
}
]
}

View File

@@ -0,0 +1,3 @@
{
"invalid": "should not have comma",
}

View File

@@ -0,0 +1,24 @@
{
"steps": [
{
"step": "installPlugin",
"pluginZipFile": {
"resource": "wordpress.org\/plugins",
"slug": "woocommerce"
},
"options": {
"activate": true
}
},
{
"step": "installPlugin",
"pluginZipFile": {
"resource": "wordpress.org\/plugins",
"slug": "code-snippets"
},
"options": {
"activate": true
}
}
]
}

View File

@@ -0,0 +1,13 @@
<?php
/**
* Dump and die.
*
* @param mixed $x The variable to dump.
*
* @return void
*/
function dd( $x ) {
print_r( $x );
exit;
}

View File

@@ -0,0 +1,31 @@
<?php
namespace Automattic\WooCommerce\Blueprint\Tests\stubs\Exporters;
use Automattic\WooCommerce\Blueprint\Exporters\StepExporter;
use Automattic\WooCommerce\Blueprint\Steps\SetSiteOptions;
/**
* Class EmptySetSiteOptionsExporter
*
* Exports an empty SetSiteOptions step for testing.
*/
class EmptySetSiteOptionsExporter implements StepExporter {
/**
* Export the step.
*
* @return SetSiteOptions
*/
public function export() {
return new SetSiteOptions( array() );
}
/**
* Get the step name.
*
* @return string The step name.
*/
public function get_step_name() {
return SetSiteOptions::get_step_name();
}
}