Σ(゚Д゚;≡;゚д゚)Σ(゚Д゚;≡;゚д゚)Σ(゚Д゚;≡;゚д゚)始,故人唐宰相鲁公,🆚开府南服,余以布衣从戎。明年,别公漳水湄。后明年,公以事过张睢阳庙及颜杲卿所尝往来处,悲歌慷慨,卒不负其言而从之游。今其诗具在,可考也。😭
class-wc-cli-com-extension-command.php 0000644 00000006317 15233337277 0013763 0 ustar 00 <?php
/**
* WC_CLI_COM_Command class file.
*
* @package WooCommerce\CLI
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
if ( ! class_exists( 'Plugin_Command' ) ) {
exit;
}
/**
* Allows to interact with extensions from WCCOM marketplace via CLI.
*
* @version 6.8
* @package WooCommerce
*/
class WC_CLI_COM_Extension_Command extends Plugin_Command {
/**
* Registers a commands for managing WooCommerce.com extensions.
*/
public static function register_commands() {
WP_CLI::add_command( 'wc com extension', 'WC_CLI_COM_Extension_Command' );
}
/**
* Installs one or more plugins from wccom marketplace.
*
* ## OPTIONS
*
* <extension>...
* : One or more plugins to install. Accepts a plugin slug.
*
* [--force]
* : If set, the command will overwrite any installed version of the plugin, without prompting
* for confirmation.
*
* [--activate]
* : If set, the plugin will be activated immediately after install.
*
* [--activate-network]
* : If set, the plugin will be network activated immediately after install
*
* [--insecure]
* : Retry downloads without certificate validation if TLS handshake fails. Note: This makes the request vulnerable to a MITM attack.
*
* ## EXAMPLES
*
* # Install the latest version from woocommerce.com and activate
* $ wp wc com extension install automatewoo --activate
* Downloading install package from http://s3.amazonaws.com/bucketname/automatewoo.zip?AWSAccessKeyId=123&Expires=456&Signature=abcdef......
* Using cached file '/home/vagrant/.wp-cli/cache/plugin/automatewoo.zip'...
* Unpacking the package...
* Installing the plugin...
* Plugin installed successfully.
* Activating 'automatewoo'...
* Plugin 'automatewoo' activated.
* Success: Installed 1 of 1 plugins.
*
* # Forcefully re-install an installed plugin
* $ wp wc com extension install automatewoo --force
* Downloading install package from http://s3.amazonaws.com/bucketname/automatewoo.zip?AWSAccessKeyId=123&Expires=456&Signature=abcdef...
* Unpacking the package...
* Installing the plugin...
* Removing the old version of the plugin...
* Plugin updated successfully
* Success: Installed 1 of 1 plugins.
*
* @param array $args WP-CLI positional arguments.
* @param array $assoc_args WP-CLI associative arguments.
*/
public function install( $args, $assoc_args ) {
$subscriptions = WC_Helper_Updater::get_available_extensions_downloads_data();
$extension = reset( $args );
$extension_package_url = null;
// Remove `--version` as we don't support it.
unset( $assoc_args['version'] );
// Filter by slug.
foreach ( $subscriptions as $subscription ) {
if ( $subscription['slug'] === $extension && ! is_null( $subscription['package'] ) ) {
$extension_package_url = $subscription['package'];
break;
}
}
// No package found.
if ( is_null( $extension_package_url ) ) {
WP_CLI::warning( sprintf( 'We couldn\'t find a Subscription for \'%s\'', $extension ) );
WP_CLI\Utils\report_batch_operation_results( $this->item_type, 'install', count( $args ), 0, 1 );
return;
}
parent::install( array( $extension_package_url ), $assoc_args );
}
}
class-wc-cli-tracker-command.php 0000644 00000002353 15233337277 0012622 0 ustar 00 <?php
/**
* WC_CLI_Tracker_Command class file.
*
* @package WooCommerce\CLI
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Allows access to tracker snapshot for transparency and debugging.
*
* @since 5.5.0
* @package WooCommerce
*/
class WC_CLI_Tracker_Command {
/**
* Registers a command for showing WooCommerce Tracker snapshot data.
*/
public static function register_commands() {
WP_CLI::add_command( 'wc tracker snapshot', array( 'WC_CLI_Tracker_Command', 'show_tracker_snapshot' ) );
}
/**
* Dump tracker snapshot data to screen.
*
* ## EXAMPLES
*
* wp wc tracker snapshot --format=yaml
* wp wc tracker snapshot --format=json
*
* ## OPTIONS
*
* [--format=<format>]
* : Render output in a particular format, see WP_CLI\Formatter for details.
*
* @see \WP_CLI\Formatter
* @see WC_Tracker::get_tracking_data()
* @param array $args WP-CLI positional arguments.
* @param array $assoc_args WP-CLI associative arguments.
*/
public static function show_tracker_snapshot( $args, $assoc_args ) {
$snapshot_data = WC_Tracker::get_tracking_data();
$formatter = new \WP_CLI\Formatter(
$assoc_args,
array_keys( $snapshot_data )
);
$formatter->display_items( array( $snapshot_data ) );
}
}
class-wc-cli-com-command.php 0000644 00000014327 15233337277 0011751 0 ustar 00 <?php
/**
* WC_CLI_COM_Command class file.
*
* @package WooCommerce\CLI
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Allows to interact with extensions from WCCOM marketplace via CLI.
*
* @version 6.8
* @package WooCommerce
*/
class WC_CLI_COM_Command {
const APPLICATION_PASSWORD_SECTION_URL = 'https://woocommerce.com/my-account/#application-passwords';
/**
* Registers a commands for managing WooCommerce.com extensions.
*/
public static function register_commands() {
WP_CLI::add_command( 'wc com extension list', array( 'WC_CLI_COM_Command', 'list_extensions' ) );
WP_CLI::add_command( 'wc com disconnect', array( 'WC_CLI_COM_Command', 'disconnect' ) );
WP_CLI::add_command( 'wc com connect', array( 'WC_CLI_COM_Command', 'connect' ) );
}
/**
* List extensions owned by the connected site
*
* [--format]
* : If set, the command will use the specified format. Possible values are table, json, csv and yaml. By default the table format will be used.
*
* [--fields]
* : If set, the command will show only the specified fields instead of showing all the fields in the output.
*
* ## EXAMPLES
*
* # List extensions owned by the connected site in table format with all the fields
* $ wp wc com extension list
*
* # List the product slug of the extension owned by the connected site in csv format
* $ wp wc com extension list --format=csv --fields=product_slug
*
* @param array $args WP-CLI positional arguments.
* @param array $assoc_args WP-CLI associative arguments.
*/
public static function list_extensions( array $args, array $assoc_args ) {
$data = WC_Helper::get_subscriptions();
$data = array_values( $data );
$formatter = new \WP_CLI\Formatter(
$assoc_args,
array(
'product_slug',
'product_name',
'auto_renew',
'expires_on',
'expired',
'sites_max',
'sites_active',
'maxed',
)
);
$data = array_map(
function( $item ) {
$product_slug = '';
$product_url_parts = explode( '/', $item['product_url'] );
if ( count( $product_url_parts ) > 2 ) {
$product_slug = $product_url_parts[ count( $product_url_parts ) - 2 ];
}
return array(
'product_slug' => $product_slug,
'product_name' => htmlspecialchars_decode( $item['product_name'] ),
'auto_renew' => $item['autorenew'] ? 'On' : 'Off',
'expires_on' => gmdate( 'Y-m-d', $item['expires'] ),
'expired' => $item['expired'] ? 'Yes' : 'No',
'sites_max' => $item['sites_max'],
'sites_active' => $item['sites_active'],
'maxed' => $item['maxed'] ? 'Yes' : 'No',
);
},
$data
);
$formatter->display_items( $data );
}
/**
* ## OPTIONS
*
* [--yes]
* : Do not prompt for confirmation.
*
* ## EXAMPLES
*
* # Disconnect from site.
* $ wp wc com disconnect
*
* # Disconnect without prompt for confirmation.
* $ wp wc com disconnect --yes
*
* @param array $args Positional arguments to include when calling the command.
* @param array $assoc_args Associative arguments to include when calling the command.
* @return void
* @throws \WP_CLI\ExitException If WP_CLI::$capture_exit is true.
*/
public static function disconnect( array $args, array $assoc_args ) {
if ( ! WC_Helper::is_site_connected() ) {
WP_CLI::error( __( 'Your store is not connected to WooCommerce.com. Run `wp wc com connect` command.', 'woocommerce' ) );
}
WP_CLI::confirm( __( 'Are you sure you want to disconnect your store from WooCommerce.com?', 'woocommerce' ), $assoc_args );
WC_Helper::disconnect();
WP_CLI::success( __( 'You have successfully disconnected your store from WooCommerce.com', 'woocommerce' ) );
}
/**
* Connects to WooCommerce.com with application-password.
*
* [--password]
* : If set, password won't be prompt.
*
* [--force]
* : If set, site will be disconnected and a new connection will be forced.
*
* ## EXAMPLES
*
* # Connect to WCCOM using password.
* $ wp wc com connect
*
* # force connecting to WCCOM even if site is already connected.
* $ wp wc com connect --force
*
* # Pass password to comman.
* $ wp wc com connect --password=PASSWORD
*
* @param array $args Positional arguments to include when calling the command.
* @param array $assoc_args Associative arguments to include when calling the command.
*
* @return void
* @throws \WP_CLI\ExitException If WP_CLI::$capture_exit is true.
*/
public static function connect( array $args, array $assoc_args ) {
$password = \WP_CLI\Utils\get_flag_value( $assoc_args, 'password' );
$force = \WP_CLI\Utils\get_flag_value( $assoc_args, 'force', false );
if ( WC_Helper::is_site_connected() ) {
if ( $force ) {
WC_Helper::disconnect();
} else {
WP_CLI::error( __( 'Your store is already connected.', 'woocommerce' ) );
return;
}
}
if ( empty( $password ) ) {
// translators: %s is the URL for the application-password section in WooCommerce.com.
WP_CLI::log( sprintf( __( 'If you don\'t have an application password (not your account password), generate a password from %s', 'woocommerce' ), esc_url( self::APPLICATION_PASSWORD_SECTION_URL ) ) );
$password = self::ask( __( 'Connection password:', 'woocommerce' ) );
}
$password = sanitize_text_field( $password );
if ( empty( $password ) ) {
// translators: %s is the URL for the application-password section in WooCommerce.com.
WP_CLI::error( sprintf( __( 'Invalid password. Generate a new one from %s.', 'woocommerce' ), esc_url( self::APPLICATION_PASSWORD_SECTION_URL ) ) );
}
$auth = WC_Helper::connect_with_password( $password );
if ( is_wp_error( $auth ) ) {
WP_CLI::error( $auth->get_error_message() );
}
if ( WC_Helper::is_site_connected() ) {
WP_CLI::success( __( 'Store connected successfully.', 'woocommerce' ) );
}
}
/**
* We are asking a question and returning an answer as a string.
*
* @param string $question The question being prompt.
*
* @return string
*/
protected static function ask( $question ) {
// phpcs:disable WordPress.WP.AlternativeFunctions.file_system_read_fwrite
// Adding space to question and showing it.
fwrite( STDOUT, $question . ' ' );
return trim( fgets( STDIN ) );
// phpcs:enable
}
}
class-wc-cli-tool-command.php 0000644 00000005546 15233337277 0012153 0 ustar 00 <?php
/**
* WC_CLI_Tool_Command class file.
*
* @package WooCommerce\CLI
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Hooks up our system status tools to the CLI.
*
* Forked from wp-cli/restful (by Daniel Bachhuber, released under the MIT license https://opensource.org/licenses/MIT).
* https://github.com/wp-cli/restful
*
* @version 3.0.0
* @package WooCommerce
*/
class WC_CLI_Tool_Command {
/**
* Registers just a 'list' and 'run' command to the WC CLI
* since we only want to enable certain actions on the system status
* tools endpoints.
*/
public static function register_commands() {
global $wp_rest_server;
$request = new WP_REST_Request( 'OPTIONS', '/wc/v2/system_status/tools' );
$response = $wp_rest_server->dispatch( $request );
$response_data = $response->get_data();
if ( empty( $response_data ) ) {
return;
}
$parent = 'wc tool';
$supported_commands = array( 'list', 'run' );
foreach ( $supported_commands as $command ) {
$synopsis = array();
if ( 'run' === $command ) {
$synopsis[] = array(
'name' => 'id',
'type' => 'positional',
'description' => __( 'The id for the resource.', 'woocommerce' ),
'optional' => false,
);
$method = 'update_item';
$route = '/wc/v2/system_status/tools/(?P<id>[\w-]+)';
} elseif ( 'list' === $command ) {
$synopsis[] = array(
'name' => 'fields',
'type' => 'assoc',
'description' => __( 'Limit response to specific fields. Defaults to all fields.', 'woocommerce' ),
'optional' => true,
);
$synopsis[] = array(
'name' => 'field',
'type' => 'assoc',
'description' => __( 'Get the value of an individual field.', 'woocommerce' ),
'optional' => true,
);
$synopsis[] = array(
'name' => 'format',
'type' => 'assoc',
'description' => __( 'Render response in a particular format.', 'woocommerce' ),
'optional' => true,
'default' => 'table',
'options' => array(
'table',
'json',
'csv',
'ids',
'yaml',
'count',
'headers',
'body',
'envelope',
),
);
$method = 'list_items';
$route = '/wc/v2/system_status/tools';
}
$before_invoke = null;
if ( empty( $command_args['when'] ) && WP_CLI::get_config( 'debug' ) ) {
$before_invoke = function() {
wc_maybe_define_constant( 'SAVEQUERIES', true );
};
}
$rest_command = new WC_CLI_REST_Command( 'system_status_tool', $route, $response_data['schema'] );
WP_CLI::add_command(
"{$parent} {$command}",
array( $rest_command, $method ),
array(
'synopsis' => $synopsis,
'when' => ! empty( $command_args['when'] ) ? $command_args['when'] : '',
'before_invoke' => $before_invoke,
)
);
}
}
}
class-wc-cli-update-command.php 0000644 00000005102 15233337277 0012444 0 ustar 00 <?php
/**
* WC_CLI_Update_Command class file.
*
* @package WooCommerce\CLI
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Allows updates via CLI.
*
* @version 3.0.0
* @package WooCommerce
*/
class WC_CLI_Update_Command {
/**
* Registers the update command.
*/
public static function register_commands() {
WC()->call_static( WP_CLI::class, 'add_command', 'wc update', array( 'WC_CLI_Update_Command', 'update' ) );
}
/**
* Runs all pending WooCommerce database updates.
*/
public static function update() {
global $wpdb;
$wpdb->hide_errors();
include_once WC_ABSPATH . 'includes/class-wc-install.php';
include_once WC_ABSPATH . 'includes/wc-update-functions.php';
$current_db_version = get_option( 'woocommerce_db_version' );
$update_count = 0;
$callbacks = WC_Install::get_db_update_callbacks();
$callbacks_to_run = array();
foreach ( $callbacks as $version => $update_callbacks ) {
if ( version_compare( $current_db_version, $version, '<' ) ) {
foreach ( $update_callbacks as $update_callback ) {
$callbacks_to_run[] = $update_callback;
}
}
}
if ( empty( $callbacks_to_run ) ) {
// Ensure DB version is set to the current WC version to match WP-Admin update routine.
WC_Install::update_db_version();
WC()->call_static(
WP_CLI::class,
'success',
/* translators: %s Database version number */
sprintf( __( 'No updates required. Database version is %s', 'woocommerce' ), get_option( 'woocommerce_db_version' ) )
);
return;
}
WC()->call_static(
WP_CLI::class,
'log',
/* translators: 1: Number of database updates 2: List of update callbacks */
sprintf( __( 'Found %1$d updates (%2$s)', 'woocommerce' ), count( $callbacks_to_run ), implode( ', ', $callbacks_to_run ) )
);
$progress = WC()->call_function(
'WP_CLI\Utils\make_progress_bar',
__( 'Updating database', 'woocommerce' ),
count( $callbacks_to_run ) // phpcs:ignore PHPCompatibility.LanguageConstructs.NewLanguageConstructs.t_ns_separatorFound
);
foreach ( $callbacks_to_run as $update_callback ) {
call_user_func( $update_callback );
$update_count ++;
$progress->tick();
}
WC_Install::update_db_version();
$progress->finish();
WC_Admin_Notices::remove_notice( 'update', true );
WC()->call_static(
WP_CLI::class,
'success',
/* translators: 1: Number of database updates performed 2: Database version number */
sprintf( __( '%1$d update functions completed. Database version is %2$s', 'woocommerce' ), absint( $update_count ), get_option( 'woocommerce_db_version' ) )
);
}
}
class-wc-cli-rest-command.php 0000644 00000032270 15233337277 0012145 0 ustar 00 <?php
/**
* WP_CLI_Rest_Command class file.
*
* @package WooCommerce\CLI
*/
use Automattic\Jetpack\Constants;
use Automattic\WooCommerce\Utilities\NumberUtil;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Main Command for WooCommerce CLI.
*
* Since a lot of WC operations can be handled via the REST API, we base our CLI
* off of Restful to generate commands for each WooCommerce REST API endpoint
* so most of the logic is shared.
*
* Forked from wp-cli/restful (by Daniel Bachhuber, released under the MIT license https://opensource.org/licenses/MIT).
* https://github.com/wp-cli/restful
*
* @version 3.0.0
* @package WooCommerce
*/
class WC_CLI_REST_Command {
/**
* Endpoints that have a parent ID.
* Ex: Product reviews, which has a product ID and a review ID.
*
* @var array
*/
protected $routes_with_parent_id = array(
'customer_download',
'product_review',
'order_note',
'shop_order_refund',
);
/**
* Name of command/endpoint object.
*
* @var string
*/
private $name;
/**
* Endpoint route.
*
* @var string
*/
private $route;
/**
* Main resource ID.
*
* @var int
*/
private $resource_identifier;
/**
* Schema for command.
*
* @var array
*/
private $schema;
/**
* List of supported IDs and their description (name => desc).
*
* @var array
*/
private $supported_ids = array();
/**
* Sets up REST Command.
*
* @param string $name Name of endpoint object (comes from schema).
* @param string $route Path to route of this endpoint.
* @param array $schema Schema object.
*/
public function __construct( $name, $route, $schema ) {
$this->name = $name;
preg_match_all( '#\([^\)]+\)#', $route, $matches );
$first_match = $matches[0];
$resource_id = ! empty( $matches[0] ) ? array_pop( $matches[0] ) : null;
$this->route = rtrim( $route );
$this->schema = $schema;
$this->resource_identifier = $resource_id;
if ( in_array( $name, $this->routes_with_parent_id, true ) ) {
$is_singular = substr( $this->route, - strlen( $resource_id ) ) === $resource_id;
if ( ! $is_singular ) {
$this->resource_identifier = $first_match[0];
}
}
}
/**
* Passes supported ID arguments (things like product_id, order_id, etc) that we should look for in addition to id.
*
* @param array $supported_ids List of supported IDs.
*/
public function set_supported_ids( $supported_ids = array() ) {
$this->supported_ids = $supported_ids;
}
/**
* Returns an ID of supported ID arguments (things like product_id, order_id, etc) that we should look for in addition to id.
*
* @return array
*/
public function get_supported_ids() {
return $this->supported_ids;
}
/**
* Create a new item.
*
* @subcommand create
*
* @param array $args WP-CLI positional arguments.
* @param array $assoc_args WP-CLI associative arguments.
*/
public function create_item( $args, $assoc_args ) {
$assoc_args = self::decode_json( $assoc_args );
list( $status, $body ) = $this->do_request( 'POST', $this->get_filled_route( $args ), $assoc_args );
if ( \WP_CLI\Utils\get_flag_value( $assoc_args, 'porcelain' ) ) {
WP_CLI::line( $body['id'] );
} else {
WP_CLI::success( "Created {$this->name} {$body['id']}." );
}
}
/**
* Delete an existing item.
*
* @subcommand delete
*
* @param array $args WP-CLI positional arguments.
* @param array $assoc_args WP-CLI associative arguments.
*/
public function delete_item( $args, $assoc_args ) {
list( $status, $body ) = $this->do_request( 'DELETE', $this->get_filled_route( $args ), $assoc_args );
$object_id = isset( $body['id'] ) ? $body['id'] : '';
if ( ! $object_id && isset( $body['slug'] ) ) {
$object_id = $body['slug'];
}
if ( \WP_CLI\Utils\get_flag_value( $assoc_args, 'porcelain' ) ) {
WP_CLI::line( $object_id );
} else {
if ( empty( $assoc_args['force'] ) ) {
WP_CLI::success( __( 'Trashed', 'woocommerce' ) . " {$this->name} {$object_id}" );
} else {
WP_CLI::success( __( 'Deleted', 'woocommerce' ) . " {$this->name} {$object_id}." );
}
}
}
/**
* Get a single item.
*
* @subcommand get
*
* @param array $args WP-CLI positional arguments.
* @param array $assoc_args WP-CLI associative arguments.
*/
public function get_item( $args, $assoc_args ) {
$route = $this->get_filled_route( $args );
list( $status, $body, $headers ) = $this->do_request( 'GET', $route, $assoc_args );
if ( ! empty( $assoc_args['fields'] ) ) {
$body = self::limit_item_to_fields( $body, $assoc_args['fields'] );
}
if ( empty( $assoc_args['format'] ) ) {
$assoc_args['format'] = 'table';
}
if ( 'headers' === $assoc_args['format'] ) {
echo wp_json_encode( $headers );
} elseif ( 'body' === $assoc_args['format'] ) {
echo wp_json_encode( $body );
} elseif ( 'envelope' === $assoc_args['format'] ) {
echo wp_json_encode(
array(
'body' => $body,
'headers' => $headers,
'status' => $status,
)
);
} else {
$formatter = $this->get_formatter( $assoc_args );
$formatter->display_item( $body );
}
}
/**
* List all items.
*
* @subcommand list
*
* @param array $args WP-CLI positional arguments.
* @param array $assoc_args WP-CLI associative arguments.
*/
public function list_items( $args, $assoc_args ) {
if ( ! empty( $assoc_args['format'] ) && 'count' === $assoc_args['format'] ) {
$method = 'HEAD';
} else {
$method = 'GET';
}
if ( ! isset( $assoc_args['per_page'] ) || empty( $assoc_args['per_page'] ) ) {
$assoc_args['per_page'] = '100';
}
list( $status, $body, $headers ) = $this->do_request( $method, $this->get_filled_route( $args ), $assoc_args );
if ( ! empty( $assoc_args['format'] ) && 'ids' === $assoc_args['format'] ) {
$items = array_column( $body, 'id' );
} else {
$items = $body;
}
if ( ! empty( $assoc_args['fields'] ) ) {
foreach ( $items as $key => $item ) {
$items[ $key ] = self::limit_item_to_fields( $item, $assoc_args['fields'] );
}
}
if ( empty( $assoc_args['format'] ) ) {
$assoc_args['format'] = 'table';
}
if ( ! empty( $assoc_args['format'] ) && 'count' === $assoc_args['format'] ) {
echo (int) $headers['X-WP-Total'];
} elseif ( 'headers' === $assoc_args['format'] ) {
echo wp_json_encode( $headers );
} elseif ( 'body' === $assoc_args['format'] ) {
echo wp_json_encode( $body );
} elseif ( 'envelope' === $assoc_args['format'] ) {
echo wp_json_encode(
array(
'body' => $body,
'headers' => $headers,
'status' => $status,
'api_url' => $this->api_url,
)
);
} else {
$formatter = $this->get_formatter( $assoc_args );
$formatter->display_items( $items );
}
}
/**
* Update an existing item.
*
* @subcommand update
*
* @param array $args WP-CLI positional arguments.
* @param array $assoc_args WP-CLI associative arguments.
*/
public function update_item( $args, $assoc_args ) {
$assoc_args = self::decode_json( $assoc_args );
list( $status, $body ) = $this->do_request( 'POST', $this->get_filled_route( $args ), $assoc_args );
if ( \WP_CLI\Utils\get_flag_value( $assoc_args, 'porcelain' ) ) {
WP_CLI::line( $body['id'] );
} else {
WP_CLI::success( __( 'Updated', 'woocommerce' ) . " {$this->name} {$body['id']}." );
}
}
/**
* Do a REST Request
*
* @param string $method Request method. Examples: 'POST', 'PUT', 'DELETE' or 'GET'.
* @param string $route Resource route.
* @param array $assoc_args Associative arguments passed to the originating WP-CLI command.
*
* @return array
*/
private function do_request( $method, $route, $assoc_args ) {
wc_maybe_define_constant( 'REST_REQUEST', true );
$request = new WP_REST_Request( $method, $route );
if ( in_array( $method, array( 'POST', 'PUT' ), true ) ) {
$request->set_body_params( $assoc_args );
} else {
foreach ( $assoc_args as $key => $value ) {
$request->set_param( $key, $value );
}
}
if ( Constants::is_true( 'SAVEQUERIES' ) ) {
$original_queries = is_array( $GLOBALS['wpdb']->queries ) ? array_keys( $GLOBALS['wpdb']->queries ) : array();
}
$response = rest_do_request( $request );
if ( Constants::is_true( 'SAVEQUERIES' ) ) {
$performed_queries = array();
foreach ( (array) $GLOBALS['wpdb']->queries as $key => $query ) {
if ( in_array( $key, $original_queries, true ) ) {
continue;
}
$performed_queries[] = $query;
}
usort(
$performed_queries,
function( $a, $b ) {
if ( $a[1] === $b[1] ) {
return 0;
}
return ( $a[1] > $b[1] ) ? -1 : 1;
}
);
$query_count = count( $performed_queries );
$query_total_time = 0;
foreach ( $performed_queries as $query ) {
$query_total_time += $query[1];
}
$slow_query_message = '';
if ( $performed_queries && 'wc' === WP_CLI::get_config( 'debug' ) ) {
$slow_query_message .= '. Ordered by slowness, the queries are:' . PHP_EOL;
foreach ( $performed_queries as $i => $query ) {
$i++;
$bits = explode( ', ', $query[2] );
$backtrace = implode( ', ', array_slice( $bits, 13 ) );
$seconds = NumberUtil::round( $query[1], 6 );
$slow_query_message .= <<<EOT
{$i}:
- {$seconds} seconds
- {$backtrace}
- {$query[0]}
EOT;
$slow_query_message .= PHP_EOL;
}
} elseif ( 'wc' !== WP_CLI::get_config( 'debug' ) ) {
$slow_query_message = '. Use --debug=wc to see all queries.';
}
$query_total_time = NumberUtil::round( $query_total_time, 6 );
WP_CLI::debug( "wc command executed {$query_count} queries in {$query_total_time} seconds{$slow_query_message}", 'wc' );
}
$error = $response->as_error();
if ( $error ) {
// For authentication errors (status 401), include a reminder to set the --user flag.
// WP_CLI::error will only return the first message from WP_Error, so we will pass a string containing both instead.
if ( 401 === $response->get_status() ) {
$errors = $error->get_error_messages();
$errors[] = __( 'Make sure to include the --user flag with an account that has permissions for this action.', 'woocommerce' ) . ' {"status":401}';
$error = implode( "\n", $errors );
}
WP_CLI::error( $error );
}
return array( $response->get_status(), $response->get_data(), $response->get_headers() );
}
/**
* Get Formatter object based on supplied parameters.
*
* @param array $assoc_args Parameters passed to command. Determines formatting.
* @return \WP_CLI\Formatter
*/
protected function get_formatter( &$assoc_args ) {
if ( ! empty( $assoc_args['fields'] ) ) {
if ( is_string( $assoc_args['fields'] ) ) {
$fields = explode( ',', $assoc_args['fields'] );
} else {
$fields = $assoc_args['fields'];
}
} else {
if ( ! empty( $assoc_args['context'] ) ) {
$fields = $this->get_context_fields( $assoc_args['context'] );
} else {
$fields = $this->get_context_fields( 'view' );
}
}
return new \WP_CLI\Formatter( $assoc_args, $fields );
}
/**
* Get a list of fields present in a given context
*
* @param string $context Scope under which the request is made. Determines fields present in response.
* @return array
*/
private function get_context_fields( $context ) {
$fields = array();
foreach ( $this->schema['properties'] as $key => $args ) {
if ( empty( $args['context'] ) || in_array( $context, $args['context'], true ) ) {
$fields[] = $key;
}
}
return $fields;
}
/**
* Get the route for this resource
*
* @param array $args Positional arguments passed to the originating WP-CLI command.
* @return string
*/
private function get_filled_route( $args = array() ) {
$supported_id_matched = false;
$route = $this->route;
foreach ( $this->get_supported_ids() as $id_name => $id_desc ) {
if ( 'id' !== $id_name && strpos( $route, '<' . $id_name . '>' ) !== false && ! empty( $args ) ) {
$route = str_replace( array( '(?P<' . $id_name . '>[\d]+)', '(?P<' . $id_name . '>\w[\w\s\-]*)' ), $args[0], $route );
$supported_id_matched = true;
}
}
if ( ! empty( $args ) ) {
$id_replacement = $supported_id_matched && ! empty( $args[1] ) ? $args[1] : $args[0];
$route = str_replace( array( '(?P<id>[\d]+)', '(?P<id>[\w-]+)' ), $id_replacement, $route );
}
return rtrim( $route );
}
/**
* Reduce an item to specific fields.
*
* @param array $item Item to reduce.
* @param array $fields Fields to keep.
* @return array
*/
private static function limit_item_to_fields( $item, $fields ) {
if ( empty( $fields ) ) {
return $item;
}
if ( is_string( $fields ) ) {
$fields = explode( ',', $fields );
}
foreach ( $item as $i => $field ) {
if ( ! in_array( $i, $fields, true ) ) {
unset( $item[ $i ] );
}
}
return $item;
}
/**
* JSON can be passed in some more complicated objects, like the payment gateway settings array.
* This function decodes the json (if present) and tries to get it's value.
*
* @param array $arr Array that will be scanned for JSON encoded values.
*
* @return array
*/
protected function decode_json( $arr ) {
foreach ( $arr as $key => $value ) {
if ( '[' === substr( $value, 0, 1 ) || '{' === substr( $value, 0, 1 ) ) {
$arr[ $key ] = json_decode( $value, true );
} else {
continue;
}
}
return $arr;
}
}
class-wc-cli-runner.php 0000644 00000020362 15233337277 0011064 0 ustar 00 <?php
/**
* WP_CLI_Runner class file.
*
* @package WooCommerce\CLI
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* WC API to WC CLI Bridge.
*
* Hooks into the REST API, figures out which endpoints come from WC,
* and registers them as CLI commands.
*
* Forked from wp-cli/restful (by Daniel Bachhuber, released under the MIT license https://opensource.org/licenses/MIT).
* https://github.com/wp-cli/restful
*
* @version 3.0.0
* @package WooCommerce
*/
class WC_CLI_Runner {
/**
* Endpoints to disable (meaning they will not be available as CLI commands).
* Some of these can either be done via WP already, or are offered with
* some other changes (like tools).
*
* @var array
*/
private static $disabled_endpoints = array(
'settings',
'settings/(?P<group_id>[\w-]+)',
'settings/(?P<group_id>[\w-]+)/batch',
'settings/(?P<group_id>[\w-]+)/(?P<id>[\w-]+)',
'system_status',
'system_status/tools',
'system_status/tools/(?P<id>[\w-]+)',
'reports',
'reports/sales',
'reports/top_sellers',
);
/**
* The version of the REST API we should target to
* generate commands.
*
* @var string
*/
private static $target_rest_version = 'v2';
/**
* Register's all endpoints as commands once WP and WC have all loaded.
*/
public static function after_wp_load() {
global $wp_rest_server;
$wp_rest_server = new WP_REST_Server();
do_action( 'rest_api_init', $wp_rest_server );
$request = new WP_REST_Request( 'GET', '/' );
$request->set_param( 'context', 'help' );
$response = $wp_rest_server->dispatch( $request );
$response_data = $response->get_data();
if ( empty( $response_data ) ) {
return;
}
// Loop through all of our endpoints and register any valid WC endpoints.
foreach ( $response_data['routes'] as $route => $route_data ) {
// Only register endpoints for WC and our target version.
if ( substr( $route, 0, 4 + strlen( self::$target_rest_version ) ) !== '/wc/' . self::$target_rest_version ) {
continue;
}
// Only register endpoints with schemas.
if ( empty( $route_data['schema']['title'] ) ) {
/* translators: %s: Route to a given WC-API endpoint */
WP_CLI::debug( sprintf( __( 'No schema title found for %s, skipping REST command registration.', 'woocommerce' ), $route ), 'wc' );
continue;
}
// Ignore batch endpoints.
if ( 'batch' === $route_data['schema']['title'] ) {
continue;
}
// Disable specific endpoints.
$route_pieces = explode( '/', $route );
$endpoint_piece = str_replace( '/wc/' . $route_pieces[2] . '/', '', $route );
if ( in_array( $endpoint_piece, self::$disabled_endpoints, true ) ) {
continue;
}
self::register_route_commands( new WC_CLI_REST_Command( $route_data['schema']['title'], $route, $route_data['schema'] ), $route, $route_data );
}
}
/**
* Generates command information and tells WP CLI about all
* commands available from a route.
*
* @param string $rest_command WC-API command.
* @param string $route Path to route endpoint.
* @param array $route_data Command data.
* @param array $command_args WP-CLI command arguments.
*/
private static function register_route_commands( $rest_command, $route, $route_data, $command_args = array() ) {
// Define IDs that we are looking for in the routes (in addition to id)
// so that we can pass it to the rest command, and use it here to generate documentation.
$supported_ids = array(
'product_id' => __( 'Product ID.', 'woocommerce' ),
'customer_id' => __( 'Customer ID.', 'woocommerce' ),
'order_id' => __( 'Order ID.', 'woocommerce' ),
'refund_id' => __( 'Refund ID.', 'woocommerce' ),
'attribute_id' => __( 'Attribute ID.', 'woocommerce' ),
'zone_id' => __( 'Zone ID.', 'woocommerce' ),
'instance_id' => __( 'Instance ID.', 'woocommerce' ),
'id' => __( 'The ID for the resource.', 'woocommerce' ),
'slug' => __( 'The slug for the resource.', 'woocommerce' ),
);
$rest_command->set_supported_ids( $supported_ids );
$positional_args = array_keys( $supported_ids );
$parent = "wc {$route_data['schema']['title']}";
$supported_commands = array();
// Get a list of supported commands for each route.
foreach ( $route_data['endpoints'] as $endpoint ) {
preg_match_all( '#\([^\)]+\)#', $route, $matches );
$resource_id = ! empty( $matches[0] ) ? array_pop( $matches[0] ) : null;
$trimmed_route = rtrim( $route );
$is_singular = substr( $trimmed_route, - strlen( $resource_id ?? '' ) ) === $resource_id;
// List a collection.
if ( array( 'GET' ) === $endpoint['methods'] && ! $is_singular ) {
$supported_commands['list'] = ! empty( $endpoint['args'] ) ? $endpoint['args'] : array();
}
// Create a specific resource.
if ( array( 'POST' ) === $endpoint['methods'] && ! $is_singular ) {
$supported_commands['create'] = ! empty( $endpoint['args'] ) ? $endpoint['args'] : array();
}
// Get a specific resource.
if ( array( 'GET' ) === $endpoint['methods'] && $is_singular ) {
$supported_commands['get'] = ! empty( $endpoint['args'] ) ? $endpoint['args'] : array();
}
// Update a specific resource.
if ( in_array( 'POST', $endpoint['methods'], true ) && $is_singular ) {
$supported_commands['update'] = ! empty( $endpoint['args'] ) ? $endpoint['args'] : array();
}
// Delete a specific resource.
if ( array( 'DELETE' ) === $endpoint['methods'] && $is_singular ) {
$supported_commands['delete'] = ! empty( $endpoint['args'] ) ? $endpoint['args'] : array();
}
}
foreach ( $supported_commands as $command => $endpoint_args ) {
$synopsis = array();
$arg_regs = array();
$ids = array();
foreach ( $supported_ids as $id_name => $id_desc ) {
if ( strpos( $route, '<' . $id_name . '>' ) !== false ) {
$synopsis[] = array(
'name' => $id_name,
'type' => 'positional',
'description' => $id_desc,
'optional' => false,
);
$ids[] = $id_name;
}
}
foreach ( $endpoint_args as $name => $args ) {
if ( ! in_array( $name, $positional_args, true ) || strpos( $route, '<' . $id_name . '>' ) === false ) {
$arg_regs[] = array(
'name' => $name,
'type' => 'assoc',
'description' => ! empty( $args['description'] ) ? $args['description'] : '',
'optional' => empty( $args['required'] ),
);
}
}
foreach ( $arg_regs as $arg_reg ) {
$synopsis[] = $arg_reg;
}
if ( in_array( $command, array( 'list', 'get' ), true ) ) {
$synopsis[] = array(
'name' => 'fields',
'type' => 'assoc',
'description' => __( 'Limit response to specific fields. Defaults to all fields.', 'woocommerce' ),
'optional' => true,
);
$synopsis[] = array(
'name' => 'field',
'type' => 'assoc',
'description' => __( 'Get the value of an individual field.', 'woocommerce' ),
'optional' => true,
);
$synopsis[] = array(
'name' => 'format',
'type' => 'assoc',
'description' => __( 'Render response in a particular format.', 'woocommerce' ),
'optional' => true,
'default' => 'table',
'options' => array(
'table',
'json',
'csv',
'ids',
'yaml',
'count',
'headers',
'body',
'envelope',
),
);
}
if ( in_array( $command, array( 'create', 'update', 'delete' ), true ) ) {
$synopsis[] = array(
'name' => 'porcelain',
'type' => 'flag',
'description' => __( 'Output just the id when the operation is successful.', 'woocommerce' ),
'optional' => true,
);
}
$methods = array(
'list' => 'list_items',
'create' => 'create_item',
'delete' => 'delete_item',
'get' => 'get_item',
'update' => 'update_item',
);
$before_invoke = null;
if ( empty( $command_args['when'] ) && \WP_CLI::get_config( 'debug' ) ) {
$before_invoke = function() {
wc_maybe_define_constant( 'SAVEQUERIES', true );
};
}
WP_CLI::add_command(
"{$parent} {$command}",
array( $rest_command, $methods[ $command ] ),
array(
'synopsis' => $synopsis,
'when' => ! empty( $command_args['when'] ) ? $command_args['when'] : '',
'before_invoke' => $before_invoke,
)
);
}
}
}
ExtractCommand.php 0000644 00000007446 15233613477 0010214 0 ustar 00 <?php
/**
* Called from Loco_cli_Commands::extract
*/
abstract class Loco_cli_ExtractCommand {
/**
* @param Loco_package_Project[] $projects project filter
* @param bool $noop whether dry run
* @param bool $force whether to always update
*/
public static function run( array $projects, $noop = true, $force = false ){
if( $force && $noop ){
throw new Loco_error_Exception('--force makes no sense with --noop');
}
// track total number of POT files synced
$updated = 0;
$content_dir = loco_constant('WP_CONTENT_DIR');
foreach( $projects as $project ){
$id = rtrim( $project->getId(), '.' );
WP_CLI::log( sprintf('Extracting "%s" (%s)',$project->getName(),$id) );
// POT file may or may not exist currently
$potfile = $project->getPot();
if( ! $potfile ){
WP_CLI::warning('Skipping undefined POT');
continue;
}
if( $potfile->locked() ){
WP_CLI::warning('Skipping unwritable POT');
Loco_cli_Utils::tabulateFiles( $potfile->getParent(), $potfile );
continue;
}
// Do extraction and grab only given domain's strings
$ext = new Loco_gettext_Extraction( $project->getBundle() );
$domain = $project->getDomain()->getName();
$data = $ext->addProject($project)->includeMeta()->getTemplate( $domain );
Loco_cli_Utils::debug('Extracted %u strings', count($data) );
$list = $ext->getSkipped();
if( $list ){
$current = Loco_data_Settings::get()->max_php_size;
$suggest = ceil( $ext->getMaxPhpSize() / 1024 );
WP_CLI::warning(sprintf('%u source files skipped over %s. Consider running with --maxsize=%uK',count($list),$current,$suggest) );
foreach( $list as $file ) {
$f = new Loco_mvc_FileParams([],$file);
Loco_cli_Utils::debug('%s (%s)', $f->relpath, $f->size );
}
}
// if POT exists check if update is necessary.
$data->sort();
if( $potfile->exists() && ! $force ){
try {
Loco_cli_Utils::debug('Checking if sources have changed since '.date('c',$potfile->modified()) );
$prev = Loco_gettext_Data::fromSource( $potfile->getContents() );
if( $prev->equal($data) ){
WP_CLI::log('No update required for '.$potfile->basename() );
continue;
}
}
catch( Loco_error_ParseException $e ){
Loco_cli_Utils::debug( $e->getMessage().' in '.$potfile->basename() );
}
}
if( $noop ){
WP_CLI::success( sprintf('**DRY RUN** would update %s', $potfile->basename() ) );
continue;
}
// additional headers to set in new POT file
$head = $data->getHeaders();
$head['Project-Id-Version'] = $project->getName();
$head['X-Domain'] = $domain;
// write POT file to disk returning byte length
Loco_cli_Utils::debug('Writing POT file...');
$bytes = $potfile->putContents( $data->msgcat() );
Loco_cli_Utils::debug('%u bytes written to %s',$bytes, $potfile->getRelativePath($content_dir) );
WP_CLI::success( sprintf('Updated %s', $potfile->basename() ) );
$updated++;
}
// sync summary
if( 0 === $updated ){
WP_CLI::log('Nothing updated');
}
else {
WP_CLI::success( sprintf('%u POT files written',$updated) );
}
}
} FetchCommand.php 0000644 00000023544 15233613477 0007630 0 ustar 00 <?php
/**
* Called from Loco_cli_Commands::fetch
*/
abstract class Loco_cli_FetchCommand {
/**
* @param Loco_package_Project[] $projects project filter
* @param Loco_Locale[] $locales locale filter
* @param bool[] $opts switches
*/
public static function run( array $projects, array $locales, array $opts ){
$wp = new Loco_api_WordPressTranslations;
$done = 0;
// fetch for every "installed" locale if none specified
if( ! $locales ){
foreach( $wp->getInstalledCore() as $tag ){
if( 'en_US' === $tag ){
continue;
}
$locale = Loco_Locale::parse($tag);
if( $locale->isValid() ){
$locales[] = $locale;
}
}
if( ! $locales ){
throw new Loco_error_Exception('No installed languages, try with --locale=<code>');
}
}
foreach( $projects as $project ){
$type = strtolower( $project->getBundle()->getType() );
$domain = $project->getDomain()->getName();
$info = $project->getBundle()->getHeaderInfo();
$version = $info->Version;
// Currently only supporting WordPress community translation sources.
$args = [ 'version' => $version ];
if( 'core' !== $type ){
$type.= 's';
if( $project->getSlug() !== $domain ){
WP_CLI::warning( sprintf('Skipping %s, only single text domain %s supported',$project->getId(),$type));
continue;
}
$args['slug'] = $domain;
}
WP_CLI::log( sprintf('Looking up %s v%s..',$project,$version) );
Loco_cli_Utils::debug('Querying WordPress translations API for %s => %s..',$type,json_encode($args) );
$result = $wp->apiGet($type,$args);
// pre-index installable language packs
$packages = [];
foreach( $result['translations'] as $data ){
$packages[$data['language']] = $data['package'];
}
// Translations API does not error when GlotPress project doesn't exist, it just returns empty.
if( ! $packages ){
Loco_cli_Utils::debug('No installable language packs available for %s. Checking if a GlotPress project exists..',$project);
// Ping GlotPress project page. This is the only way we can know if an incomplete project exists
$response = wp_remote_head( sprintf('https://translate.wordpress.org/projects/wp-%s/%s/',$type,$args['slug']) );
$status = wp_remote_retrieve_response_code($response);
if( 404 === $status ){
WP_CLI::warning( sprintf("Skipping %s: 404 from translate.wordpress.org. Probably no GlotPress project.",$project) );
continue;
}
else if( 200 !== $status ){
WP_CLI::warning( sprintf("Status %u from translate.wordpress.org. Skipping %s.",$status,$project) );
}
Loco_cli_Utils::debug('> Ok, looks like GlotPress project exists; probably no locales above the threshold for a package build');
}
// Save path is under "system" location because we are installing from GlotPress
$dir = new Loco_fs_Directory( 'core' === $type ? '.' : $type );
$dir->normalize( loco_constant('WP_LANG_DIR') );
foreach( $locales as $locale ){
$tag = (string) $locale;
if( 'en_US' == $tag ){
WP_CLI::warning('There are no translations in en_US. It is the source locale.');
continue;
}
// Map WP locale codes to GlotPress teams. They differ, naturally.
$team = $locale->lang;
if( $locale->region ){
$team.= '-'.strtolower($locale->region);
}
$gp = Loco_data_CompiledData::get('gp');
if( array_key_exists($team,$gp['aliases']) ){
$team = $gp['aliases'][$team];
}
// variant code (e.g. formal) is a sub-entity and not part of team language id
$variant = $locale->variant;
if( ! $variant ){
$variant = 'default';
}
if( 'core' === $type ){
// core projects are per-version. "dev" being upcoming. Then e.g. 5.6.x for stable
if( $opts['trunk'] || preg_match('/^\\d.\\d-(?:rc|dev|beta)/i',$version) ){
$slug = 'dev';
}
else {
list($major,$minor) = explode('.',$version,3);
$slug = sprintf('%u.%u.x',$major,$minor);
}
// Core projects are sub projects. plugins and themes don't have this
$map = [
'default.' => '',
'default.admin' => '/admin',
'default.admin-network' => '/admin/network',
'continents-cities' => '/cc',
];
$slug .= $map[ $project->getId() ];
$url = 'https://translate.wordpress.org/projects/wp/'.$slug.'/'.$team.'/'.$variant.'/export-translations/?format=po';
}
else {
$slug = $domain;
// plugins are either "stable" or "dev"; themes don't appear to have stability/version slug ??
if( 'plugins' === $type ) {
$slug .= $opts['trunk'] ? '/dev' : '/stable';
}
$url = 'https://translate.wordpress.org/projects/wp-'.$type.'/'.$slug.'/'.$team.'/' . $variant . '/export-translations/?format=po';
}
// Note that this export URL is not a documented API and may change without notice
// TODO We could pass If-Modified-Since with current PO file header, BUT that could not know if existing file is purged or not. Make configurable?
WP_CLI::log( sprintf('Fetching PO from %s..',$url));
$response = wp_remote_get($url);
$status = wp_remote_retrieve_response_code($response);
if( 200 !== $status ){
WP_CLI::warning( sprintf('Status %u from translate.wordpress.org; skipping "%s". Probably no translation team',$status,$tag) );
continue;
}
Loco_cli_Utils::debug('OK, last modified %s', wp_remote_retrieve_header($response,'last-modified') );
/*/ TODO fallback to installable package
if( $packages && ! array_key_exists($tag,$packages) ){
WP_ClI::warning( sprintf('%s is not installable in `%s` (probably not complete enough)',$project,$tag) );
}*/
// Parse PO data to check it's valid, and also because we're going to compile it.
$pobody = wp_remote_retrieve_body($response);
$podata = Loco_gettext_Data::fromSource($pobody);
$response = null;
// keep translations if file already exists in this location.
$pofile = $project->initLocaleFile($dir,$locale);
$info = new Loco_mvc_FileParams( [], $pofile );
Loco_cli_Utils::debug('Saving %s..', $info->relpath );
$compiler = new Loco_gettext_Compiler($pofile);
if( $pofile->exists() ){
$info = new Loco_mvc_FileParams( [], $pofile );
Loco_cli_Utils::debug('PO already exists at %s (%s), merging..',$info->relpath,$info->size);
$original = Loco_gettext_Data::load($pofile);
$matcher = new Loco_gettext_Matcher($project);
$matcher->loadRefs($podata,true);
// downloaded file is in memory can be replaced with merged version
$podata = clone $original;
$podata->clear();
$stats = $matcher->merge($original,$podata);
$original = null;
if( ! $stats['add'] && ! $stats['del'] && ! $stats['fuz'] && ! $stats['trn'] ){
WP_CLI::log( sprintf('%s unchanged in "%s". Skipping %s', $project,$locale,$info->relpath) );
continue;
}
// Overwrite merged PO, which will back up first if configured
Loco_cli_Utils::debug('OK: %u added, %u dropped, %u fuzzy', count($stats['add']), count($stats['del']), count($stats['fuz']) );
$podata->localize($locale);
$compiler->writePo($podata);
}
// Copy PO directly to disk as per remote source
else {
$compiler->writeFile($pofile,$pobody);
$podata->inheritHeader( Loco_gettext_Data::dummy()->localize($locale)->getHeaders() );
}
// Compile new MO and JSON files..
Loco_cli_Utils::debug('Compiling %s.{mo,json}',$pofile->filename() );
$compiler->writeMo($podata);
$compiler->writeJson($project,$podata);
$pofile->clearStat();
WP_CLI::success( sprintf('Fetched %s for "%s": %s PO at %s', $project,$locale,$info->size,$info->relpath) );
Loco_error_AdminNotices::get()->flush();
// clean up memory and ready for next file
unset($podata,$pobody);
$done++;
}
}
if( 0 === $done ){
WP_CLI::success('Completed OK, but no files were installed');
}
}
}
Utils.php 0000644 00000012023 15233613477 0006366 0 ustar 00 <?php
/**
* Utility functions for wp cli commands
*/
abstract class Loco_cli_Utils {
/**
* Collect translation sets according to type/domain filter
* @return Loco_package_Project[]
*/
public static function collectProjects( $filter ){
$projects = [];
$domain = null;
$slug = null;
// bundle type filter, with optional argument
if( preg_match('/^(plugins|themes|core)(?::(.+))?/i',$filter,$matched) ){
$type = strtolower($matched[1]);
$handle = isset($matched[2]) ? $matched[2] : '';
if( 'plugins' === $type ){
if( $handle ){
$bundles = [ Loco_package_Plugin::create($handle) ];
}
else {
$bundles = Loco_package_Plugin::getAll();
}
}
else if( 'themes' === $type ){
if( $handle ){
$bundles = [ Loco_package_Theme::create($handle) ];
}
else {
$bundles = Loco_package_Theme::getAll();
}
}
else {
$bundles = [ Loco_package_Core::create() ];
$slug = $handle;
}
}
// else fall back to text domain filter
else {
$domain = $filter;
$bundles = [ Loco_package_Core::create() ];
$bundles = array_merge( $bundles, Loco_package_Plugin::getAll() );
$bundles = array_merge( $bundles, Loco_package_Theme::getAll() );
}
/* @var Loco_package_Project $project */
foreach( $bundles as $bundle ){
foreach( $bundle as $project ){
if( $domain && $project->getDomain()->getName() !== $domain ){
continue;
}
if( $slug && $project->getSlug() !== $slug ){
continue;
}
$projects[] = $project;
}
}
if( ! $projects ){
throw new Loco_error_Exception('No translation sets found');
}
return $projects;
}
/**
* Collect locales from one or more language tags
* @param string zero or more language tags
* @return Loco_Locale[]
*/
public static function collectLocales( $tags ){
$locales = [];
if( '' !== $tags ){
$api = new Loco_api_WordPressTranslations;
foreach( preg_split('/[\\s,;]+/i',$tags,-1,PREG_SPLIT_NO_EMPTY) as $tag ){
$locale = Loco_Locale::parse($tag);
if( ! $locale->isValid() ){
throw new Loco_error_Exception('Invalid locale: '.json_encode($tag) );
}
// TODO could expand language-only tags to known WordPress locales e.g. fr -> fr_FR
$locales[ (string) $locale ] = $locale;
$locale->ensureName($api);
}
// empty locales means ALL locales, so refuse to return ALL when filter was non-empty
if( 0 === count($locales) ){
throw new Loco_error_Exception('No valid locales in: '.json_encode($tags) );
}
}
return $locales;
}
/**
* Simple space-padded table
* @param string[][] data rows to print
*/
public static function tabulate( array $t ){
$w = [];
foreach( $t as $y => $row ){
foreach( $row as $x => $value ){
$width = mb_strlen($value,'UTF-8');
$w[$x] = isset($w[$x]) ? max($w[$x],$width) : $width;
}
}
foreach( $t as $y => $row ){
$line = [];
foreach( $w as $x => $width ){
$value = isset($row[$x]) ? $row[$x] : '';
$value = str_pad($value,$width,' ',STR_PAD_RIGHT);
$line[] = $value;
}
self::debug( implode(' ',$line) );
}
}
/**
* Prints file listing to stdout
*/
public static function tabulateFiles(){
$t = [];
/* @var Loco_fs_File $file */
foreach( func_get_args() as $file ){
if( $file instanceof Loco_fs_File && $file->exists() ){
$f = new Loco_mvc_FileParams([],$file);
$t[] = [ $f->owner, $f->group, $f->smode, $f->relpath ];
}
}
self::tabulate($t);
}
/**
* WP_CLI debug logger
*/
public static function debug(){
$args = func_get_args();
$message = array_shift($args);
if( $args ){
$message = vsprintf($message,$args);
}
WP_CLI::debug( $message,'loco' );
}
/**
* Parse boolean command line option. Absence is equal to false
* @param string[]
* @param string
* @return bool
*/
public static function bool( array $opts, $key ){
$value = isset($opts[$key]) ? $opts[$key] : false;
if( ! is_bool($value) ){
$value = $value && 'false' !== $value & 'no' !== $value;
}
return $value;
}
} SyncCommand.php 0000644 00000020671 15233613477 0007511 0 ustar 00 <?php
/**
* Called from Loco_cli_Commands::sync
*/
abstract class Loco_cli_SyncCommand {
/**
* @param Loco_package_Project[] $projects project filter
* @param Loco_Locale[] $locales locale filter
* @param bool $noop whether dry run
* @param bool $force whether to always update
*/
public static function run( array $projects, array $locales, $noop = true, $force = false ){
if( $force && $noop ){
throw new Loco_error_Exception('--force makes no sense with --noop');
}
$content_dir = loco_constant('WP_CONTENT_DIR');
$wp_locales = new Loco_api_WordPressTranslations;
// track total number of PO files synced, plus MO and JSON files compiled
$updated = 0;
$compiled = 0;
foreach( $projects as $project ){
$id = rtrim( $project->getId(), '.' );
$base_dir = $project->getBundle()->getDirectoryPath();
WP_CLI::log( sprintf('Syncing "%s" (%s)',$project->getName(),$id) );
// Check if project has POT, which will be used as default template unless PO overrides
$pot = null;
$potfile = $project->getPot();
if( $potfile && $potfile->exists() ){
Loco_cli_Utils::debug('Parsing template: %s',$potfile->getRelativePath($content_dir));
try {
$pot = Loco_gettext_Data::fromSource( $potfile->getContents() );
}
catch( Loco_error_ParseException $e ){
WP_CLI::error( $e->getMessage().' in '.$potfile->getRelativePath($content_dir), false );
$potfile = null;
}
}
/* @var Loco_fs_LocaleFile $pofile */
$pofiles = $project->findLocaleFiles('po');
foreach( $pofiles as $pofile ){
$locale = $pofile->getLocale();
$tag = (string) $locale;
if( $locales && ! array_key_exists($tag,$locales) ){
continue;
}
// Preempt write errors and print useful file mode info
$mofile = $pofile->cloneExtension('mo');
if( ! $pofile->writable() || $mofile->locked() ){
WP_CLI::warning('Skipping unwritable: '.self::fname($pofile) );
Loco_cli_Utils::tabulateFiles( $pofile->getParent(), $pofile, $mofile );
continue;
}
// Parsing candidate PO file (definitions)
Loco_cli_Utils::debug('Parsing PO: %s',$pofile->getRelativePath($content_dir));
try {
$def = Loco_gettext_Data::fromSource( $pofile->getContents() );
}
catch( Loco_error_ParseException $e ){
WP_CLI::error( $e->getMessage().' in '.$pofile->getRelativePath($content_dir), false );
continue;
}
// Check if PO defines alternative template (reference)
$ref = $pot;
$head = $def->getHeaders();
$opts = new Loco_gettext_SyncOptions($head);
$translate = $opts->mergeMsgstr();
if( $opts->hasTemplate() ){
$ref = null;
$potfile = $opts->getTemplate();
$potfile->normalize( $base_dir );
if( $potfile->exists() ){
try {
Loco_cli_Utils::debug('> Parsing alternative template: %s',$potfile->getRelativePath($content_dir) );
$ref = Loco_gettext_Data::fromSource( $potfile->getContents() );
}
catch( Loco_error_ParseException $e ){
WP_CLI::error( $e->getMessage().' in '.$potfile->getRelativePath($content_dir), false );
}
}
else {
Loco_cli_Utils::debug('Template not found (%s)', $potfile->basename() );
}
}
if( ! $ref ){
WP_CLI::warning( sprintf('Skipping %s; no valid translation template',$pofile->getRelativePath($content_dir) ) );
continue;
}
// Perform merge if we have a reference file
Loco_cli_Utils::debug('Merging %s <- %s', $pofile->basename(), $potfile->basename() );
$matcher = new Loco_gettext_Matcher($project);
$matcher->loadRefs($ref,$translate );
// Merge jsons if configured and available
if( $opts->mergeJson() ){
$siblings = new Loco_fs_Siblings( $potfile->cloneBasename( $pofile->basename() ) );
$jsons = $siblings->getJsons( $project->getDomain()->getName() );
$njson = $matcher->loadJsons($jsons);
Loco_cli_Utils::debug('> merged %u json files', $njson );
}
// Get fuzzy matching tolerance from plugin settings, can be set temporarily in command line
$fuzziness = Loco_data_Settings::get()->fuzziness;
$matcher->setFuzziness( (string) $fuzziness );
// update matches sources, deferring unmatched for deferred fuzzy match
$po = clone $def;
$po->clear();
$nvalid = count( $matcher->mergeValid($def,$po) );
$nfuzzy = count( $matcher->mergeFuzzy($po) );
$nadded = count( $matcher->mergeAdded($po) );
$ndropped = count( $matcher->redundant() );
// TODO Support --previous to keep old strings, or at least comment them out as #| msgid.....
if( $nfuzzy || $nadded || $ndropped ){
Loco_cli_Utils::debug('> unchanged:%u added:%u fuzzy:%u dropped:%u', $nvalid, $nadded, $nfuzzy, $ndropped );
}
else {
Loco_cli_Utils::debug('> %u identical sources',$nvalid);
}
// File is synced, but may be identical
$po->sort();
if( ! $force && $po->equal($def) ){
WP_CLI::log( sprintf('No update required for %s', self::fname($pofile) ) );
continue;
}
if( $noop ){
WP_CLI::success( sprintf('**DRY RUN** would update %s', self::fname($pofile) ) );
continue;
}
try {
$locale->ensureName($wp_locales);
$po->localize($locale);
$compiler = new Loco_gettext_Compiler($pofile);
$bytes = $compiler->writePo($po);
Loco_cli_Utils::debug('+ %u bytes written to %s',$bytes, $pofile->basename());
$updated++;
// compile MO
$bytes = $compiler->writeMo($po);
if( $bytes ){
Loco_cli_Utils::debug('+ %u bytes written to %s',$bytes, $mofile->basename());
$compiled++;
}
// Done PO/MO pair, now generate JSON fragments as applicable
$jsons = $compiler->writeJson($project,$po);
foreach( $jsons as $file ){
$compiled++;
$param = new Loco_mvc_FileParams([],$file);
Loco_cli_Utils::debug('+ %u bytes written to %s',$param->size,$param->name);
}
// Done compile of this set
Loco_error_AdminNotices::get()->flush();
WP_CLI::success( sprintf('Updated %s', self::fname($pofile) ) );
}
catch( Loco_error_WriteException $e ){
WP_CLI::error( $e->getMessage(), false );
}
}
}
// sync summary
if( 0 === $updated ){
WP_CLI::log('Nothing updated');
}
else {
WP_CLI::success( sprintf('%u PO files synced, %u files compiled',$updated,$compiled) );
}
}
/**
* Debug file name showing directory location
* @param Loco_fs_File
* @return string
*/
private static function fname( Loco_fs_File $file ){
$dir = new Loco_fs_LocaleDirectory( $file->dirname() );
return $file->filename().' ('.$dir->getTypeLabel( $dir->getTypeId() ).')';
}
} Commands.php 0000644 00000007667 15233613477 0007051 0 ustar 00 <?php
/**
* Loco Translate commands
* @codeCoverageIgnore
*/
class Loco_cli_Commands {
/**
* Sync translation files with the available source strings
*
* ## OPTIONS
*
* [<filter>]
* : Restrict to a type of bundle (plugins|themes|core); a single bundle (e.g. plugins:<handle>); or a Text Domain
*
* [--locale=<code>]
* : Restrict to one or more locales. Separate multiple codes with commas.
*
* [--fuzziness=<percent>]
* : Override plugin settings for fuzzy matching tolerance (0-100).
*
* [--noop]
* : Specify dry run. Makes no changes on disk.
*
* [--force]
* : Update even when nothing has changed. Useful for recompiling MO/JSON.
*
* ## EXAMPLES
*
* wp loco sync plugins
*
* @param string[] $args
* @param string[] $opts
*/
public function sync( $args, $opts ){
if( array_key_exists('fuzziness',$opts) ){
Loco_data_Settings::get()->fuzziness = (int) $opts['fuzziness'];
}
try {
Loco_cli_SyncCommand::run (
Loco_cli_Utils::collectProjects( isset($args[0]) ? $args[0] : '' ),
Loco_cli_Utils::collectLocales( isset($opts['locale']) ? $opts['locale'] : '' ),
Loco_cli_Utils::bool($opts,'noop'),
Loco_cli_Utils::bool($opts,'force')
);
}
catch( Loco_error_Exception $e ){
WP_CLI::error( $e->getMessage() );
}
}
/**
* Extract available source strings
*
* ## OPTIONS
*
* [<filter>]
* : Restrict to a type of bundle (plugins|themes|core); a single bundle (e.g. plugins:<handle>); or a Text Domain
*
* [--maxsize=<size>]
* : Override plugin settings for maximum PHP file size
*
* [--noop]
* : Specify dry run. Makes no changes on disk.
*
* [--force]
* : Update even when nothing has changed. Useful for updating meta properties.
*
* ## EXAMPLES
*
* wp loco extract core --maxsize=400K
*
* @param string[] $args
* @param string[] $opts
*/
public function extract( $args, $opts ){
try {
if( array_key_exists('maxsize',$opts) ){
Loco_data_Settings::get()->max_php_size = $opts['maxsize'];
}
Loco_cli_ExtractCommand::run (
Loco_cli_Utils::collectProjects( isset($args[0]) ? $args[0] : '' ),
Loco_cli_Utils::bool($opts,'noop'),
Loco_cli_Utils::bool($opts,'force')
);
}
catch( Loco_error_Exception $e ){
WP_CLI::error( $e->getMessage() );
}
}
/**
* EXPERIMENTAL. Attempts to install translation source files from an external repository.
* Use this to replace *installed* PO files if they are missing or have been purged of script translations.
*
* ## OPTIONS
*
* [<filter>]
* : Restrict to a type of bundle (plugins|themes|core); a single bundle (e.g. plugins:<handle>); or a Text Domain
*
* [--locale=<code>]
* : Restrict to one or more locales. Separate multiple codes with commas.
*
* [--trunk]
* : Install strings for upcoming dev version as opposed to latest stable
*
* ## EXAMPLES
*
* wp loco fetch loco-translate --locale=en_GB
*
* @param string[] $args
* @param string[] $opts
*/
public function fetch( $args, $opts ){
try {
Loco_cli_FetchCommand::run (
Loco_cli_Utils::collectProjects( isset($args[0]) ? $args[0] : '' ),
Loco_cli_Utils::collectLocales( isset($opts['locale']) ? $opts['locale'] : '' ),
[
'trunk' => Loco_cli_Utils::bool($opts,'trunk')
]
);
}
catch( Loco_error_Exception $e ){
WP_CLI::error( $e->getMessage() );
}
}
}