Σ(゚Д゚;≡;゚д゚)Σ(゚Д゚;≡;゚д゚)Σ(゚Д゚;≡;゚д゚)始,故人唐宰相鲁公,🆚开府南服,余以布衣从戎。明年,别公漳水湄。后明年,公以事过张睢阳庙及颜杲卿所尝往来处,悲歌慷慨,卒不负其言而从之游。今其诗具在,可考也。😭 HEX
HEX
Server: Apache
System: Linux webm006.cluster120.gra.hosting.ovh.net 6.18.39-ovh-vps-grsec-zfs+ #1 SMP PREEMPT_DYNAMIC Tue Jul 21 12:03:15 CEST 2026 x86_64
User: studionolh (122383)
PHP: 7.3.33
Disabled: _dyuweyrj4,_dyuweyrj4r,dl
Upload Files
File: /home/s/t/u/studionolh/www/wp-includes/Requests/Cookie/reusable-blocks/cli.tar
class-wc-cli-com-extension-command.php000064400000006317152333372770013763 0ustar00<?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.php000064400000002353152333372770012622 0ustar00<?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.php000064400000014327152333372770011751 0ustar00<?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.php000064400000005546152333372770012153 0ustar00<?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.php000064400000005102152333372770012444 0ustar00<?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.php000064400000032270152333372770012145 0ustar00<?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.php000064400000020362152333372770011064 0ustar00<?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.php000064400000007446152336134770010214 0ustar00<?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.php000064400000023544152336134770007630 0ustar00<?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.php000064400000012023152336134770006366 0ustar00<?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.php000064400000020671152336134770007511 0ustar00<?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.php000064400000007667152336134770007051 0ustar00<?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() );
        }        
    }

}