first commit

This commit is contained in:
User A0264400
2026-04-01 23:20:16 +03:00
commit a766acdc90
23071 changed files with 4933189 additions and 0 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,57 @@
{
"name": "woocommerce/blueprint",
"version": "0.0.1",
"type": "wordpress-plugin",
"autoload": {
"psr-4": {
"Automattic\\WooCommerce\\Blueprint\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"Automattic\\WooCommerce\\Blueprint\\Tests\\": "tests/"
}
},
"require": {
"opis/json-schema": "^2.3"
},
"scripts": {
"test:setup": "wp-env start",
"test:unit": "wp-env run tests-cli --env-cwd=wp-content/plugins/blueprint ./vendor/bin/phpunit",
"phpcs": [
"phpcs -s -p"
],
"phpcbf": [
"phpcbf -p"
]
},
"require-dev": {
"phpunit/phpunit": "^9",
"mockery/mockery": "^1.6",
"automattic/jetpack-changelogger": "3.3.0",
"woocommerce/woocommerce-sniffs": "^1.0.0",
"yoast/phpunit-polyfills": "^2.0"
},
"extra": {
"changelogger": {
"formatter": {
"filename": "../../../tools/changelogger/class-legacy-core-formatter.php"
},
"types": {
"fix": "Fixes an existing bug",
"add": "Adds functionality",
"update": "Update existing functionality",
"dev": "Development related task",
"tweak": "A minor adjustment to the codebase",
"performance": "Address performance issues",
"enhancement": "Improve existing functionality"
},
"changelog": "changelog.md"
}
},
"config": {
"allow-plugins": {
"dealerdirect/phpcodesniffer-composer-installer": true
}
}
}

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,63 @@
<?php
namespace Automattic\WooCommerce\Blueprint;
use Automattic\WooCommerce\Blueprint\Importers\ImportActivatePlugin;
use Automattic\WooCommerce\Blueprint\Importers\ImportActivateTheme;
use Automattic\WooCommerce\Blueprint\Importers\ImportInstallPlugin;
use Automattic\WooCommerce\Blueprint\Importers\ImportInstallTheme;
use Automattic\WooCommerce\Blueprint\Importers\ImportRunSql;
use Automattic\WooCommerce\Blueprint\Importers\ImportSetSiteOptions;
use Automattic\WooCommerce\Blueprint\ResourceStorages\OrgPluginResourceStorage;
use Automattic\WooCommerce\Blueprint\ResourceStorages\OrgThemeResourceStorage;
/**
* Class BuiltInStepProcessors
*
* @package Automattic\WooCommerce\Blueprint
*/
class BuiltInStepProcessors {
/**
* BuiltInStepProcessors constructor.
*/
public function __construct() {
}
/**
* 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 ImportActivatePlugin(),
new ImportActivateTheme(),
new ImportRunSql(),
);
}
/**
* 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() );
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() );
return new ImportInstallTheme( $storage );
}
}

View File

@@ -0,0 +1,196 @@
<?php
namespace Automattic\WooCommerce\Blueprint;
/**
* Class ClassExtractor
*
* Provides functionality to manipulate PHP class files by replacing variables,
* adding prefixes, and removing strict types declarations.
*
* This class is used to generate 'code' part for runPHP step from a template file.
*/
class ClassExtractor {
/**
* Path to the PHP file being processed.
*
* @var string
*/
private string $file_path;
/**
* Whether the file contains a strict types declaration.
*
* @var bool
*/
private bool $has_strict_types_declaration = false;
/**
* PHP code to prefix to the final output.
*
* @var string
*/
private string $prefix = '';
/**
* Replacements for class variables.
*
* @var array
*/
private array $class_variable_replacements = array();
/**
* Replacements for method variables.
*
* @var array
*/
private array $method_variable_replacements = array();
/**
* Constructor.
*
* @param string $file_path Path to the PHP file to process.
*
* @throws \InvalidArgumentException If the file does not exist.
*/
public function __construct( string $file_path ) {
if ( ! file_exists( $file_path ) ) {
throw new \InvalidArgumentException( "File not found: $file_path" );
}
$this->file_path = $file_path;
}
/**
* Adds a prefix to include the WordPress wp-load.php file.
*
* @return $this
*/
public function with_wp_load() {
$this->prefix .= "<?php require_once 'wordpress/wp-load.php'; ";
return $this;
}
/**
* Replaces a class variable with a new value.
*
* @param string $variable_name Name of the class variable.
* @param mixed $new_value The new value to assign to the variable.
*
* @return $this
*/
public function replace_class_variable( $variable_name, $new_value ) {
$this->class_variable_replacements[ $variable_name ] = $new_value;
return $this;
}
/**
* Replaces a variable inside a method with a new value.
*
* @param string $method_name Name of the method.
* @param string $variable_name Name of the variable to replace.
* @param mixed $new_value The new value to assign to the variable.
*
* @return $this
*/
public function replace_method_variable( $method_name, $variable_name, $new_value ) {
$this->method_variable_replacements[] = array(
'method' => $method_name,
'variable' => $variable_name,
'value' => $new_value,
);
return $this;
}
/**
* Generates the processed PHP code with applied replacements and prefixes.
*
* @return string The modified PHP code.
*/
public function get_code() {
// Security check: Check if we can replace this with a more secure function.
$file_content = file_get_contents( $this->file_path ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
$file_content = preg_replace( '/<\?php\s*/', '', $file_content );
if ( preg_match( '/declare\s*\(\s*strict_types\s*=\s*1\s*\)\s*;/', $file_content ) ) {
$this->has_strict_types_declaration = true;
$file_content = preg_replace( '/declare\s*\(\s*strict_types\s*=\s*1\s*\)\s*;/', '', $file_content );
}
$file_content = preg_replace( '/\/\*.*?\*\/|\/\/.*?(?=\r?\n)/s', '', $file_content );
foreach ( $this->class_variable_replacements as $variable => $value ) {
$file_content = $this->apply_class_variable_replacement( $file_content, $variable, $value );
}
foreach ( $this->method_variable_replacements as $replacement ) {
$file_content = $this->apply_variable_replacement(
$file_content,
$replacement['method'],
$replacement['variable'],
$replacement['value']
);
}
return $this->prefix . trim( $file_content );
}
/**
* Applies a replacement to a class variable in the file content.
*
* @param string $file_content The content of the PHP file.
* @param string $variable_name The name of the variable to replace.
* @param mixed $new_value The new value for the variable.
*
* @return string The updated file content.
*/
private function apply_class_variable_replacement( $file_content, $variable_name, $new_value ) {
// Security check: Check if it's necessary to use var_export.
$replacement_value = var_export( $new_value, true ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_var_export
$pattern = '/(protected|private|public)\s+\$' . preg_quote( $variable_name, '/' ) . '\s*=\s*.*?;|'
. '(protected|private|public)\s+\$' . preg_quote( $variable_name, '/' ) . '\s*;?/';
$replacement = "$1 \$$variable_name = $replacement_value;";
return preg_replace( $pattern, $replacement, $file_content, 1 );
}
/**
* Applies a replacement to a variable in a specific method.
*
* @param string $file_content The content of the PHP file.
* @param string $method_name The name of the method containing the variable.
* @param string $variable_name The name of the variable to replace.
* @param mixed $new_value The new value for the variable.
*
* @return string The updated file content.
*/
private function apply_variable_replacement( $file_content, $method_name, $variable_name, $new_value ) {
$pattern = '/function\s+' . preg_quote( $method_name, '/' ) . '\s*\([^)]*\)\s*\{\s*(.*?)\s*\}/s';
if ( preg_match( $pattern, $file_content, $matches ) ) {
$method_body = $matches[1];
// Security check: Check if it's necessary to use var_export.
$new_value_exported = var_export( $new_value, true ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_var_export
$variable_pattern = '/\$' . preg_quote( $variable_name, '/' ) . '\s*=\s*[^;]+;/';
$replacement = '$' . $variable_name . ' = ' . $new_value_exported . ';';
$updated_method_body = preg_replace( $variable_pattern, $replacement, $method_body, 1 );
if ( null !== $updated_method_body ) {
$file_content = str_replace( $method_body, $updated_method_body, $file_content );
}
}
return $file_content;
}
/**
* Checks if the file has a strict types declaration.
*
* @return bool True if the file has a strict types declaration, false otherwise.
*/
public function has_strict_type_declaration() {
return $this->has_strict_types_declaration;
}
}

View File

@@ -0,0 +1,87 @@
<?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();
if ( isset( $assoc_args['steps'] ) ) {
$steps = array_map(
function ( $step ) {
return trim( $step );
},
explode( ',', $assoc_args['steps'] )
);
}
$export->run(
array(
'steps' => $steps,
'format' => 'json',
)
);
},
array(
'synopsis' => array(
array(
'type' => 'positional',
'name' => 'save-to',
'optional' => false,
),
array(
'type' => 'assoc',
'name' => 'steps',
'optional' => true,
),
),
'when' => 'after_wp_load',
)
);
}
}

