File: //usr/local/wp/vendor/wp-cli/package-command/src/Package_Command.php
<?php
use Composer\Composer;
use Composer\Config;
use Composer\Config\JsonConfigSource;
use Composer\DependencyResolver\Pool;
use Composer\Factory;
use Composer\IO\NullIO;
use Composer\Installer;
use Composer\Json\JsonFile;
use Composer\Package\BasePackage;
use Composer\Package\PackageInterface;
use Composer\Package\Version\VersionSelector;
use Composer\Repository;
use Composer\Repository\CompositeRepository;
use Composer\Repository\ComposerRepository;
use Composer\Util\HttpDownloader;
use WP_CLI\Package\ComposerIO;
use WP_CLI\Extractor;
use WP_CLI\Utils;
use WP_CLI\JsonManipulator;
use WP_CLI\PackageManagerEventSubscriber;
use WP_CLI\RequestsLibrary;
/**
* Lists, installs, and removes WP-CLI packages.
*
* WP-CLI packages are community-maintained projects built on WP-CLI. They can
* contain WP-CLI commands, but they can also just extend WP-CLI in some way.
*
* Learn how to create your own command from the
* [Commands Cookbook](https://make.wordpress.org/cli/handbook/guides/commands-cookbook/)
*
* ## EXAMPLES
*
* # List installed packages.
* $ wp package list
* +-----------------------+------------------+----------+-----------+----------------+
* | name | authors | version | update | update_version |
* +-----------------------+------------------+----------+-----------+----------------+
* | wp-cli/server-command | Daniel Bachhuber | dev-main | available | 2.x-dev |
* +-----------------------+------------------+----------+-----------+----------------+
*
* # Install the latest development version of the package.
* $ wp package install wp-cli/server-command
* Installing package wp-cli/server-command (dev-main)
* Updating /home/person/.wp-cli/packages/composer.json to require the package...
* Using Composer to install the package...
* ---
* Loading composer repositories with package information
* Updating dependencies
* Resolving dependencies through SAT
* Dependency resolution completed in 0.005 seconds
* Analyzed 732 packages to resolve dependencies
* Analyzed 1034 rules to resolve dependencies
* - Installing package
* Writing lock file
* Generating autoload files
* ---
* Success: Package installed.
*
* # Uninstall package.
* $ wp package uninstall wp-cli/server-command
* Removing require statement for package 'wp-cli/server-command' from /home/person/.wp-cli/packages/composer.json
* Removing repository details from /home/person/.wp-cli/packages/composer.json
* Removing package directories and regenerating autoloader...
* Success: Uninstalled package.
*
* @package WP-CLI
*
* @when before_wp_load
*/
class Package_Command extends WP_CLI_Command {
const PACKAGE_INDEX_URL = 'https://wp-cli.org/package-index/';
const DEFAULT_DEV_BRANCH_CONSTRAINTS = 'dev-main || dev-master || dev-trunk';
private $version_selector = false;
/**
* Default author data used while creating default WP-CLI packages composer.json.
*
* @var array
*/
private $author_data = [
'name' => 'WP-CLI',
'email' => 'noreply@wpcli.org',
];
/**
* Default repository data used while creating default WP-CLI packages composer.json.
* @var array
*/
private $composer_type_package = [
'type' => 'composer',
'url' => self::PACKAGE_INDEX_URL,
];
/**
* Browses WP-CLI packages available for installation.
*
* Lists packages available for installation from the [Package Index](http://wp-cli.org/package-index/).
* Although the package index will remain in place for backward compatibility reasons, it has been
* deprecated and will not be updated further. Please refer to https://github.com/wp-cli/ideas/issues/51
* to read about its potential replacement.
*
* ## OPTIONS
*
* [--fields=<fields>]
* : Limit the output to specific fields. Defaults to all fields.
*
* [--format=<format>]
* : Render output in a particular format.
* ---
* default: table
* options:
* - table
* - csv
* - ids
* - json
* - yaml
* ---
*
* ## AVAILABLE FIELDS
*
* These fields will be displayed by default for each package:
*
* * name
* * description
* * authors
* * version
*
* There are no optionally available fields.
*
* ## EXAMPLES
*
* $ wp package browse --format=yaml
* ---
* 10up/mu-migration:
* name: 10up/mu-migration
* description: A set of WP-CLI commands to support the migration of single WordPress instances to multisite
* authors: NĂcholas AndrĂ©
* version: dev-main, dev-develop
* aaemnnosttv/wp-cli-dotenv-command:
* name: aaemnnosttv/wp-cli-dotenv-command
* description: Dotenv commands for WP-CLI
* authors: Evan Mattson
* version: v0.1, v0.1-beta.1, v0.2, dev-main, dev-dev, dev-develop, dev-tests/behat
* aaemnnosttv/wp-cli-http-command:
* name: aaemnnosttv/wp-cli-http-command
* description: WP-CLI command for using the WordPress HTTP API
* authors: Evan Mattson
* version: dev-main
*/
public function browse( $_, $assoc_args ) {
$this->set_composer_auth_env_var();
if ( empty( $assoc_args['format'] ) || 'table' === $assoc_args['format'] ) {
WP_CLI::line( WP_CLI::colorize( '%CAlthough the package index will remain in place for backward compatibility reasons, it has been deprecated and will not be updated further. Please refer to https://github.com/wp-cli/ideas/issues/51 to read about its potential replacement.%n' ) );
}
$this->show_packages( 'browse', $this->get_community_packages(), $assoc_args );
}
/**
* Installs a WP-CLI package.
*
* Packages are required to be a valid Composer package, and can be
* specified as:
*
* * Package name from WP-CLI's package index.
* * Git URL accessible by the current shell user.
* * Path to a directory on the local machine.
* * Local or remote .zip file.
*
* Packages are installed to `~/.wp-cli/packages/` by default. Use the
* `WP_CLI_PACKAGES_DIR` environment variable to provide a custom path.
*
* When installing a local directory, WP-CLI simply registers a
* reference to the directory. If you move or delete the directory, WP-CLI's
* reference breaks.
*
* When installing a .zip file, WP-CLI extracts the package to
* `~/.wp-cli/packages/local/<package-name>`.
*
* If Github token authorization is required, a GitHub Personal Access Token
* (https://github.com/settings/tokens) can be used. The following command
* will add a GitHub Personal Access Token to Composer's global configuration:
* composer config -g github-oauth.github.com <GITHUB_TOKEN>
* Once this has been added, the value used for <GITHUB_TOKEN> will be used
* for future authorization requests.
*
* ## OPTIONS
*
* <name|git|path|zip>
* : Name, git URL, directory path, or .zip file for the package to install.
* Names can optionally include a version constraint
* (e.g. wp-cli/server-command:@stable).
*
* [--insecure]
* : Retry downloads without certificate validation if TLS handshake fails. Note: This makes the request vulnerable to a MITM attack.
*
* ## EXAMPLES
*
* # Install a package hosted at a git URL.
* $ wp package install runcommand/hook
*
* # Install the latest stable version.
* $ wp package install wp-cli/server-command:@stable
*
* # Install a package hosted at a GitLab.com URL.
* $ wp package install https://gitlab.com/foo/wp-cli-bar-command.git
*
* # Install a package in a .zip file.
* $ wp package install google-sitemap-generator-cli.zip
*/
public function install( $args, $assoc_args ) {
list( $package_name ) = $args;
$insecure = (bool) Utils\get_flag_value( $assoc_args, 'insecure', false );
$this->set_composer_auth_env_var();
$git_package = false;
$dir_package = false;
$version = '';
if ( $this->is_git_repository( $package_name ) ) {
if ( '' === $version ) {
$version = "dev-{$this->get_github_default_branch( $package_name, $insecure )}";
}
$git_package = $package_name;
$matches = [];
if ( preg_match( '#([^:\/]+\/[^\/]+)\.git#', $package_name, $matches ) ) {
$package_name = $this->check_git_package_name( $matches[1], $package_name, $version, $insecure );
} else {
WP_CLI::error( "Couldn't parse package name from expected path '<name>/<package>'." );
}
} elseif ( ( false !== strpos( $package_name, '://' ) && false !== stripos( $package_name, '.zip' ) )
|| ( pathinfo( $package_name, PATHINFO_EXTENSION ) === 'zip' && is_file( $package_name ) ) ) {
// Download the remote ZIP file to a temp directory
$temp = false;
if ( false !== strpos( $package_name, '://' ) ) {
$temp = Utils\get_temp_dir() . uniqid( 'wp-cli-package_', true /*more_entropy*/ ) . '.zip';
$options = [
'timeout' => 600,
'filename' => $temp,
'insecure' => $insecure,
];
$gitlab_token = getenv( 'GITLAB_TOKEN' ); // Use GITLAB_TOKEN if available to avoid authorization failures or rate-limiting.
$headers = $gitlab_token && strpos( $package_name, '://gitlab.com/' ) !== false ? [ 'PRIVATE-TOKEN' => $gitlab_token ] : [];
$response = Utils\http_request( 'GET', $package_name, null, $headers, $options );
if ( 20 !== (int) substr( $response->status_code, 0, 2 ) ) {
@unlink( $temp ); // @codingStandardsIgnoreLine
WP_CLI::error( sprintf( "Couldn't download package from '%s' (HTTP code %d).", $package_name, $response->status_code ) );
}
$package_name = $temp;
}
$dir_package = Utils\get_temp_dir() . uniqid( 'wp-cli-package_', true /*more_entropy*/ );
try {
// Extract the package to get the package name
Extractor::extract( $package_name, $dir_package );
if ( $temp ) {
unlink( $temp );
$temp = false;
}
list( $package_name, $version ) = self::get_package_name_and_version_from_dir_package( $dir_package );
// Move to a location based on the package name
$local_dir = rtrim( WP_CLI::get_runner()->get_packages_dir_path(), '/' ) . '/local/';
$actual_dir_package = $local_dir . str_replace( '/', '-', $package_name );
Extractor::copy_overwrite_files( $dir_package, $actual_dir_package );
Extractor::rmdir( $dir_package );
// Behold, the extracted package
$dir_package = $actual_dir_package;
} catch ( Exception $e ) {
if ( $temp ) {
unlink( $temp );
}
if ( file_exists( $dir_package ) ) {
try {
Extractor::rmdir( $dir_package );
} catch ( Exception $rmdir_e ) {
WP_CLI::warning( $rmdir_e->getMessage() );
}
}
WP_CLI::error( $e->getMessage() );
}
} elseif ( is_dir( $package_name ) && file_exists( $package_name . '/composer.json' ) ) {
$dir_package = $package_name;
if ( ! Utils\is_path_absolute( $dir_package ) ) {
$dir_package = getcwd() . DIRECTORY_SEPARATOR . $dir_package;
}
list( $package_name, $version ) = self::get_package_name_and_version_from_dir_package( $dir_package );
} else {
if ( false !== strpos( $package_name, ':' ) ) {
list( $package_name, $version ) = explode( ':', $package_name );
}
$package = $this->get_package_by_shortened_identifier( $package_name );
if ( ! $package ) {
WP_CLI::error( sprintf( "Invalid package: shortened identifier '%s' not found.", $package_name ) );
}
if ( is_string( $package ) ) {
if ( $this->is_git_repository( $package ) ) {
$git_package = $package;
if ( '' === $version ) {
$version = "dev-{$this->get_github_default_branch( $package_name, $insecure )}";
}
if ( '@stable' === $version ) {
$tag = $this->get_github_latest_release_tag( $package_name, $insecure );
$version = $this->guess_version_constraint_from_tag( $tag );
}
$package_name = $this->check_github_package_name( $package_name, $version, $insecure );
}
} elseif ( $package_name !== $package->getPrettyName() ) {
// BC support for specifying lowercase names for mixed-case package index packages - don't bother warning.
$package_name = $package->getPrettyName();
}
}
if ( $this->is_composer_v2() ) {
$package_name = function_exists( 'mb_strtolower' )
? mb_strtolower( $package_name )
: strtolower( $package_name );
}
if ( '' === $version ) {
$version = self::DEFAULT_DEV_BRANCH_CONSTRAINTS;
}
WP_CLI::log( sprintf( 'Installing package %s (%s)', $package_name, $version ) );
// Read the WP-CLI packages composer.json and do some initial error checking.
list( $json_path, $composer_backup, $composer_backup_decoded ) = $this->get_composer_json_path_backup_decoded();
// Revert on shutdown if `$revert` is true (set to false on success).
$revert = true;
$this->register_revert_shutdown_function( $json_path, $composer_backup, $revert );
// Add the 'require' to composer.json
WP_CLI::log( sprintf( 'Updating %s to require the package...', $json_path ) );
$json_manipulator = new JsonManipulator( $composer_backup );
$json_manipulator->addMainKey( 'name', 'wp-cli/wp-cli' );
$json_manipulator->addMainKey( 'version', self::get_wp_cli_version_composer() );
$json_manipulator->addLink( 'require', $package_name, $version, false /*sortPackages*/, true /*caseInsensitive*/ );
$json_manipulator->addConfigSetting( 'secure-http', true );
$package_args = [];
if ( $git_package ) {
WP_CLI::log( sprintf( 'Registering %s as a VCS repository...', $git_package ) );
$package_args = [
'type' => 'vcs',
'url' => $git_package,
];
$json_manipulator->addSubNode(
'repositories',
$package_name,
$package_args,
true /*caseInsensitive*/
);
} elseif ( $dir_package ) {
WP_CLI::log( sprintf( 'Registering %s as a path repository...', $dir_package ) );
$package_args = [
'type' => 'path',
'url' => $dir_package,
];
$json_manipulator->addSubNode(
'repositories',
$package_name,
$package_args,
true /*caseInsensitive*/
);
}
// If the composer file does not contain the current package index repository, refresh the repository definition.
if ( empty( $composer_backup_decoded['repositories']['wp-cli']['url'] ) || self::PACKAGE_INDEX_URL !== $composer_backup_decoded['repositories']['wp-cli']['url'] ) {
WP_CLI::log( 'Updating package index repository url...' );
$package_args = $this->composer_type_package;
$json_manipulator->addRepository(
'wp-cli',
$package_args
);
}
file_put_contents( $json_path, $json_manipulator->getContents() );
$composer = $this->get_composer();
// Set up the EventSubscriber
$event_subscriber = new PackageManagerEventSubscriber();
$composer->getEventDispatcher()->addSubscriber( $event_subscriber );
// Set up the installer
$install = Installer::create( new ComposerIO(), $composer );
$install->setUpdate( true ); // Installer class will only override composer.lock with this flag
$install->setPreferSource( true ); // Use VCS when VCS for easier contributions.
// Try running the installer, but revert composer.json if failed
WP_CLI::log( 'Using Composer to install the package...' );
WP_CLI::log( '---' );
$res = false;
try {
$res = $install->run();
} catch ( Exception $e ) {
WP_CLI::warning( $e->getMessage() );
}
// TODO: The --insecure flag should cause another Composer run with verify disabled.
WP_CLI::log( '---' );
if ( 0 === $res ) {
$revert = false;
WP_CLI::success( 'Package installed.' );
} else {
$res_msg = $res ? " (Composer return code {$res})" : ''; // $res may be null apparently.
WP_CLI::debug( "composer.json content:\n" . file_get_contents( $json_path ), 'packages' );
WP_CLI::error( "Package installation failed{$res_msg}." );
}
}
/**
* Lists installed WP-CLI packages.
*
* ## OPTIONS
*
* [--fields=<fields>]
* : Limit the output to specific fields. Defaults to all fields.
*
* [--format=<format>]
* : Render output in a particular format.
* ---
* default: table
* options:
* - table
* - csv
* - ids
* - json
* - yaml
* ---
*
* ## AVAILABLE FIELDS
*
* These fields will be displayed by default for each package:
*
* * name
* * authors
* * version
* * update
* * update_version
*
* These fields are optionally available:
*
* * description
*
* ## EXAMPLES
*
* # List installed packages.
* $ wp package list
* +-----------------------+------------------+----------+-----------+----------------+
* | name | authors | version | update | update_version |
* +-----------------------+------------------+----------+-----------+----------------+
* | wp-cli/server-command | Daniel Bachhuber | dev-main | available | 2.x-dev |
* +-----------------------+------------------+----------+-----------+----------------+
*
* @subcommand list
*/
public function list_( $args, $assoc_args ) {
$this->set_composer_auth_env_var();
$this->show_packages( 'list', $this->get_installed_packages(), $assoc_args );
}
/**
* Gets the path to an installed WP-CLI package, or the package directory.
*
* If you want to contribute to a package, this is a great way to jump to it.
*
* ## OPTIONS
*
* [<name>]
* : Name of the package to get the directory for.
*
* ## EXAMPLES
*
* # Get package path.
* $ wp package path
* /home/person/.wp-cli/packages/
*
* # Get path to an installed package.
* $ wp package path wp-cli/server-command
* /home/person/.wp-cli/packages/vendor/wp-cli/server-command
*
* # Change directory to package path.
* $ cd $(wp package path) && pwd
* /home/vagrant/.wp-cli/packages
*/
public function path( $args ) {
$packages_dir = WP_CLI::get_runner()->get_packages_dir_path();
if ( ! empty( $args ) ) {
$packages_dir .= 'vendor/' . $args[0];
if ( ! is_dir( $packages_dir ) ) {
WP_CLI::error( 'Invalid package name.' );
}
}
WP_CLI::line( $packages_dir );
}
/**
* Updates all installed WP-CLI packages to their latest version.
*
* ## EXAMPLES
*
* $ wp package update
* Using Composer to update packages...
* ---
* Loading composer repositories with package information
* Updating dependencies
* Resolving dependencies through SAT
* Dependency resolution completed in 0.074 seconds
* Analyzed 1062 packages to resolve dependencies
* Analyzed 22383 rules to resolve dependencies
* Writing lock file
* Generating autoload files
* ---
* Success: Packages updated.
*/
public function update() {
$this->set_composer_auth_env_var();
$composer = $this->get_composer();
// Set up the EventSubscriber
$event_subscriber = new PackageManagerEventSubscriber();
$composer->getEventDispatcher()->addSubscriber( $event_subscriber );
// Set up the installer
$install = Installer::create( new ComposerIO(), $composer );
$install->setUpdate( true ); // Installer class will only override composer.lock with this flag
$install->setPreferSource( true ); // Use VCS when VCS for easier contributions.
WP_CLI::log( 'Using Composer to update packages...' );
WP_CLI::log( '---' );
$res = false;
try {
$res = $install->run();
} catch ( Exception $e ) {
WP_CLI::warning( $e->getMessage() );
}
WP_CLI::log( '---' );
// TODO: The --insecure (to be added here) flag should cause another Composer run with verify disabled.
if ( 0 === $res ) {
WP_CLI::success( 'Packages updated.' );
} else {
$res_msg = $res ? " (Composer return code {$res})" : ''; // $res may be null apparently.
WP_CLI::error( "Failed to update packages{$res_msg}." );
}
}
/**
* Uninstalls a WP-CLI package.
*
* ## OPTIONS
*
* <name>
* : Name of the package to uninstall.
*
* [--insecure]
* : Retry downloads without certificate validation if TLS handshake fails. Note: This makes the request vulnerable to a MITM attack.
*
* ## EXAMPLES
*
* # Uninstall package.
* $ wp package uninstall wp-cli/server-command
* Removing require statement for package 'wp-cli/server-command' from /home/person/.wp-cli/packages/composer.json
* Removing repository details from /home/person/.wp-cli/packages/composer.json
* Removing package directories and regenerating autoloader...
* Success: Uninstalled package.
*/
public function uninstall( $args, $assoc_args ) {
list( $package_name ) = $args;
$insecure = (bool) Utils\get_flag_value( $assoc_args, 'insecure', false );
$this->set_composer_auth_env_var();
$package = $this->get_installed_package_by_name( $package_name );
if ( false === $package ) {
$package_name = $this->get_package_by_shortened_identifier( $package_name );
if ( false === $package_name ) {
WP_CLI::error( 'Package not installed.' );
}
$version = "dev-{$this->get_github_default_branch( $package_name, $insecure )}";
$matches = [];
if ( preg_match( '#^(?:https?://github\.com/|git@github\.com:)(?<repo_name>.*?).git$#', $package_name, $matches ) ) {
$package_name = $this->check_git_package_name( $matches['repo_name'], $package_name, $version, $insecure );
}
} else {
$package_name = $package->getPrettyName(); // Make sure package name is what's in composer.json.
}
// Read the WP-CLI packages composer.json and do some initial error checking.
list( $json_path, $composer_backup, $composer_backup_decoded ) = $this->get_composer_json_path_backup_decoded();
// Revert on shutdown if `$revert` is true (set to false on success).
$revert = true;
$this->register_revert_shutdown_function( $json_path, $composer_backup, $revert );
// Remove the 'require' from composer.json.
WP_CLI::log( sprintf( 'Removing require statement for package \'%s\' from %s', $package_name, $json_path ) );
$manipulator = new JsonManipulator( $composer_backup );
$manipulator->removeSubNode( 'require', $package_name, true /*caseInsensitive*/ );
// Remove the 'repository' details from composer.json.
WP_CLI::log( sprintf( 'Removing repository details from %s', $json_path ) );
$manipulator->removeSubNode( 'repositories', $package_name, true /*caseInsensitive*/ );
file_put_contents( $json_path, $manipulator->getContents() );
$composer = $this->get_composer();
// Set up the installer.
$install = Installer::create( new NullIO(), $composer );
$install->setUpdate( true ); // Installer class will only override composer.lock with this flag
$install->setPreferSource( true ); // Use VCS when VCS for easier contributions.
WP_CLI::log( 'Removing package directories and regenerating autoloader...' );
$res = false;
try {
$res = $install->run();
} catch ( Exception $e ) {
WP_CLI::warning( $e->getMessage() );
}
if ( 0 === $res ) {
$revert = false;
WP_CLI::success( 'Uninstalled package.' );
} else {
$res_msg = $res ? " (Composer return code {$res})" : ''; // $res may be null apparently.
WP_CLI::error( "Package removal failed{$res_msg}." );
}
}
/**
* Checks whether a package is a WP-CLI community package based
* on membership in our package index.
*
* @param object $package A package object
* @return bool
*/
private function is_community_package( $package ) {
return $this->package_index()->hasPackage( $package );
}
/**
* Gets a Composer instance.
*/
private function get_composer() {
$this->avoid_composer_ca_bundle();
try {
$composer_path = $this->get_composer_json_path();
// Composer's auto-load generating code makes some assumptions about where
// the 'vendor-dir' is, and where Composer is running from.
// Best to just pretend we're installing a package from ~/.wp-cli or similar
chdir( pathinfo( $composer_path, PATHINFO_DIRNAME ) );
// Prevent DateTime error/warning when no timezone set.
// Note: The package is loaded before WordPress load, For environments that don't have set time in php.ini.
// phpcs:ignore WordPress.DateTime.RestrictedFunctions.timezone_change_date_default_timezone_set,WordPress.PHP.NoSilencedErrors.Discouraged
date_default_timezone_set( @date_default_timezone_get() );
$composer = Factory::create( new NullIO(), $composer_path );
} catch ( Exception $e ) {
WP_CLI::error( sprintf( 'Failed to get composer instance: %s', $e->getMessage() ) );
}
return $composer;
}
/**
* Gets all of the community packages.
*
* @return array
*/
private function get_community_packages() {
static $community_packages;
if ( null === $community_packages ) {
$this->avoid_composer_ca_bundle();
try {
$community_packages = $this->package_index()->getPackages();
} catch ( Exception $e ) {
WP_CLI::error( $e->getMessage() );
}
}
return $community_packages;
}
/**
* Gets the package index instance
*
* We need to construct the instance manually, because there's no way to select
* a particular instance using $composer->getRepositoryManager()
*
* @return ComposerRepository
*/
private function package_index() {
static $package_index;
if ( ! $package_index ) {
$config_args = [
'config' => [
'secure-http' => true,
'home' => dirname( $this->get_composer_json_path() ),
],
];
$config = new Config();
$config->merge( $config_args );
$config->setConfigSource( new JsonConfigSource( $this->get_composer_json() ) );
$io = new NullIO();
try {
if ( $this->is_composer_v2() ) {
$http_downloader = new HttpDownloader( $io, $config );
$package_index = new ComposerRepository( [ 'url' => self::PACKAGE_INDEX_URL ], $io, $config, $http_downloader );
} else {
$package_index = new ComposerRepository( [ 'url' => self::PACKAGE_INDEX_URL ], $io, $config );
}
} catch ( Exception $e ) {
WP_CLI::error( $e->getMessage() );
}
}
return $package_index;
}
/**
* Displays a set of packages
*
* @param string $context
* @param array
* @param array
*/
private function show_packages( $context, $packages, $assoc_args ) {
if ( 'list' === $context ) {
$default_fields = [
'name',
'authors',
'version',
'update',
'update_version',
];
} elseif ( 'browse' === $context ) {
$default_fields = [
'name',
'description',
'authors',
'version',
];
}
$defaults = [
'fields' => implode( ',', $default_fields ),
'format' => 'table',
];
$assoc_args = array_merge( $defaults, $assoc_args );
$composer = $this->get_composer();
$list = [];
foreach ( $packages as $package ) {
$name = $package->getPrettyName();
if ( isset( $list[ $name ] ) ) {
$list[ $name ]['version'][] = $package->getPrettyVersion();
} else {
$package_output = [];
$package_output['name'] = $package->getPrettyName();
$package_output['description'] = $package->getDescription();
$package_output['authors'] = implode( ', ', array_column( (array) $package->getAuthors(), 'name' ) );
$package_output['version'] = [ $package->getPrettyVersion() ];
$update = 'none';
$update_version = '';
if ( 'list' === $context ) {
try {
$latest = $this->find_latest_package( $package, $composer, null );
if ( $latest && $latest->getFullPrettyVersion() !== $package->getFullPrettyVersion() ) {
$update = 'available';
$update_version = $latest->getPrettyVersion();
}
} catch ( Exception $e ) {
WP_CLI::warning( $e->getMessage() );
$update = 'error';
$update_version = $update;
}
}
$package_output['update'] = $update;
$package_output['update_version'] = $update_version;
$package_output['pretty_name'] = $package->getPrettyName(); // Deprecated but kept for BC with package-command 1.0.8.
$list[ $package_output['name'] ] = $package_output;
}
}
$list = array_map(
function ( $package ) {
$package['version'] = implode( ', ', $package['version'] );
return $package;
},
$list
);
ksort( $list );
if ( 'ids' === $assoc_args['format'] ) {
$list = array_keys( $list );
}
Utils\format_items( $assoc_args['format'], $list, $assoc_args['fields'] );
}
/**
* Gets a package by its shortened identifier.
*
* A shortened identifier has the form `<vendor>/<package>`.
*
* This method first checks the deprecated package index, for BC reasons,
* and then falls back to the corresponding GitHub URL.
*
* @param string $package_name Name of the package to get.
* @param bool $insecure Optional. Whether to insecurely retry downloads that failed TLS handshake. Defaults
* to false.
*/
private function get_package_by_shortened_identifier( $package_name, $insecure = false ) {
// Check the package index first, so we don't break existing behavior.
$lc_package_name = strtolower( $package_name ); // For BC check.
foreach ( $this->get_community_packages() as $package ) {
if ( $package_name === $package->getPrettyName() ) {
return $package;
}
// For BC allow getting by lowercase name.
if ( $lc_package_name === $package->getName() ) {
return $package;
}
}
$options = [ 'insecure' => $insecure ];
// Check if the package exists on Packagist.
$url = "https://repo.packagist.org/p/{$package_name}.json";
$response = Utils\http_request( 'GET', $url, null, [], $options );
if ( 20 === (int) substr( $response->status_code, 0, 2 ) ) {
return $package_name;
}
// Fall back to GitHub URL if we had no match yet.
$url = "https://github.com/{$package_name}.git";
$github_token = getenv( 'GITHUB_TOKEN' ); // Use GITHUB_TOKEN if available to avoid authorization failures or rate-limiting.
$headers = $github_token ? [ 'Authorization' => 'token ' . $github_token ] : [];
$response = Utils\http_request( 'GET', $url, null /*data*/, $headers, $options );
if ( 20 === (int) substr( $response->status_code, 0, 2 ) ) {
return $url;
}
// Fall back to GitLab URL if we had no match yet.
$url = "https://gitlab.com/{$package_name}.git";
$gitlab_token = getenv( 'GITLAB_TOKEN' ); // Use GITLAB_TOKEN if available to avoid authorization failures or rate-limiting.
$headers = $github_token ? [ 'Authorization' => 'token ' . $github_token ] : [];
$headers = $gitlab_token && strpos( $package_name, '://gitlab.com/' ) !== false ? [ 'PRIVATE-TOKEN' => $gitlab_token ] : [];
$response = Utils\http_request( 'GET', $url, null /*data*/, $headers, $options );
if ( 20 === (int) substr( $response->status_code, 0, 2 ) ) {
return $url;
}
return false;
}
/**
* Gets the installed community packages.
*/
private function get_installed_packages() {
$composer = $this->get_composer();
$repo = $composer->getRepositoryManager()->getLocalRepository();
$existing = json_decode( file_get_contents( $this->get_composer_json_path() ), true );
$installed_package_keys = ! empty( $existing['require'] ) ? array_keys( $existing['require'] ) : [];
if ( empty( $installed_package_keys ) ) {
return [];
}
// For use by legacy incorrect name check.
$lc_installed_package_keys = array_map( 'strtolower', $installed_package_keys );
$installed_packages = [];
foreach ( $repo->getCanonicalPackages() as $package ) {
$idx = array_search( $package->getName(), $lc_installed_package_keys, true );
// Use pretty name as it's case sensitive and what's in composer.json (or at least should be).
if ( in_array( $package->getPrettyName(), $installed_package_keys, true ) ) {
$installed_packages[] = $package;
} elseif ( false !== $idx ) { // Legacy incorrect name check.
if ( ! $this->is_composer_v2() ) {
WP_CLI::warning( sprintf( "Found package '%s' misnamed '%s' in '%s'.", $package->getPrettyName(), $installed_package_keys[ $idx ], $this->get_composer_json_path() ) );
}
$installed_packages[] = $package;
}
}
return $installed_packages;
}
/**
* Gets an installed package by its name.
*/
private function get_installed_package_by_name( $package_name ) {
foreach ( $this->get_installed_packages() as $package ) {
if ( $package_name === $package->getPrettyName() ) {
return $package;
}
// Also check non-pretty (lowercase) name in case of legacy incorrect name.
if ( $package_name === $package->getName() ) {
return $package;
}
}
return false;
}
/**
* Checks if the package name provided is already installed.
*/
private function is_package_installed( $package_name ) {
if ( $this->get_installed_package_by_name( $package_name ) ) {
return true;
} else {
return false;
}
}
/**
* Gets the name of the package from the composer.json in a directory path
*
* @param string $dir_package
* @return array Two-element array containing package name and version.
*/
private static function get_package_name_and_version_from_dir_package( $dir_package ) {
$composer_file = $dir_package . '/composer.json';
if ( ! file_exists( $composer_file ) ) {
WP_CLI::error( sprintf( "Invalid package: composer.json file '%s' not found.", $composer_file ) );
}
$composer_data = json_decode( file_get_contents( $composer_file ), true );
if ( null === $composer_data ) {
WP_CLI::error( sprintf( "Invalid package: failed to parse composer.json file '%s' as json.", $composer_file ) );
}
if ( empty( $composer_data['name'] ) ) {
WP_CLI::error( sprintf( "Invalid package: no name in composer.json file '%s'.", $composer_file ) );
}
$package_name = $composer_data['name'];
$version = self::DEFAULT_DEV_BRANCH_CONSTRAINTS;
if ( ! empty( $composer_data['version'] ) ) {
$version = $composer_data['version'];
}
return [ $package_name, $version ];
}
/**
* Gets the WP-CLI packages composer.json object.
*/
private function get_composer_json() {
return new JsonFile( $this->get_composer_json_path() );
}
/**
* Gets the absolute path to the WP-CLI packages composer.json.
*/
private function get_composer_json_path() {
static $composer_path;
if ( null === $composer_path || getenv( 'WP_CLI_TEST_PACKAGE_GET_COMPOSER_JSON_PATH' ) ) {
if ( getenv( 'WP_CLI_PACKAGES_DIR' ) ) {
$composer_path = Utils\trailingslashit( getenv( 'WP_CLI_PACKAGES_DIR' ) ) . 'composer.json';
} else {
$composer_path = Utils\trailingslashit( Utils\get_home_dir() ) . '.wp-cli/packages/composer.json';
}
// `composer.json` and its directory might need to be created
if ( ! file_exists( $composer_path ) ) {
$composer_path = $this->create_default_composer_json( $composer_path );
} else {
$composer_path = realpath( $composer_path );
if ( false === $composer_path ) {
$error = error_get_last();
WP_CLI::error( sprintf( "Composer path '%s' for packages/composer.json not found: %s", $composer_path, $error['message'] ) );
}
}
}
return $composer_path;
}
/**
* Gets the WP-CLI version for composer.json
*/
private static function get_wp_cli_version_composer() {
preg_match( '#^[0-9\.]+(-(alpha|beta)[^-]{0,})?#', WP_CLI_VERSION, $matches );
$version = isset( $matches[0] ) ? $matches[0] : '';
return $version;
}
/**
* Creates a default WP-CLI packages composer.json.
*
* @param string $composer_path Where the composer.json should be created
* @return string Returns the absolute path of the newly created default WP-CLI packages composer.json.
*/
private function create_default_composer_json( $composer_path ) {
$composer_dir = pathinfo( $composer_path, PATHINFO_DIRNAME );
if ( ! is_dir( $composer_dir ) ) {
if ( ! @mkdir( $composer_dir, 0777, true ) ) { // @codingStandardsIgnoreLine
$error = error_get_last();
WP_CLI::error( sprintf( "Composer directory '%s' for packages couldn't be created: %s", $composer_dir, $error['message'] ) );
}
}
$composer_dir = realpath( $composer_dir );
if ( false === $composer_dir ) {
$error = error_get_last();
WP_CLI::error( sprintf( "Composer directory '%s' for packages not found: %s", $composer_dir, $error['message'] ) );
}
$composer_path = Utils\trailingslashit( $composer_dir ) . Utils\basename( $composer_path );
$json_file = new JsonFile( $composer_path );
$repositories = (object) [
'wp-cli' => (object) $this->composer_type_package,
];
$options = [
'name' => 'wp-cli/wp-cli',
'description' => 'Installed community packages used by WP-CLI',
'version' => self::get_wp_cli_version_composer(),
'authors' => [ (object) $this->author_data ],
'homepage' => self::PACKAGE_INDEX_URL,
'require' => new stdClass(),
'require-dev' => new stdClass(),
'minimum-stability' => 'dev',
'prefer-stable' => true,
'license' => 'MIT',
'repositories' => $repositories,
];
try {
$json_file->write( $options );
} catch ( Exception $e ) {
WP_CLI::error( $e->getMessage() );
}
return $composer_path;
}
/**
* Given a package, this finds the latest package matching it
*
* @param PackageInterface $package
* @param Composer $composer
* @param string $phpVersion
* @param bool $minorOnly
*
* @return PackageInterface|null
*/
private function find_latest_package( PackageInterface $package, Composer $composer, $php_version, $minor_only = false ) {
// Find the latest version allowed in this pool/repository set.
$name = $package->getPrettyName();
$version_selector = $this->get_version_selector( $composer );
$stability = $composer->getPackage()->getMinimumStability();
$flags = $composer->getPackage()->getStabilityFlags();
if ( isset( $flags[ $name ] ) ) {
$stability = array_search( $flags[ $name ], BasePackage::$stabilities, true );
}
$best_stability = $stability;
if ( $composer->getPackage()->getPreferStable() ) {
$best_stability = $package->getStability();
}
$target_version = null;
if ( 0 === strpos( $package->getVersion(), 'dev-' ) ) {
$target_version = $package->getVersion();
}
if ( null === $target_version && $minor_only ) {
$target_version = '^' . $package->getVersion();
}
if ( $this->is_composer_v2() ) {
return $version_selector->findBestCandidate( $name, $target_version, $best_stability );
}
return $version_selector->findBestCandidate( $name, $target_version, $php_version, $best_stability );
}
private function get_version_selector( Composer $composer ) {
if ( ! $this->version_selector ) {
if ( $this->is_composer_v2() ) {
$repository_set = new Repository\RepositorySet(
$composer->getPackage()->getMinimumStability(),
$composer->getPackage()->getStabilityFlags()
);
$repository_set->addRepository( new CompositeRepository( $composer->getRepositoryManager()->getRepositories() ) );
$this->version_selector = new VersionSelector( $repository_set );
} else {
$pool = new Pool( $composer->getPackage()->getMinimumStability(), $composer->getPackage()->getStabilityFlags() );
$pool->addRepository( new CompositeRepository( $composer->getRepositoryManager()->getRepositories() ) );
$this->version_selector = new VersionSelector( $pool );
}
}
return $this->version_selector;
}
/**
* Checks whether a given package is a git repository.
*
* @param string $package Package name to check.
*
* @return bool Whether the package is a git repository.
*/
private function is_git_repository( $package ) {
return '.git' === strtolower( substr( $package, -4, 4 ) );
}
/**
* Checks that `$package_name` matches the name in composer.json at Github.com, and return corrected value if not.
*
* @param string $package_name Package name to check.
* @param string $version Optional. Package version. Defaults to empty string.
* @param bool $insecure Optional. Whether to insecurely retry downloads that failed TLS handshake. Defaults
* to false.
*/
private function check_github_package_name( $package_name, $version = '', $insecure = false ) {
$github_token = getenv( 'GITHUB_TOKEN' ); // Use GITHUB_TOKEN if available to avoid authorization failures or rate-limiting.
$headers = $github_token ? [ 'Authorization' => 'token ' . $github_token ] : [];
$options = [ 'insecure' => $insecure ];
// Generate raw git URL of composer.json file.
$raw_content_url = "https://raw.githubusercontent.com/{$package_name}/{$this->get_raw_git_version( $version )}/composer.json";
$response = Utils\http_request( 'GET', $raw_content_url, null /*data*/, $headers, $options );
if ( 20 !== (int) substr( $response->status_code, 0, 2 ) ) {
// Could not get composer.json. Possibly private so warn and return best guess from input (always xxx/xxx).
WP_CLI::warning(
sprintf(
"Couldn't download composer.json file from '%s' (HTTP code %d). Presuming package name is '%s'.",
$raw_content_url,
$response->status_code,
$package_name
)
);
return $package_name;
}
// Convert composer.json JSON to Array.
$composer_content_as_array = json_decode( $response->body, true );
if ( null === $composer_content_as_array ) {
WP_CLI::error( sprintf( "Failed to parse '%s' as json.", $raw_content_url ) );
}
if ( empty( $composer_content_as_array['name'] ) ) {
WP_CLI::error( sprintf( "Invalid package: no name in composer.json file '%s'.", $raw_content_url ) );
}
// Package name in composer.json that is hosted on GitHub.
$package_name_on_repo = $composer_content_as_array['name'];
// If package name and repository name are not identical, then fix it.
if ( $package_name !== $package_name_on_repo ) {
WP_CLI::warning( sprintf( "Package name mismatch...Updating from git name '%s' to composer.json name '%s'.", $package_name, $package_name_on_repo ) );
$package_name = $package_name_on_repo;
}
return $package_name;
}
/**
* Checks that `$package_name` matches the name in composer.json at the corresponding upstream repository, and return corrected value if not.
*
* @param string $package_name Package name to check.
* @param string $url URL to fetch the package from.
* @param string $version Optional. Package version. Defaults to empty string.
* @param bool $insecure Optional. Whether to insecurely retry downloads that failed TLS handshake. Defaults
* to false.
*/
private function check_git_package_name( $package_name, $url = '', $version = '', $insecure = false ) {
if ( $url && ( ( strpos( $url, '://gitlab.com/' ) !== false ) || ( strpos( $url, 'git@gitlab.com:' ) !== false ) ) ) {
$matches = [];
preg_match( '#gitlab.com[:/](.*?)\.git#', $url, $matches );
return $this->check_gitlab_package_name( $matches[1], $version, $insecure );
}
return $this->check_github_package_name( $package_name, $version, $insecure );
}
/**
* Checks that `$package_name` matches the name in composer.json at GitLab.com, and return corrected value if not.
*
* @param string $project_name Package name to check.
* @param string $version Optional. Package version. Defaults to empty string.
* @param bool $insecure Optional. Whether to insecurely retry downloads that failed TLS handshake. Defaults
* to false.
*/
private function check_gitlab_package_name( $project_name, $version = '', $insecure = false ) {
$options = [ 'insecure' => $insecure ];
// Generate raw git URL of composer.json file.
$raw_content_public_url = 'https://gitlab.com/' . $project_name . '/-/raw/' . $this->get_raw_git_version( $version ) . '/composer.json';
$raw_content_private_url = 'https://gitlab.com/api/v4/projects/' . rawurlencode( $project_name ) . '/repository/files/composer.json/raw?ref=' . $this->get_raw_git_version( $version );
$matches = [];
preg_match( '#([^:\/]+\/[^\/]+$)#', $project_name, $matches );
$package_name = $matches[1];
$gitlab_token = getenv( 'GITLAB_TOKEN' ); // Use GITLAB_TOKEN if available to avoid authorization failures or rate-limiting.
$response = Utils\http_request( 'GET', $raw_content_public_url, null /*data*/, [], $options );
if ( ! $gitlab_token && ( $response->status_code < 200 || $response->status_code >= 300 ) ) {
// Could not get composer.json. Possibly private so warn and return best guess from input (always xxx/xxx).
WP_CLI::warning( sprintf( "Couldn't download composer.json file from '%s' (HTTP code %d). Presuming package name is '%s'.", $raw_content_public_url, $response->status_code, $package_name ) );
return $package_name;
}
if ( strpos( $response->headers['content-type'], 'text/html' ) === 0 ) {
$headers = $gitlab_token ? [ 'PRIVATE-TOKEN' => $gitlab_token ] : [];
$response = Utils\http_request( 'GET', $raw_content_private_url, null /*data*/, $headers, $options );
if ( $response->status_code < 200 || $response->status_code >= 300 ) {
// Could not get composer.json. Possibly private so warn and return best guess from input (always xxx/xxx).
WP_CLI::warning( sprintf( "Couldn't download composer.json file from '%s' (HTTP code %d). Presuming package name is '%s'.", $raw_content_private_url, $response->status_code, $package_name ) );
return $package_name;
}
}
// Convert composer.json JSON to Array.
$composer_content_as_array = json_decode( $response->body, true );
if ( null === $composer_content_as_array ) {
WP_CLI::error( sprintf( "Failed to parse '%s' as json.", $response->url ) );
}
if ( empty( $composer_content_as_array['name'] ) ) {
WP_CLI::error( sprintf( "Invalid package: no name in composer.json file '%s'.", $response->url ) );
}
// Package name in composer.json that is hosted on Gitlab.
$package_name_on_repo = $composer_content_as_array['name'];
// If package name and repository name are not identical, then fix it.
if ( $package_name !== $package_name_on_repo ) {
WP_CLI::warning( sprintf( "Package name mismatch...Updating from git name '%s' to composer.json name '%s'.", $package_name, $package_name_on_repo ) );
$package_name = $package_name_on_repo;
}
return $package_name;
}
/**
* Get the version to use for raw GitHub request. Very basic.
*
* @string $version Package version.
* @string Version to use for GitHub request.
*/
private function get_raw_git_version( $version ) {
if ( '' === $version ) {
return 'master';
}
// If Composer hash given then just use whatever's after it.
$hash_pos = strpos( $version, '#' );
if ( false !== $hash_pos ) {
return substr( $version, $hash_pos + 1 );
}
// Strip any Composer 'dev-' prefix.
if ( 0 === strncmp( $version, 'dev-', 4 ) ) {
$version = substr( $version, 4 );
}
// Ignore/strip any relative suffixes.
return str_replace( [ '^', '~' ], '', $version );
}
/**
* Gets the release tag for the latest stable release of a GitHub repository.
*
* @param string $package_name Name of the repository.
*
* @return string Release tag.
*/
private function get_github_latest_release_tag( $package_name, $insecure ) {
$url = "https://api.github.com/repos/{$package_name}/releases/latest";
$options = [ 'insecure' => $insecure ];
$response = Utils\http_request( 'GET', $url, null, [], $options );
if ( 20 !== (int) substr( $response->status_code, 0, 2 ) ) {
WP_CLI::warning( 'Could not guess stable version from GitHub repository, falling back to master branch' );
return 'master';
}
$package_data = json_decode( $response->body );
if ( JSON_ERROR_NONE !== json_last_error() ) {
WP_CLI::warning( 'Could not guess stable version from GitHub repository, falling back to master branch' );
return 'master';
}
$tag = $package_data->tag_name;
WP_CLI::debug( "Extracted latest stable release tag: {$tag}", 'packages' );
return $tag;
}
/**
* Guesses the version constraint from a release tag.
*
* @param string $tag Release tag to guess the version constraint from.
*
* @return string Version constraint.
*/
private function guess_version_constraint_from_tag( $tag ) {
$matches = [];
if ( 1 !== preg_match( '/(?:version|v)\s*((?:[0-9]+\.?)+)(?:-.*)/i', $tag, $matches ) ) {
return $tag;
}
$constraint = "^{$matches[1]}";
WP_CLI::debug( "Guessing version constraint to use: {$constraint}", 'packages' );
return $constraint;
}
/**
* Sets `COMPOSER_AUTH` environment variable (which Composer merges into the config setup in `Composer\Factory::createConfig()`) depending on available environment variables.
* Avoids authorization failures when accessing various sites.
*/
private function set_composer_auth_env_var() {
$changed = false;
$composer_auth = getenv( 'COMPOSER_AUTH' );
if ( false !== $composer_auth ) {
$composer_auth = json_decode( $composer_auth, true /*assoc*/ );
}
if ( empty( $composer_auth ) || ! is_array( $composer_auth ) ) {
$composer_auth = [];
}
$github_token = getenv( 'GITHUB_TOKEN' );
if ( ! isset( $composer_auth['github-oauth'] ) && is_string( $github_token ) ) {
$composer_auth['github-oauth'] = [ 'github.com' => $github_token ];
$changed = true;
}
if ( $changed ) {
putenv( 'COMPOSER_AUTH=' . json_encode( $composer_auth ) );
}
}
/**
* Avoid using default Composer CA bundle if in phar as we don't include it.
* See https://github.com/composer/ca-bundle/blob/1.1.0/src/CaBundle.php#L64
*/
private function avoid_composer_ca_bundle() {
if ( Utils\inside_phar() && ! getenv( 'SSL_CERT_FILE' ) && ! getenv( 'SSL_CERT_DIR' ) && ! ini_get( 'openssl.cafile' ) && ! ini_get( 'openssl.capath' ) ) {
$certificate_path = Utils\extract_from_phar( RequestsLibrary::get_bundled_certificate_path() );
putenv( "SSL_CERT_FILE={$certificate_path}" );
}
}
/**
* Reads the WP-CLI packages composer.json, checking validity and returning array containing its path, contents, and decoded contents.
*
* @return array Indexed array containing the path, the contents, and the decoded contents of the WP-CLI packages composer.json.
*/
private function get_composer_json_path_backup_decoded() {
$composer_json_obj = $this->get_composer_json();
$json_path = $composer_json_obj->getPath();
$composer_backup = file_get_contents( $json_path );
if ( false === $composer_backup ) {
$error = error_get_last();
WP_CLI::error( sprintf( "Failed to read '%s': %s", $json_path, $error['message'] ) );
}
try {
$composer_backup_decoded = $composer_json_obj->read();
} catch ( Exception $e ) {
WP_CLI::error( sprintf( "Failed to parse '%s' as json: %s", $json_path, $e->getMessage() ) );
}
return [ $json_path, $composer_backup, $composer_backup_decoded ];
}
/**
* Registers a PHP shutdown function to revert composer.json unless
* referenced `$revert` flag is false.
*
* @param string $json_path Path to composer.json.
* @param string $composer_backup Original contents of composer.json.
* @param bool &$revert Flags whether to revert or not.
*/
private function register_revert_shutdown_function( $json_path, $composer_backup, &$revert ) {
// Allocate all needed memory beforehand as much as possible.
$revert_msg = "Reverted composer.json.\n";
$revert_fail_msg = "Failed to revert composer.json.\n";
$memory_msg = "WP-CLI ran out of memory. Please see https://bit.ly/wpclimem for further help.\n";
$memory_string = 'Allowed memory size of';
$error_array = [
'type' => 42,
'message' => 'Some random dummy string to take up memory',
'file' => 'Another random string, which would be a filename this time',
'line' => 314,
];
register_shutdown_function(
static function () use (
$json_path,
$composer_backup,
&$revert,
$revert_msg,
$revert_fail_msg,
$memory_msg,
$memory_string,
$error_array
) {
if ( $revert ) {
if ( false !== file_put_contents( $json_path, $composer_backup ) ) {
fwrite( STDERR, $revert_msg );
} else {
fwrite( STDERR, $revert_fail_msg );
}
}
$error_array = error_get_last();
if ( is_array( $error_array ) && false !== strpos( $error_array['message'], $memory_string ) ) {
fwrite( STDERR, $memory_msg );
}
}
);
}
/**
* Check whether we are dealing with Composer version 2.0.0+.
*
* @return bool
*/
private function is_composer_v2() {
return version_compare( Composer::getVersion(), '2.0.0', '>=' );
}
/**
* Try to retrieve default branch via GitHub API.
*
* @param string $package_name GitHub package name to retrieve the default branch from.
* @param bool $insecure Optional. Whether to insecurely retry downloads that failed TLS handshake. Defaults
* to false.
* @return string Default branch, or 'master' if it could not be retrieved.
*/
private function get_github_default_branch( $package_name, $insecure = false ) {
$github_token = getenv( 'GITHUB_TOKEN' ); // Use GITHUB_TOKEN if available to avoid authorization failures or rate-limiting.
$headers = $github_token ? [ 'Authorization' => 'token ' . $github_token ] : [];
$options = [ 'insecure' => $insecure ];
$matches = [];
if ( preg_match( '#^(?:https?://github\.com/|git@github\.com:)(?<repo_name>.*?).git$#', $package_name, $matches ) ) {
$package_name = $matches['repo_name'];
}
$github_api_repo_url = "https://api.github.com/repos/{$package_name}";
$response = Utils\http_request( 'GET', $github_api_repo_url, null /*data*/, $headers, $options );
if ( 20 !== (int) substr( $response->status_code, 0, 2 ) ) {
WP_CLI::warning(
sprintf(
"Couldn't fetch default branch for package '%s' (HTTP code %d). Presuming default branch is 'master'.",
$package_name,
$response->status_code
)
);
return 'master';
}
$package_data = json_decode( $response->body );
if ( JSON_ERROR_NONE !== json_last_error() ) {
WP_CLI::warning( "Couldn't fetch default branch for package '%s' (failed to decode JSON response). Presuming default branch is 'master'." );
return 'master';
}
$default_branch = $package_data->default_branch;
if ( ! is_string( $default_branch ) || empty( $default_branch ) ) {
WP_CLI::warning(
sprintf(
"Couldn't fetch default branch for package '%s'. Presuming default branch is 'master'.",
$package_name
)
);
return 'master';
}
WP_CLI::debug( "Detected package default branch: {$default_branch}", 'packages' );
return $default_branch;
}
}