View File

@@ -0,0 +1,60 @@
<?php
namespace Automattic\WooCommerce\Blueprint\Cli;
use Automattic\WooCommerce\Blueprint\ExportSchema;
use Automattic\WooCommerce\Blueprint\UseWPFunctions;
/**
* Class ExportCli
*
* This class handles the CLI commands for exporting schemas.
*
* @package Automattic\WooCommerce\Blueprint\Cli
*/
class ExportCli {
use UseWPFunctions;
/**
* 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() ) {
if ( ! isset( $args['steps'] ) ) {
$args['steps'] = array();
}
$exporter = new ExportSchema();
$result = $exporter->export( $args['steps'] );
if ( is_wp_error( $result ) ) {
\WP_CLI::error( $result->get_error_message() );
return;
}
$is_saved = $this->wp_filesystem_put_contents( $this->save_to, wp_json_encode( $result, JSON_PRETTY_PRINT ) );
if ( false === $is_saved ) {
\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,58 @@
<?php
namespace Automattic\WooCommerce\Blueprint\Cli;
use Automattic\WooCommerce\Blueprint\ImportSchema;
use Automattic\WooCommerce\Blueprint\ResultFormatters\CliResultFormatter;
/**
* 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 ) {
try {
$blueprint = ImportSchema::create_from_file( $this->schema_path );
} catch ( \Exception $e ) {
\WP_CLI::error( $e->getMessage() );
return;
}
$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,159 @@
<?php
namespace Automattic\WooCommerce\Blueprint;
use Automattic\WooCommerce\Blueprint\Exporters\StepExporter;
use Automattic\WooCommerce\Blueprint\Exporters\HasAlias;
use Automattic\WooCommerce\Blueprint\Logger;
use Automattic\WooCommerce\Blueprint\Steps\Step;
use WP_Error;
/**
* Class ExportSchema
*
* Handles the export schema functionality for WooCommerce.
*
* @package Automattic\WooCommerce\Blueprint
*/
class ExportSchema {
use UseWPFunctions;
use UsePubSub;
/**
* 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.
*
* @return array|WP_Error The exported schema array or a WP_Error if the export fails.
*/
public function export( $steps = array() ) {
$loading_page_path = $this->wp_apply_filters( 'wooblueprint_export_landingpage', '/' );
/**
* Validate that the landing page path is a valid relative local URL path.
*
* Accepts:
* - /
* - /path/to/page
*
* Rejects:
* - http://example.com/path/to/page
* - invalid-path
*/
if ( ! preg_match( '#^/$|^/[^/].*#', $loading_page_path ) ) {
return new WP_Error( 'wooblueprint_invalid_landing_page_path', 'Invalid loading page path.' );
}
$schema = array(
'landingPage' => $loading_page_path,
'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 ) );
// Validate that the exporters are instances of StepExporter.
$exporters = array_filter(
$exporters,
function ( $exporter ) {
return $exporter instanceof StepExporter;
}
);
// 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 ] );
}
}
}
// Make sure the user has the required capabilities to export the steps.
foreach ( $exporters as $exporter ) {
if ( ! $exporter->check_step_capabilities() ) {
return new WP_Error( 'wooblueprint_insufficient_permissions', 'Insufficient permissions to export for step: ' . $exporter->get_step_name() );
}
}
$logger = new Logger();
$logger->start_export( $exporters );
foreach ( $exporters as $exporter ) {
try {
$this->publish( 'onBeforeExport', $exporter );
$step = $exporter->export();
$this->add_result_to_schema( $schema, $step );
} catch ( \Throwable $e ) {
$step_name = $exporter instanceof HasAlias ? $exporter->get_alias() : $exporter->get_step_name();
$logger->export_step_failed( $step_name, $e );
return new WP_Error( 'wooblueprint_export_step_failed', 'Export step failed: ' . $e->getMessage() );
}
}
$logger->complete_export( $exporters );
return $schema;
}
/**
* Subscribe to the onBeforeExport event.
*
* @param string $step_name The step name to subscribe to.
* @param callable $callback The callback to execute.
*/
public function on_before_export( $step_name, $callback ) {
$this->subscribe(
'onBeforeExport',
function ( $exporter ) use ( $step_name, $callback ) {
if ( $step_name === $exporter->get_step_name() ) {
$callback( $exporter );
}
}
);
}
/**
* Add export result to the schema array.
*
* @param array $schema Schema array to add steps to.
* @param array|Step $step Step or array of steps to add.
*/
private function add_result_to_schema( array &$schema, $step ): void {
if ( is_array( $step ) ) {
foreach ( $step as $_step ) {
$schema['steps'][] = $_step->get_json_array();
}
return;
}
$schema['steps'][] = $step->get_json_array();
}
}

View File

@@ -0,0 +1,169 @@
<?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;
/**
* Filter callback.
*
* @var callable
*/
private $filter_callback;
/**
* 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;
}
/**
* Register a filter callback to filter the plugins to export.
*
* @param callable $callback Filter callback.
*
* @return void
*/
public function filter( callable $callback ) {
$this->filter_callback = $callback;
}
/**
* Export the steps required to install plugins.
*
* @return array The array of InstallPlugin steps.
*/
public function export() {
$plugins = $this->sort_plugins_by_dep( $this->wp_get_plugins() );
if ( is_callable( $this->filter_callback ) ) {
$plugins = call_user_func( $this->filter_callback, $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;
}
$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;
}
/**
* Sort plugins by dependencies -- put the dependencies at the top.
*
* @param array $plugins List of plugins to sort (from wp_get_plugins function).
*
* @return array
*/
public function sort_plugins_by_dep( array $plugins ) {
$sorted = array();
$visited = array();
// Create a mapping of lowercase titles to plugin keys for quick lookups.
$title_map = array_reduce(
array_keys( $plugins ),
function ( $carry, $key ) use ( $plugins ) {
$title = strtolower( $plugins[ $key ]['Title'] ?? '' );
if ( $title ) {
$carry[ $title ] = $key;
}
return $carry;
},
array()
);
// Recursive function for topological sort.
$visit = function ( $plugin_key ) use ( &$visit, &$sorted, &$visited, $plugins, $title_map ) {
if ( isset( $visited[ $plugin_key ] ) ) {
return;
}
$visited[ $plugin_key ] = true;
$requires = $plugins[ $plugin_key ]['RequiresPlugins'] ?? array();
foreach ( (array) $requires as $dependency ) {
$dependency_key = $title_map[ strtolower( $dependency ) ] ?? null;
if ( $dependency_key ) {
$visit( $dependency_key );
}
}
$sorted[ $plugin_key ] = $plugins[ $plugin_key ];
};
// Perform sort for each plugin.
foreach ( array_keys( $plugins ) as $plugin_key ) {
$visit( $plugin_key );
}
return $sorted;
}
/**
* Get the name of the step.
*
* @return string The step name.
*/
public function get_step_name() {
return InstallPlugin::get_step_name();
}
/**
* Check if the current user has the required capabilities for this step.
*
* @return bool True if the user has the required capabilities. False otherwise.
*/
public function check_step_capabilities(): bool {
return current_user_can( 'activate_plugins' );
}
}

View File

@@ -0,0 +1,92 @@
<?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;
/**
* Filter callback.
*
* @var callable
*/
private $filter_callback;
/**
* Register a filter callback to filter the plugins to export.
*
* @param callable $callback Filter callback.
*
* @return void
*/
public function filter( callable $callback ) {
$this->filter_callback = $callback;
}
/**
* Export the steps.
*
* @return array
*/
public function export() {
$steps = array();
$themes = $this->wp_get_themes();
if ( is_callable( $this->filter_callback ) ) {
$themes = call_user_func( $this->filter_callback, $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();
}
/**
* Check if the current user has the required capabilities for this step.
*
* @return bool True if the user has the required capabilities. False otherwise.
*/
public function check_step_capabilities(): bool {
return current_user_can( 'switch_themes' );
}
}

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,34 @@
<?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();
/**
* Check if the current user has the required capabilities for this step.
*
* @return bool True if the user has the required capabilities. False otherwise.
*/
public function check_step_capabilities(): bool;
}

View File

@@ -0,0 +1,101 @@
<?php
namespace Automattic\WooCommerce\Blueprint;
use Automattic\WooCommerce\Blueprint\Schemas\JsonSchema;
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;
/**
* 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;
}
/**
* 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.
*
* @throws \RuntimeException If the JSON file cannot be read.
* @throws \InvalidArgumentException If the JSON is invalid or missing 'steps' field.
*/
public static function create_from_file( $file ) {
return 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.
*
* @throws \RuntimeException If the JSON file cannot be read.
* @throws \InvalidArgumentException If the JSON is invalid or missing 'steps' field.
*/
public static function create_from_json( $json_path ) {
return new self( new JsonSchema( $json_path ) );
}
/**
* Import the schema steps.
*
* @return StepProcessorResult[]
*/
public function import() {
$results = array();
$result = StepProcessorResult::success( 'ImportSchema' );
$results[] = $result;
foreach ( $this->schema->get_steps() as $step_schema ) {
$step_importer = new ImportStep( $step_schema, $this->validator );
$results[] = $step_importer->import();
}
return $results;
}
}

View File

@@ -0,0 +1,168 @@
<?php
namespace Automattic\WooCommerce\Blueprint;
use Opis\JsonSchema\Errors\ErrorFormatter;
use Opis\JsonSchema\Validator;
use Automattic\WooCommerce\Blueprint\Logger;
/**
* Class ImportStep
*
* Import a single step from a JSON definition.
*
* @package Automattic\WooCommerce\Blueprint
*/
class ImportStep {
use UseWPFunctions;
/**
* Step definition.
*
* @var object The step definition.
*/
private object $step_definition;
/**
* 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;
/**
* Importers.
*
* @var array|mixed The importers.
*/
private array $importers;
/**
* Indexed importers.
*
* @var array The indexed importers by step name.
*/
private array $indexed_importers;
/**
* ImportStep constructor.
*
* @param object $step_definition The step definition.
* @param Validator|null $validator The validator instance, optional.
*/
public function __construct( $step_definition, ?Validator $validator = null ) {
$this->step_definition = $step_definition;
if ( null === $validator ) {
$validator = new Validator();
}
$this->validator = $validator;
$this->importers = $this->wp_apply_filters( 'wooblueprint_importers', ( ( new BuiltInStepProcessors() )->get_all() ) );
$this->indexed_importers = Util::index_array(
$this->importers,
function ( $key, $importer ) {
return $importer->get_step_class()::get_step_name();
}
);
}
/**
* Import the schema steps.
*
* @return StepProcessorResult
*/
public function import() {
$result = StepProcessorResult::success( $this->step_definition->step );
if ( ! $this->can_import( $result ) ) {
return $result;
}
$importer = $this->indexed_importers[ $this->step_definition->step ];
$logger = new Logger();
$logger->start_import( $this->step_definition->step, get_class( $importer ) );
$importer_result = $importer->process( $this->step_definition );
if ( $importer_result->is_success() ) {
$logger->complete_import( $this->step_definition->step, $importer_result );
} else {
$logger->import_step_failed( $this->step_definition->step, $importer_result );
}
$result->merge_messages( $importer_result );
return $result;
}
/**
* Check if the step can be imported.
*
* @param StepProcessorResult $result The result object to add messages to.
*
* @return bool True if the step can be imported, false otherwise.
*/
protected function can_import( &$result ) {
// Check if the importer exists.
if ( ! isset( $this->indexed_importers[ $this->step_definition->step ] ) ) {
$result->add_error( 'Unable to find an importer' );
return false;
}
$importer = $this->indexed_importers[ $this->step_definition->step ];
// Validate importer is a step processor before processing.
if ( ! $importer instanceof StepProcessor ) {
$result->add_error( 'Incorrect importer type' );
return false;
}
// Validate steps schemas before processing.
if ( ! $this->validate_step_schemas( $importer, $result ) ) {
$result->add_error( 'Schema validation failed for step' );
return false;
}
// Validate step capabilities before processing.
if ( ! $importer->check_step_capabilities( $this->step_definition ) ) {
$result->add_error( 'User does not have the required capabilities to run step' );
return false;
}
return true;
}
/**
* Validate the step schemas.
*
* @param StepProcessor $importer The importer.
* @param StepProcessorResult $result The result object to add messages to.
*
* @return bool True if the step schemas are valid, false otherwise.
*/
protected function validate_step_schemas( StepProcessor $importer, StepProcessorResult $result ) {
$step_schema = call_user_func( array( $importer->get_step_class(), 'get_schema' ) );
$validate = $this->validator->validate( $this->step_definition, wp_json_encode( $step_schema ) );
if ( ! $validate->isValid() ) {
$result->add_error( "Schema validation failed for step {$this->step_definition->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 ) );
return false;
}
return true;
}
}

View File

@@ -0,0 +1,59 @@
<?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() );
// Not snake case because it's a property of the schema.
$plugin_path = $schema->pluginPath; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
$activate = $this->wp_activate_plugin( $plugin_path );
if ( $this->is_wp_error( $activate ) ) {
$result->add_error( "Unable to activate {$plugin_path}." );
} else {
$result->add_info( "Activated {$plugin_path}." );
}
return $result;
}
/**
* Get the step class.
*
* @return string
*/
public function get_step_class(): string {
return ActivatePlugin::class;
}
/**
* Check if the current user has the required capabilities for this step.
*
* @param object $schema The schema to process.
*
* @return bool True if the user has the required capabilities. False otherwise.
*/
public function check_step_capabilities( $schema ): bool {
return current_user_can( 'activate_plugins' );
}
}

View File

@@ -0,0 +1,62 @@
<?php
namespace Automattic\WooCommerce\Blueprint\Importers;
use Automattic\WooCommerce\Blueprint\StepProcessor;
use Automattic\WooCommerce\Blueprint\StepProcessorResult;
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;
$this->wp_switch_theme( $name );
$current_theme = $this->wp_get_theme()->get_stylesheet();
if ( $current_theme === $name ) {
$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;
}
/**
* Check if the current user has the required capabilities for this step.
*
* @param object $schema The schema to process.
*
* @return bool True if the user has the required capabilities. False otherwise.
*/
public function check_step_capabilities( $schema ): bool {
return current_user_can( 'switch_themes' );
}
}

View File

@@ -0,0 +1,185 @@
<?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->pluginData;
// We only support CorePluginReference at the moment.
if ( 'wordpress.org/plugins' !== $plugin->resource ) {
$result->add_info( "Skipped installing a plugin. Unsupported resource type. Only 'wordpress.org/plugins' is supported at the moment." );
return $result;
}
// If the plugin is already installed, skip the installation.
if ( isset( $installed_plugins[ $plugin->slug ] ) ) {
$result->add_info( "Skipped installing {$plugin->slug}. It is already installed." );
return $result;
}
// If the resource type is not supported, return an error.
if ( $this->storage->is_supported_resource( $plugin->resource ) === false ) {
$result->add_error( "Invalid resource type for {$plugin->slug}." );
return $result;
}
// Download the plugin.
$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 the plugin.
$install = $this->install( $downloaded_path );
if ( is_wp_error( $install ) ) {
$result->add_error( "Failed to install {$plugin->slug}." );
return $result;
}
$result->add_info( "Installed {$plugin->slug}." );
// If the plugin should be activated, activate it.
$should_activate = isset( $schema->options, $schema->options->activate ) && true === $schema->options->activate;
if ( $should_activate ) {
$activate = $this->activate( $plugin->slug );
if ( $activate instanceof \WP_Error ) {
$result->add_error( "Failed to activate {$plugin->slug}." );
return $result;
}
$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|WP_Error True on success, WP_Error 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;
}
/**
* Check if the current user has the required capabilities for this step.
*
* @param object $schema The schema to process.
*
* @return bool True if the user has the required capabilities. False otherwise.
*/
public function check_step_capabilities( $schema ): bool {
return current_user_can( 'install_plugins' );
}
}

View File

@@ -0,0 +1,157 @@
<?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;
/**
* 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->themeData;
if ( 'wordpress.org/themes' !== $theme->resource ) {
$this->result->add_info( "Skipped installing a theme. Unsupported resource type. Only 'wordpress.org/themes' is supported at the moment." );
return $this->result;
}
if ( ! isset( $schema->options ) ) {
$schema->options = new \stdClass();
}
if ( isset( $installed_themes[ $theme->slug ] ) ) {
$this->activate_theme( $schema );
$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'." );
}
$this->activate_theme( $schema );
return $this->result;
}
/**
* Attempt to activate the theme if the schema specifies to do so.
*
* @param object $schema installTheme schema.
*
* @return void
*/
protected function activate_theme( $schema ) {
// phpcs:ignore
$theme = $schema->themeData;
if ( isset( $schema->options->activate ) && true === $schema->options->activate ) {
$this->wp_switch_theme( $theme->slug );
$current_theme = $this->wp_get_theme()->get_stylesheet();
if ( $current_theme === $theme->slug ) {
$this->result->add_info( "Switched theme to '$theme->slug'." );
} else {
$this->result->add_error( "Failed to switch theme to '$theme->slug'." );
}
}
}
/**
* Install the theme from the local path.
*
* @param string $local_path The local path of the theme to be installed.
*
* @return bool True if the installation was successful, false otherwise.
*/
protected function install( $local_path ) {
$unzip_result = $this->wp_unzip_file( $local_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;
}
/**
* Check if the current user has the required capabilities for this step.
*
* @param object $schema The schema to process.
*
* @return bool True if the user has the required capabilities. False otherwise.
*/
public function check_step_capabilities( $schema ): bool {
return current_user_can( 'install_themes' );
}
}

View File

@@ -0,0 +1,311 @@
<?php
namespace Automattic\WooCommerce\Blueprint\Importers;
use Automattic\WooCommerce\Blueprint\StepProcessor;
use Automattic\WooCommerce\Blueprint\StepProcessorResult;
use Automattic\WooCommerce\Blueprint\Steps\RunSql;
use Automattic\WooCommerce\Blueprint\UsePluginHelpers;
use Automattic\WooCommerce\Blueprint\UseWPFunctions;
/**
* Processes SQL execution steps in the Blueprint.
*
* Handles the execution of SQL queries with safety checks to prevent
* unauthorized modifications to sensitive WordPress data.
*
* @package Automattic\WooCommerce\Blueprint\Importers
*/
class ImportRunSql implements StepProcessor {
use UsePluginHelpers;
use UseWPFunctions;
/**
* List of allowed SQL query types.
*
* @var array
*/
private const ALLOWED_QUERY_TYPES = array(
'INSERT',
'UPDATE',
'REPLACE INTO',
);
/**
* Process the SQL execution step.
*
* Validates and executes the SQL query while ensuring:
* 1. Only allowed query types are executed
* 2. No modifications to admin users or roles
* 3. No unauthorized changes to user capabilities
*
* @param object $schema The schema containing the SQL query to execute.
* @return StepProcessorResult The result of the SQL execution.
*/
public function process( $schema ): StepProcessorResult {
global $wpdb;
$result = StepProcessorResult::success( RunSql::get_step_name() );
$sql = trim( $schema->sql->contents );
// Check if the query type is allowed.
if ( ! $this->is_allowed_query_type( $sql ) ) {
$result->add_error(
sprintf(
'Only %s queries are allowed.',
implode( ', ', self::ALLOWED_QUERY_TYPES )
)
);
return $result;
}
// Check for SQL comments that might be hiding malicious code.
if ( $this->contains_suspicious_comments( $sql ) ) {
$result->add_error( 'SQL query contains suspicious comment patterns.' );
return $result;
}
// Detect SQL injection patterns.
if ( $this->contains_sql_injection_patterns( $sql ) ) {
$result->add_error( 'SQL query contains potential injection patterns.' );
return $result;
}
// Check if the query affects protected tables.
if ( $this->affects_protected_tables( $sql ) ) {
$result->add_error( 'Modifications to admin users or roles are not allowed.' );
return $result;
}
// Check if the query affects user capabilities in wp_options.
if ( $this->affects_user_capabilities( $sql ) ) {
$result->add_error( 'Modifications to user roles or capabilities are not allowed.' );
return $result;
}
$wpdb->suppress_errors( true );
$wpdb->query( 'START TRANSACTION' );
try {
$query_result = $wpdb->query( $sql ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
$last_error = $wpdb->last_error;
if ( $last_error ) {
$wpdb->query( 'ROLLBACK' );
$result->add_error( 'Error executing SQL: ' . $last_error );
} else {
$wpdb->query( 'COMMIT' );
$result->add_debug( "Executed SQL ({$schema->sql->name}): Affected {$query_result} rows" );
}
} catch ( \Throwable $e ) {
$wpdb->query( 'ROLLBACK' );
$result->add_error( "Exception executing SQL: {$e->getMessage()}" );
}
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 RunSql::class;
}
/**
* Check if the current user has the required capabilities for this step.
*
* @param object $schema The schema to process.
*
* @return bool True if the user has the required capabilities. False otherwise.
*/
public function check_step_capabilities( $schema ): bool {
if ( ! current_user_can( 'manage_options' ) ) {
return false;
}
if ( ! current_user_can( 'edit_posts' ) ) {
return false;
}
if ( ! current_user_can( 'edit_users' ) ) {
return false;
}
return true;
}
/**
* Check if the SQL query type is allowed.
*
* @param string $sql_content The SQL query to check.
* @return bool True if the query type is allowed, false otherwise.
*/
private function is_allowed_query_type( string $sql_content ): bool {
$uppercase_sql_content = strtoupper( trim( $sql_content ) );
foreach ( self::ALLOWED_QUERY_TYPES as $query_type ) {
if ( 0 === stripos( $uppercase_sql_content, $query_type ) ) {
return true;
}
}
return false;
}
/**
* Check for suspicious comment patterns that might hide malicious code.
*
* This method detects various types of SQL comments that might be used
* to hide malicious SQL commands or bypass security filters.
*
* @param string $sql_content The SQL query to check.
* @return bool True if suspicious comments found, false otherwise.
*/
private function contains_suspicious_comments( string $sql_content ): bool {
// Quick check if there are any comments at all before running regex.
if (
strpos( $sql_content, '--' ) === false &&
strpos( $sql_content, '/*' ) === false &&
strpos( $sql_content, '#' ) === false
) {
return false;
}
// List of potentially dangerous SQL commands to check for in comments.
$dangerous_commands = array(
'DELETE',
'DROP',
'ALTER',
'CREATE',
'TRUNCATE',
'GRANT',
'REVOKE',
'EXEC',
'EXECUTE',
'CALL',
'INTO OUTFILE',
'INTO DUMPFILE',
'LOAD_FILE',
'LOAD DATA',
'BENCHMARK',
'SLEEP',
'INFORMATION_SCHEMA',
'USER\\(',
'DATABASE\\(',
'SCHEMA\\(',
);
$dangerous_pattern = implode( '|', $dangerous_commands );
// Check for SQL comments that might be hiding malicious code.
$patterns = array(
// Single-line comments (-- style) containing dangerous commands.
'/--.*?(' . $dangerous_pattern . ')/i',
// Single-line comments (# style) containing dangerous commands.
'/#.*?(' . $dangerous_pattern . ')/i',
// Multi-line comments hiding dangerous commands.
'/\/\*.*?(' . $dangerous_pattern . ').*?\*\//is',
// MySQL-specific execution comments (version-specific code execution).
'/\/\*![0-9]*.*?\*\//',
);
foreach ( $patterns as $pattern ) {
if ( preg_match( $pattern, $sql_content ) ) {
return true;
}
}
return false;
}
/**
* Check for common SQL injection patterns.
*
* @param string $sql_content The SQL query to check.
* @return bool True if potential injection patterns found, false otherwise.
*/
private function contains_sql_injection_patterns( string $sql_content ): bool {
$patterns = array(
'/UNION\s+(?:ALL\s+)?SELECT/i', // UNION-based injections.
'/OR\s+1\s*=\s*1/i', // OR 1=1 condition.
'/AND\s+0\s*=\s*0/i', // AND 0=0 condition.
'/;\s*--/i', // Inline comment terminations.
'/SLEEP\s*\(/i', // Time-based injections.
'/BENCHMARK\s*\(/i', // Benchmark-based injections.
'/LOAD_FILE\s*\(/i', // File access.
'/INTO\s+OUTFILE/i', // File write.
'/INTO\s+DUMPFILE/i', // File dump.
'/CREATE\s+(?:TEMPORARY\s+)?TABLE/i', // Table creation.
'/DROP\s+TABLE/i', // Table deletion.
'/ALTER\s+TABLE/i', // Table alteration.
'/INFORMATION_SCHEMA/i', // Database metadata access.
'/EXEC\s*\(/i', // Stored procedure execution.
'/SCHEMA_NAME/i', // Schema access.
'/DATABASE\(\)/i', // Current database name.
'/CHR\s*\(/i', // Character function for evasion.
'/CHAR\s*\(/i', // Character function for evasion.
'/FROM\s+mysql\./i', // Direct MySQL system table access.
'/FROM\s+information_schema\./i', // Direct information schema access.
);
foreach ( $patterns as $pattern ) {
if ( preg_match( $pattern, $sql_content ) ) {
return true;
}
}
return false;
}
/**
* Check if the SQL query affects protected user tables.
*
* @param string $sql_content The SQL query to check.
* @return bool True if the query affects protected tables, false otherwise.
*/
private function affects_protected_tables( string $sql_content ): bool {
global $wpdb;
$protected_tables = array(
$wpdb->users,
$wpdb->usermeta,
);
foreach ( $protected_tables as $table ) {
if ( preg_match( '/\b' . preg_quote( $table, '/' ) . '\b/i', $sql_content ) ) {
return true;
}
}
return false;
}
/**
* Check if the SQL query affects user capabilities in wp_options.
*
* @param string $sql_content The SQL query to check.
* @return bool True if the query affects user capabilities, false otherwise.
*/
private function affects_user_capabilities( string $sql_content ): bool {
global $wpdb;
// Check if the query affects user capabilities in wp_options.
if ( stripos( $sql_content, $wpdb->prefix . 'options' ) !== false ) {
$option_patterns = array(
'user_roles',
'capabilities',
'wp_user_',
'role_',
'administrator',
);
foreach ( $option_patterns as $pattern ) {
if ( stripos( $sql_content, $pattern ) !== false ) {
return true;
}
}
}
return false;
}
}

View File

@@ -0,0 +1,98 @@
<?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;
/**
* List of WordPress options that should not be modified.
*
* @var array<string>
*/
private const RESTRICTED_OPTIONS = array(
'siteurl',
'home',
'active_plugins',
'template',
'stylesheet',
'admin_email',
'unfiltered_html',
'users_can_register',
'default_role',
'db_version',
'cron',
'rewrite_rules',
'wp_user_roles',
);
/**
* 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 ) {
// Skip if the option should not be modified.
if ( in_array( $key, self::RESTRICTED_OPTIONS, true ) ) {
$result->add_warn( "Cannot modify '{$key}' option: Modifying is restricted for this key." );
continue;
}
$value = json_decode( wp_json_encode( $value ), true );
$updated = $this->wp_update_option( $key, $value );
$current_value = $this->wp_get_option( $key );
if ( $current_value !== $value ) {
$result->add_warn( "{$key} was intended to be set, but the stored value may have been overridden by a hook." );
continue;
}
if ( $updated ) {
$result->add_info( "{$key} has been updated." );
continue;
}
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;
}
/**
* Check if the current user has the required capabilities for this step.
*
* @param object $schema The schema to process.
*
* @return bool True if the user has the required capabilities. False otherwise.
*/
public function check_step_capabilities( $schema ): bool {
return current_user_can( 'manage_options' );
}
}

View File

@@ -0,0 +1,169 @@
<?php
namespace Automattic\WooCommerce\Blueprint;
use Automattic\WooCommerce\Blueprint\UseWPFunctions;
/**
* Class Logger
*/
class Logger {
use UseWPFunctions;
/**
* WooCommerce logger class instance.
*
* @var \WC_Logger_Interface
*/
private $logger;
/**
* Constructor.
*/
public function __construct() {
$this->logger = wc_get_logger();
}
/**
* Log a message as a debug log entry.
*
* @param string $message The message to log.
* @param string $level The log level.
* @param array $context The context of the log.
*/
public function log( string $message, string $level = \WC_Log_Levels::DEBUG, $context = array() ) {
$this->logger->log(
$level,
$message,
array_merge(
array(
'source' => 'wc-blueprint',
'user_id' => $this->wp_get_current_user_id(),
),
$context
)
);
}
/**
* Log the start of an export operation.
*
* @param array $exporters Array of exporters.
*/
public function start_export( array $exporters ) {
$export_data = $this->get_export_data( $exporters );
$this->log(
sprintf( 'Starting export of %d steps', count( $export_data['steps'] ) ),
\WC_Log_Levels::INFO,
array(
'steps' => $export_data['steps'],
'exporters' => $export_data['exporters'],
)
);
}
/**
* Log the completion of an export operation.
*
* @param array $exporters Array of exporters.
*/
public function complete_export( array $exporters ) {
$export_data = $this->get_export_data( $exporters );
$this->log(
sprintf( 'Export of %d steps completed', count( $export_data['steps'] ) ),
\WC_Log_Levels::INFO,
array(
'steps' => $export_data['steps'],
'exporters' => $export_data['exporters'],
)
);
}
/**
* Extract export step names and exporter classes from exporters.
*
* @param array $exporters Array of exporters.
* @return array Associative array with 'steps' and 'exporters' keys.
*/
private function get_export_data( array $exporters ) {
$export_steps = array();
$exporter_classes = array();
foreach ( $exporters as $exporter ) {
$step_name = method_exists( $exporter, 'get_alias' ) ? $exporter->get_alias() : $exporter->get_step_name();
$export_steps[] = $step_name;
$exporter_classes[] = get_class( $exporter );
}
return array(
'steps' => $export_steps,
'exporters' => $exporter_classes,
);
}
/**
* Log an export step failure.
*
* @param string $step_name The name of the step that failed.
* @param \Throwable $exception The exception that was thrown.
*/
public function export_step_failed( string $step_name, \Throwable $exception ) {
$this->log(
sprintf( 'Export "%s" step failed', $step_name ),
\WC_Log_Levels::ERROR,
array(
'error' => $exception->getMessage(),
)
);
}
/**
* Log the start of an import step.
*
* @param string $step_name The name of the step being imported.
* @param string $importer_class The class name of the importer.
*/
public function start_import( string $step_name, string $importer_class ) {
$this->log(
sprintf( 'Starting import "%s" step', $step_name ),
\WC_Log_Levels::INFO,
array(
'importer' => $importer_class,
)
);
}
/**
* Log the successful completion of an import step.
*
* @param string $step_name The name of the step that was imported.
* @param StepProcessorResult $result The result of the import.
*/
public function complete_import( string $step_name, StepProcessorResult $result ) {
$this->log(
sprintf( 'Import "%s" step completed', $step_name ),
\WC_Log_Levels::INFO,
array(
'messages' => $result->get_messages( 'info' ),
)
);
}
/**
* Log an import step failure.
*
* @param string $step_name The name of the step that failed.
* @param StepProcessorResult $result The result of the import.
*/
public function import_step_failed( string $step_name, StepProcessorResult $result ) {
$this->log(
sprintf( 'Import "%s" step failed', $step_name ),
\WC_Log_Levels::ERROR,
array(
'messages' => $result->get_messages( 'error' ),
)
);
}
}

View File

@@ -0,0 +1,66 @@
<?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_type The resource type to check.
*
* @return bool
*/
public function is_supported_resource( $resource_type ) {
return isset( $this->storages[ $resource_type ] );
}
/**
* Download the resource.
*
* @param string $slug The slug of the resource to download.
* @param string $resource_type The resource type to download.
*
* @return false|string
*/
public function download( $slug, $resource_type ) {
if ( ! isset( $this->storages[ $resource_type ] ) ) {
return false;
}
$storages = $this->storages[ $resource_type ];
foreach ( $storages as $storage ) {
$found = $storage->download( $slug );
if ( $found ) {
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,86 @@
<?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|false The path to the downloaded plugin file, or false on failure.
*/
public function download( $slug ): ?string {
$download_link = $this->get_download_link( $slug );
if ( ! $download_link ) {
return false;
}
$result = $this->download_url( $download_link );
if ( is_wp_error( $result ) ) {
return false;
}
return $result;
}
/**
* Download the file from the given URL.
*
* @param string $url The URL to download the file from.
*
* @return string|WP_Error The path to the downloaded file, or WP_Error 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_wp_error( $info ) ) {
return null;
}
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,46 @@
<?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 ( is_wp_error( $info ) ) {
return null;
}
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,75 @@
<?php
namespace Automattic\WooCommerce\Blueprint\ResultFormatters;
use Automattic\WooCommerce\Blueprint\StepProcessorResult;
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
*
* @throws \Exception If WP CLI Utils is not found.
*/
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_exist = function_exists( '\WP_CLI\Utils\format_items' );
if ( ! $format_items_exist ) {
throw new \Exception( 'WP CLI Utils not found' );
}
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,71 @@
<?php
namespace Automattic\WooCommerce\Blueprint\ResultFormatters;
use Automattic\WooCommerce\Blueprint\StepProcessorResult;
/**
* 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,95 @@
<?php
namespace Automattic\WooCommerce\Blueprint\Schemas;
use Automattic\WooCommerce\Blueprint\UseWPFunctions;
/**
* Class JsonSchema
*/
class JsonSchema {
use UseWPFunctions;
/**
* The schema.
*
* @var object The schema.
*/
protected $schema;
/**
* JsonSchema constructor.
*
* @param string $json_path The path to the JSON file.
*
* @throws \RuntimeException If the JSON file cannot be read.
* @throws \InvalidArgumentException If the JSON is invalid or missing 'steps' field.
*/
public function __construct( $json_path ) {
$real_path = realpath( $json_path );
if ( false === $real_path ) {
throw new \InvalidArgumentException( 'Invalid schema path' );
}
$contents = $this->wp_filesystem_get_contents( $real_path );
if ( false === $contents ) {
throw new \RuntimeException( "Failed to read the JSON file at {$real_path}." );
}
$schema = json_decode( $contents );
$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 ) || ! is_array( $this->schema->steps ) ) {
return false;
}
return true;
}
}

View File

@@ -0,0 +1,32 @@
<?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;
/**
* Check if the current user has the required capabilities for this step.
*
* @param object $schema The schema to process.
*
* @return bool True if the user has the required capabilities. False otherwise.
*/
public function check_step_capabilities( $schema ): bool;
}

View File

@@ -0,0 +1,179 @@
<?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', 'warn' );
/**
* 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' );
}
/**
* Merge messages from another StepProcessorResult instance.
*
* @param StepProcessorResult $other The other StepProcessorResult instance.
*
* @return void
*/
public function merge_messages( StepProcessorResult $other ) {
$this->messages = array_merge( $this->messages, $other->get_messages() );
}
/**
* 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' );
}
/**
* Add a new warn message.
*
* @param string $message message.
*
* @return void
*/
public function add_warn( string $message ) {
$this->add_message( $message, 'warn' );
}
/**
* 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,87 @@
<?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;
/**
* The path to the plugin file relative to the plugins directory.
*
* @var string The path to the plugin file relative to the plugins directory.
*/
private string $plugin_path;
/**
* ActivatePlugin constructor.
*
* @param string $plugin_path Path to the plugin file relative to the plugins directory.
* @param string $plugin_name The name of the plugin to be activated.
*/
public function __construct( $plugin_path, $plugin_name = '' ) {
$this->plugin_name = $plugin_name;
$this->plugin_path = $plugin_path;
}
/**
* 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',
),
'pluginPath' => array(
'type' => 'string',
),
),
'required' => array( 'step', 'pluginPath' ),
);
}
/**
* Prepares an associative array for JSON encoding.
*
* @return array Array of data to be encoded as JSON.
*/
public function prepare_json_array(): array {
$data = array(
'step' => static::get_step_name(),
'pluginPath' => $this->plugin_path,
);
if ( ! empty( $this->plugin_name ) ) {
$data['pluginName'] = $this->plugin_name;
}
return $data;
}
}

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_folder_name;
/**
* ActivateTheme constructor.
*
* @param string $theme_folder_name The name of the theme to be activated.
*/
public function __construct( $theme_folder_name ) {
$this->theme_folder_name = $theme_folder_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() ),
),
'themeFolderName' => array(
'type' => 'string',
),
),
'required' => array( 'step', 'themeFolderName' ),
);
}
/**
* 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(),
'themeFolderName' => $this->theme_folder_name,
);
}
}

View File

@@ -0,0 +1,112 @@
<?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(),
'pluginData' => 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() ),
),
'pluginData' => array(
'anyOf' => array(
require __DIR__ . '/schemas/definitions/VFSReference.php',
require __DIR__ . '/schemas/definitions/LiteralReference.php',
require __DIR__ . '/schemas/definitions/CorePluginReference.php',
require __DIR__ . '/schemas/definitions/CoreThemeReference.php',
require __DIR__ . '/schemas/definitions/UrlReference.php',
require __DIR__ . '/schemas/definitions/GitDirectoryReference.php',
require __DIR__ . '/schemas/definitions/DirectoryLiteralReference.php',
),
),
'options' => array(
'type' => 'object',
'properties' => array(
'activate' => array(
'type' => 'boolean',
),
),
),
),
'required' => array( 'step', 'pluginData' ),
);
}
/**
* 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,112 @@
<?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(),
'themeData' => 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() ),
),
'themeData' => array(
'anyOf' => array(
require __DIR__ . '/schemas/definitions/VFSReference.php',
require __DIR__ . '/schemas/definitions/LiteralReference.php',
require __DIR__ . '/schemas/definitions/CorePluginReference.php',
require __DIR__ . '/schemas/definitions/CoreThemeReference.php',
require __DIR__ . '/schemas/definitions/UrlReference.php',
require __DIR__ . '/schemas/definitions/GitDirectoryReference.php',
require __DIR__ . '/schemas/definitions/DirectoryLiteralReference.php',
),
),
'options' => array(
'type' => 'object',
'properties' => array(
'activate' => array(
'type' => 'boolean',
),
),
),
),
'required' => array( 'step', 'themeData' ),
);
}
/**
* 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,95 @@
<?php
namespace Automattic\WooCommerce\Blueprint\Steps;
/**
* Class RunSql
*
* @package Automattic\WooCommerce\Blueprint\Steps
*/
class RunSql extends Step {
/**
* Sql code to run.
*
* @var string
*/
protected string $sql = '';
/**
* Name of the sql file.
*
* @var string
*/
protected string $name = 'schema.sql';
/**
* Constructor.
*
* @param string $sql Sql code to run.
* @param string $name Name of the sql file.
*/
public function __construct( string $sql, $name = 'schema.sql' ) {
$this->sql = $sql;
$this->name = $name;
}
/**
* Returns the name of this step.
*
* @return string The step name.
*/
public static function get_step_name(): string {
return 'runSql';
}
/**
* 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() ),
),
'sql' => array(
'type' => 'object',
'required' => array( 'contents', 'resource', 'name' ),
'properties' => array(
'resource' => array(
'type' => 'string',
'enum' => array( 'literal' ),
),
'name' => array(
'type' => 'string',
),
'contents' => array(
'type' => 'string',
),
),
),
),
'required' => array( 'step', 'sql' ),
);
}
/**
* 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(),
'sql' => array(
'resource' => 'literal',
'name' => $this->name,
'contents' => $this->sql,
),
);
}
}

View File

@@ -0,0 +1,66 @@
<?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() ),
),
),
'required' => array( 'step' ),
);
}
/**
* 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' => (object) $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,21 @@
<?php
return array(
'type' => 'object',
'properties' => array(
'resource' => array(
'type' => 'string',
'const' => 'wordpress.org/plugins',
'description' => 'Identifies the file resource as a WordPress Core plugin',
),
'slug' => array(
'type' => 'string',
'description' => 'The slug of the WordPress Core plugin',
),
),
'required' => array(
'resource',
'slug',
),
'additionalProperties' => false,
);

View File

@@ -0,0 +1,21 @@
<?php
return array(
'type' => 'object',
'properties' => array(
'resource' => array(
'type' => 'string',
'const' => 'wordpress.org/themes',
'description' => 'Identifies the file resource as a WordPress Core theme',
),
'slug' => array(
'type' => 'string',
'description' => 'The slug of the WordPress Core theme',
),
),
'required' => array(
'resource',
'slug',
),
'additionalProperties' => false,
);

View File

@@ -0,0 +1,24 @@
<?php
return array(
'type' => 'object',
'additionalProperties' => false,
'properties' => array(
'resource' => array(
'type' => 'string',
'const' => 'literal:directory',
'description' => 'Identifies the file resource as a git directory',
),
'files' => array(
'$ref' => '#/definitions/FileTree',
),
'name' => array(
'type' => 'string',
),
),
'required' => array(
'files',
'name',
'resource',
),
);

View File

@@ -0,0 +1,31 @@
<?php
return array(
'type' => 'object',
'properties' => array(
'resource' => array(
'type' => 'string',
'const' => 'git:directory',
'description' => 'Identifies the file resource as a git directory',
),
'url' => array(
'type' => 'string',
'description' => 'The URL of the git repository',
),
'ref' => array(
'type' => 'string',
'description' => 'The branch of the git repository',
),
'path' => array(
'type' => 'string',
'description' => 'The path to the directory in the git repository',
),
),
'required' => array(
'resource',
'url',
'ref',
'path',
),
'additionalProperties' => false,
);

View File

@@ -0,0 +1,69 @@
<?php
return array(
'type' => 'object',
'properties' => array(
'resource' => array(
'type' => 'string',
'const' => 'literal',
'description' => 'Identifies the file resource as a literal file',
),
'name' => array(
'type' => 'string',
'description' => 'The name of the file',
),
'contents' => array(
'anyOf' => array(
array(
'type' => 'string',
),
array(
'type' => 'object',
'properties' => array(
'BYTES_PER_ELEMENT' => array(
'type' => 'number',
),
'buffer' => array(
'type' => 'object',
'properties' => array(
'byteLength' => array(
'type' => 'number',
),
),
'required' => array(
'byteLength',
),
'additionalProperties' => false,
),
'byteLength' => array(
'type' => 'number',
),
'byteOffset' => array(
'type' => 'number',
),
'length' => array(
'type' => 'number',
),
),
'required' => array(
'BYTES_PER_ELEMENT',
'buffer',
'byteLength',
'byteOffset',
'length',
),
'additionalProperties' => array(
'type' => 'number',
),
),
),
'description' => 'The contents of the file',
),
),
'required' => array(
'resource',
'name',
'contents',
),
'additionalProperties' => false,
);

View File

@@ -0,0 +1,25 @@
<?php
return array(
'type' => 'object',
'properties' => array(
'resource' => array(
'type' => 'string',
'const' => 'url',
'description' => 'Identifies the file resource as a URL',
),
'url' => array(
'type' => 'string',
'description' => 'The URL of the file',
),
'caption' => array(
'type' => 'string',
'description' => 'Optional caption for displaying a progress message',
),
),
'required' => array(
'resource',
'url',
),
'additionalProperties' => false,
);

View File

@@ -0,0 +1,18 @@
<?php
return array(
'type' => 'object',
'properties' => array(
'resource' => array(
'type' => 'string',
'const' => 'vfs',
'description' => 'Identifies the file resource as Virtual File System (VFS)',
),
'path' => array(
'type' => 'string',
'description' => 'The path to the file in the VFS',
),
),
'required' => array( 'resource', 'path' ),
'additionalProperties' => false,
);

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 false|null|WP_Error Null on success, WP_Error on invalid file, false if not found.
*/
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|WP_Error 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,67 @@
<?php
namespace Automattic\WooCommerce\Blueprint;
trait UsePubSub {
/**
* Subscribers.
*
* @var array
*/
private array $subscribers = array();
/**
* Subscribe to an event with a callback.
*
* @param string $event The event name.
* @param callable $callback The callback to execute when the event is published.
* @return void
*/
public function subscribe( string $event, callable $callback ): void {
if ( ! isset( $this->subscribers[ $event ] ) ) {
$this->subscribers[ $event ] = array();
}
$this->subscribers[ $event ][] = $callback;
}
/**
* Publish an event to all subscribers.
*
* @param string $event The event name.
* @param mixed ...$args Arguments to pass to the callbacks.
* @return void
*/
public function publish( string $event, ...$args ): void {
if ( ! isset( $this->subscribers[ $event ] ) ) {
return;
}
foreach ( $this->subscribers[ $event ] as $callback ) {
call_user_func( $callback, ...$args );
}
}
/**
* Unsubscribe a specific callback from an event.
*
* @param string $event The event name.
* @param callable $callback The callback to remove.
* @return void
*/
public function unsubscribe( string $event, callable $callback ): void {
if ( ! isset( $this->subscribers[ $event ] ) ) {
return;
}
$this->subscribers[ $event ] = array_filter(
$this->subscribers[ $event ],
fn( $subscriber ) => $subscriber !== $callback
);
if ( empty( $this->subscribers[ $event ] ) ) {
unset( $this->subscribers[ $event ] );
}
}
}

View File

@@ -0,0 +1,323 @@
<?php
namespace Automattic\WooCommerce\Blueprint;
use WP_Error;
use WP_Theme;
/**
* 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 ) {
if ( ! function_exists( 'wp_get_theme' ) ) {
require_once ABSPATH . 'wp-admin/includes/theme.php';
}
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|null Array of results or WP_Error on failure, null if filesystem credentials are required to proceed.
*/
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 ) {
if ( ! function_exists( 'switch_theme' ) ) {
require_once ABSPATH . 'wp-admin/includes/theme.php';
}
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';
}
$initialized = WP_Filesystem();
$this->filesystem_initialized = $initialized;
return $initialized;
}
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 bool True on success, false on failure.
*/
public function wp_filesystem_put_contents( $file_path, $content ) {
global $wp_filesystem;
if ( ! $this->wp_init_filesystem() ) {
return false;
}
return $wp_filesystem->put_contents( $file_path, $content );
}
/**
* Alias for WP_Filesystem::get_contents().
*
* @param string $file_path The path to the file to read.
* @return string|false The contents of the file, or false on failure.
*/
public function wp_filesystem_get_contents( $file_path ) {
global $wp_filesystem;
if ( ! $this->wp_init_filesystem() ) {
return false;
}
return $wp_filesystem->get_contents( $file_path );
}
/**
* Retrieves the current user's ID.
*
* @return int The current user's ID.
*/
public function wp_get_current_user_id() {
return get_current_user_id();
}
}

View File

@@ -0,0 +1,167 @@
<?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 an array to an insert SQL query.
*
* @param array $row Array row with key and value.
* @param string $table Name of the table.
* @param string $type One of insert, insert ignore, replace into.
*
* @return false|string
*/
public static function array_to_insert_sql( $row, $table, $type = 'insert ignore' ) {
if ( empty( $row ) || ! is_array( $row ) ) {
return false; // Return false if input data is empty or not an array.
}
$allowed_types = array( 'insert', 'insert ignore', 'replace into' );
if ( ! in_array( $type, $allowed_types, true ) ) {
return false; // Return false if input type is not valid.
}
// Get column names and values.
$columns = '`' . implode( '`, `', array_keys( $row ) ) . '`';
$escaped_values = array_map( fn( $value ) => "'" . addslashes( $value ) . "'", $row );
$values = implode( ', ', $escaped_values );
// Construct final SQL query.
return "{$type} `$table` ($columns) VALUES ($values);";
}
/**
* Convert a string from snake_case to camelCase.
*
* @param string $string_to_convert The string to be converted.
*
* @return string
*/
public static function snake_to_camel( $string_to_convert ) {
// Split the string by underscores.
$words = explode( '_', $string_to_convert );
// Capitalize the first letter of each word.
$words = array_map( 'ucfirst', $words );
// Join the words back together.
return implode( '', $words );
}
/**
* Flatten an array.
*
* @param array $array_to_flatten The array to be flattened.
*
* @return \RecursiveIteratorIterator
*/
public static function array_flatten( $array_to_flatten ) {
return new RecursiveIteratorIterator( new RecursiveArrayIterator( $array_to_flatten ) );
}
/**
* 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,4 @@
{
"step": "activatePlugin",
"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
}
}