diff --git a/features/package-install.feature b/features/package-install.feature index 9aaaf2c1e..96b1e2206 100644 --- a/features/package-install.feature +++ b/features/package-install.feature @@ -128,7 +128,7 @@ Feature: Install WP-CLI packages Then the return code should be 0 And STDERR should contain: """ - Warning: Package name mismatch...Updating the name with correct value. + Warning: Package name mismatch...Updating from git name 'wp-cli-test/repository-name' to composer.json name 'wp-cli-test/package-name'. """ And STDOUT should contain: """ @@ -195,24 +195,39 @@ Feature: Install WP-CLI packages """ @github-api - Scenario: Install a package from a Git URL with mixed case git name but lower case composer.json name + Scenario: Install a package from a Git URL with mixed-case git name but lowercase composer.json name Given an empty directory When I try `wp package install https://github.com/CapitalWPCLI/examplecommand.git` Then the return code should be 0 And STDERR should contain: """ - Warning: Package name mismatch...Updating the name with correct value. + Warning: Package name mismatch...Updating from git name 'CapitalWPCLI/examplecommand' to composer.json name 'capitalwpcli/examplecommand'. + """ + And STDOUT should contain: + """ + Installing package capitalwpcli/examplecommand (dev-master) + Updating {PACKAGE_PATH}composer.json to require the package... + Registering https://github.com/CapitalWPCLI/examplecommand.git as a VCS repository... + Using Composer to install the package... """ And STDOUT should contain: """ Success: Package installed. """ + And the {PACKAGE_PATH}composer.json file should contain: + """ + "capitalwpcli/examplecommand" + """ + And the {PACKAGE_PATH}composer.json file should not contain: + """ + "CapitalWPCLI/examplecommand" + """ - When I run `wp package list --fields=name,pretty_name` + When I run `wp package list --fields=name` Then STDOUT should be a table containing rows: - | name | pretty_name | - | capitalwpcli/examplecommand | capitalwpcli/examplecommand | + | name | + | capitalwpcli/examplecommand | When I run `wp hello-world` Then STDOUT should contain: @@ -221,7 +236,7 @@ Feature: Install WP-CLI packages """ @github-api - Scenario: Install a package from a Git URL with mixed case git name and the same mixed case composer.json name + Scenario: Install a package from a Git URL with mixed-case git name and the same mixed-case composer.json name Given an empty directory When I run `wp package install https://github.com/gitlost/TestMixedCaseCommand.git` @@ -230,11 +245,19 @@ Feature: Install WP-CLI packages """ Success: Package installed. """ + And the {PACKAGE_PATH}composer.json file should contain: + """ + "gitlost/TestMixedCaseCommand" + """ + And the {PACKAGE_PATH}composer.json file should not contain: + """ + mixed + """ - When I run `wp package list --fields=name,pretty_name` + When I run `wp package list --fields=name` Then STDOUT should be a table containing rows: - | name | pretty_name | - | gitlost/testmixedcasecommand | gitlost/TestMixedCaseCommand | + | name | + | gitlost/TestMixedCaseCommand | When I run `wp TestMixedCaseCommand` Then STDOUT should contain: @@ -467,6 +490,103 @@ Feature: Install WP-CLI packages schlessera/test-command """ + Scenario: Install a package from the wp-cli package index with a mixed-case name + Given an empty directory + + # Install and uninstall with case-sensitive name + When I run `wp package install GeekPress/wp-rocket-cli` + Then STDERR should be empty + And STDOUT should contain: + """ + Installing package GeekPress/wp-rocket-cli (dev-master) + Updating {PACKAGE_PATH}composer.json to require the package... + Using Composer to install the package... + """ + And STDOUT should contain: + """ + Success: Package installed. + """ + And the {PACKAGE_PATH}composer.json file should contain: + """ + GeekPress/wp-rocket-cli + """ + And the {PACKAGE_PATH}composer.json file should not contain: + """ + geek + """ + + When I run `wp package list --fields=name` + Then STDOUT should be a table containing rows: + | name | + | GeekPress/wp-rocket-cli | + + When I run `wp help rocket` + Then STDOUT should contain: + """ + wp rocket + """ + + When I run `wp package uninstall GeekPress/wp-rocket-cli` + Then STDOUT should contain: + """ + Removing require statement from {PACKAGE_PATH}composer.json + """ + And STDOUT should contain: + """ + Success: Uninstalled package. + """ + And the {PACKAGE_PATH}composer.json file should not contain: + """ + rocket + """ + + # Install with lowercase name (for BC - no warning) and uninstall with lowercase name (for BC and convenience) + When I run `wp package install geekpress/wp-rocket-cli` + Then STDERR should be empty + And STDOUT should contain: + """ + Installing package GeekPress/wp-rocket-cli (dev-master) + Updating {PACKAGE_PATH}composer.json to require the package... + Using Composer to install the package... + """ + And STDOUT should contain: + """ + Success: Package installed. + """ + And the {PACKAGE_PATH}composer.json file should contain: + """ + GeekPress/wp-rocket-cli + """ + And the {PACKAGE_PATH}composer.json file should not contain: + """ + geek + """ + + When I run `wp package list --fields=name` + Then STDOUT should be a table containing rows: + | name | + | GeekPress/wp-rocket-cli | + + When I run `wp help rocket` + Then STDOUT should contain: + """ + wp rocket + """ + + When I run `wp package uninstall geekpress/wp-rocket-cli` + Then STDOUT should contain: + """ + Removing require statement from {PACKAGE_PATH}composer.json + """ + And STDOUT should contain: + """ + Success: Uninstalled package. + """ + And the {PACKAGE_PATH}composer.json file should not contain: + """ + rocket + """ + Scenario: Install a package in a local zip Given an empty directory And I run `wget -O google-sitemap-generator-cli.zip https://github.com/wp-cli/google-sitemap-generator-cli/archive/master.zip` @@ -511,6 +631,60 @@ Feature: Install WP-CLI packages wp-cli/google-sitemap-generator-cli """ + Scenario: Install a package from Git using a shortened mixed-case package identifier but lowercase composer.json name + Given an empty directory + + When I try `wp package install CapitalWPCLI/examplecommand` + Then the return code should be 0 + And STDERR should contain: + """ + Warning: Package name mismatch...Updating from git name 'CapitalWPCLI/examplecommand' to composer.json name 'capitalwpcli/examplecommand'. + """ + And STDOUT should contain: + """ + Installing package capitalwpcli/examplecommand (dev-master) + Updating {PACKAGE_PATH}composer.json to require the package... + Registering https://github.com/CapitalWPCLI/examplecommand.git as a VCS repository... + Using Composer to install the package... + """ + And STDOUT should contain: + """ + Success: Package installed. + """ + And the {PACKAGE_PATH}composer.json file should contain: + """ + "capitalwpcli/examplecommand" + """ + And the {PACKAGE_PATH}composer.json file should not contain: + """ + "CapitalWPCLI/examplecommand" + """ + + When I run `wp package list --fields=name` + Then STDOUT should be a table containing rows: + | name | + | capitalwpcli/examplecommand | + + When I run `wp hello-world` + Then STDOUT should contain: + """ + Success: Hello world. + """ + + When I run `wp package uninstall capitalwpcli/examplecommand` + Then STDOUT should contain: + """ + Removing require statement from {PACKAGE_PATH}composer.json + """ + And STDOUT should contain: + """ + Success: Uninstalled package. + """ + And the {PACKAGE_PATH}composer.json file should not contain: + """ + capital + """ + Scenario: Install a package from a remote ZIP Given an empty directory diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 000000000..529dae0ef --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,11 @@ + + + + tests/ + tests/ + + + diff --git a/src/Package_Command.php b/src/Package_Command.php index f1287b52a..f313920fd 100644 --- a/src/Package_Command.php +++ b/src/Package_Command.php @@ -8,7 +8,6 @@ use \Composer\IO\NullIO; use \Composer\Installer; use \Composer\Json\JsonFile; -use \Composer\Json\JsonManipulator; use \Composer\Package; use \Composer\Package\BasePackage; use \Composer\Package\PackageInterface; @@ -22,6 +21,7 @@ use \WP_CLI\ComposerIO; use \WP_CLI\Extractor; use \WP_CLI\Utils; +use \WP_CLI\JsonManipulator; /** * Runs WP-CLI package manager commands. @@ -114,9 +114,7 @@ class Package_Command extends WP_CLI_Command { * * authors * * version * - * These fields are optionally available: - * - * * pretty_name + * There are no optionally available fields. * * ## EXAMPLES * @@ -213,24 +211,7 @@ public function install( $args, $assoc_args ) { $git_package = $package_name; preg_match( '#([^:\/]+\/[^\/]+)\.git#', $package_name, $matches ); if ( ! empty( $matches[1] ) ) { - $package_name = $matches[1]; - - // Generate raw git URL of composer.json file. - $raw_content_url = 'https://raw.githubusercontent.com/' . $package_name . '/master/composer.json'; - $github_token = getenv( 'GITHUB_TOKEN' ); // Use GITHUB_TOKEN if available to avoid authorization failures or rate-limiting. - $headers = $github_token ? array( 'Authorization' => 'token ' . $github_token ) : array(); - - // Convert composer.json JSON to Array. - $composer_content_as_array = json_decode( WP_CLI\Utils\http_request( 'GET', $raw_content_url, null /*data*/, $headers )->body, true ); - - // 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 ) { - $package_name = $package_name_on_repo; - WP_CLI::warning( 'Package name mismatch...Updating the name with correct value.' ); - } + $package_name = $this->check_git_package_name( $matches[1] ); } else { WP_CLI::error( "Couldn't parse package name from expected path '/'." ); } @@ -275,12 +256,18 @@ public function install( $args, $assoc_args ) { list( $package_name, $version ) = explode( ':', $package_name ); } $package = $this->get_package_by_shortened_identifier( $package_name ); - if ( $this->is_git_repository( $package ) ) { - $git_package = $package; - } if ( ! $package ) { WP_CLI::error( "Invalid package." ); } + if ( is_string( $package ) ) { + if ( $this->is_git_repository( $package ) ) { + $git_package = $package; + $package_name = $this->check_git_package_name( $package_name ); + } + } 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(); + } } WP_CLI::log( sprintf( "Installing package %s (%s)", $package_name, $version ) ); @@ -293,15 +280,15 @@ public function install( $args, $assoc_args ) { $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 ); + $json_manipulator->addLink( 'require', $package_name, $version, false /*sortPackages*/, true /*caseInsensitive*/ ); $json_manipulator->addConfigSetting( 'secure-http', true ); if ( $git_package ) { WP_CLI::log( sprintf( 'Registering %s as a VCS repository...', $git_package ) ); - $json_manipulator->addRepository( $package_name, array( 'type' => 'vcs', 'url' => $git_package ) ); + $json_manipulator->addSubNode( 'repositories', $package_name, array( 'type' => 'vcs', 'url' => $git_package ), true /*caseInsensitive*/ ); } else if ( $dir_package ) { WP_CLI::log( sprintf( 'Registering %s as a path repository...', $dir_package ) ); - $json_manipulator->addRepository( $package_name, array( 'type' => 'path', 'url' => $dir_package ) ); + $json_manipulator->addSubNode( 'repositories', $package_name, array( 'type' => 'path', 'url' => $dir_package ), true /*caseInsensitive*/ ); } $composer_backup_decoded = json_decode( $composer_backup, true ); // If the composer file does not contain the current package index repository, refresh the repository definition. @@ -378,7 +365,6 @@ public function install( $args, $assoc_args ) { * These fields are optionally available: * * * description - * * pretty_name * * ## EXAMPLES * @@ -507,6 +493,7 @@ public function uninstall( $args ) { if ( false === ( $package = $this->get_installed_package_by_name( $package_name ) ) ) { WP_CLI::error( "Package not installed." ); } + $package_name = $package->getPrettyName(); // Make sure package name is what's in composer.json. $composer_json_obj = $this->get_composer_json(); @@ -515,14 +502,11 @@ public function uninstall( $args ) { WP_CLI::log( sprintf( 'Removing require statement from %s', $json_path ) ); $composer_backup = file_get_contents( $composer_json_obj->getPath() ); $manipulator = new JsonManipulator( $composer_backup ); - $manipulator->removeSubNode( 'require', $package_name ); - $composer_json_array = json_decode( $composer_backup ); + $manipulator->removeSubNode( 'require', $package_name, true /*caseInsensitive*/ ); // Remove the 'repository' details from composer.json. - if ( is_object( $composer_json_array ) && property_exists( $composer_json_array->repositories, $package_name ) ) { - WP_CLI::log( sprintf( 'Removing repository details from %s', $json_path ) ); - $manipulator->removeRepository( $package_name ); - } + WP_CLI::log( sprintf( 'Removing repository details from %s', $json_path ) ); + $manipulator->removeSubNode( 'repositories', $package_name, true /*caseInsensitive*/ ); file_put_contents( $composer_json_obj->getPath(), $manipulator->getContents() ); @@ -664,12 +648,12 @@ private function show_packages( $context, $packages, $assoc_args ) { $list = array(); foreach ( $packages as $package ) { - $name = $package->getName(); + $name = $package->getPrettyName(); if ( isset( $list[ $name ] ) ) { $list[ $name ]['version'][] = $package->getPrettyVersion(); } else { $package_output = array(); - $package_output['name'] = $package->getName(); + $package_output['name'] = $package->getPrettyName(); $package_output['description'] = $package->getDescription(); $package_output['authors'] = implode( ', ', array_column( (array) $package->getAuthors(), 'name' ) ); $package_output['version'] = array( $package->getPrettyVersion() ); @@ -684,7 +668,7 @@ private function show_packages( $context, $packages, $assoc_args ) { } $package_output['update'] = $update; $package_output['update_version'] = $update_version; - $package_output['pretty_name'] = $package->getPrettyName(); + $package_output['pretty_name'] = $package->getPrettyName(); // Deprecated but kept for BC with package-command 1.0.8. $list[ $package_output['name'] ] = $package_output; } } @@ -711,8 +695,13 @@ private function show_packages( $context, $packages, $assoc_args ) { */ private function get_package_by_shortened_identifier( $package_name ) { // 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->getName() ) { + if ( $package_name === $package->getPrettyName() ) { + return $package; + } + // For BC allow getting by lowercase name. + if ( $lc_package_name === $package->getName() ) { return $package; } } @@ -744,11 +733,16 @@ private function get_installed_packages() { if ( empty( $installed_package_keys ) ) { return array(); } + // For use by legacy incorrect name check. + $lc_installed_package_keys = array_map( 'strtolower', $installed_package_keys ); $installed_packages = array(); foreach( $repo->getCanonicalPackages() as $package ) { - // Use pretty name as it's case sensitive. + // 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 = array_search( $package->getName(), $lc_installed_package_keys, true ) ) ) { // Legacy incorrect name check. + 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; @@ -759,7 +753,11 @@ private function get_installed_packages() { */ private function get_installed_package_by_name( $package_name ) { foreach( $this->get_installed_packages() as $package ) { - if ( $package_name == $package->getName() ) { + 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; } } @@ -911,7 +909,7 @@ private function create_default_composer_json( $composer_path ) { */ private function find_latest_package( PackageInterface $package, Composer $composer, $phpVersion, $minorOnly = false ) { // find the latest version allowed in this pool - $name = $package->getName(); + $name = $package->getPrettyName(); $versionSelector = new VersionSelector($this->get_pool($composer)); $stability = $composer->getPackage()->getMinimumStability(); $flags = $composer->getPackage()->getStabilityFlags(); @@ -951,6 +949,37 @@ private function is_git_repository( $package ) { return '.git' === strtolower( substr( $package, -4, 4 ) ); } + /** + * Check that `$package_name` matches the name in the repo composer.json, and return corrected value if not. + */ + private function check_git_package_name( $package_name ) { + // Generate raw git URL of composer.json file. + $raw_content_url = 'https://raw.githubusercontent.com/' . $package_name . '/master/composer.json'; + $github_token = getenv( 'GITHUB_TOKEN' ); // Use GITHUB_TOKEN if available to avoid authorization failures or rate-limiting. + $headers = $github_token ? array( 'Authorization' => 'token ' . $github_token ) : array(); + + $response = WP_CLI\Utils\http_request( 'GET', $raw_content_url, null /*data*/, $headers ); + if ( 20 != substr( $response->status_code, 0, 2 ) ) { + WP_CLI::error( sprintf( "Couldn't download package from '%s' (HTTP code %d).", $raw_content_url, $response->status_code ) ); + } + + // 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 ) ); + } + + // 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; + } + /** * Set `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. diff --git a/src/WP_CLI/JsonManipulator.php b/src/WP_CLI/JsonManipulator.php new file mode 100644 index 000000000..1a4e6dc70 --- /dev/null +++ b/src/WP_CLI/JsonManipulator.php @@ -0,0 +1,558 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace WP_CLI; // WP_CLI + +use Composer\Json\JsonFile; // WP_CLI +use Composer\Repository\PlatformRepository; + +/** + * @author Jordi Boggiano + */ +class JsonManipulator +{ + private static $DEFINES = '(?(DEFINE) + (? -? (?= [1-9]|0(?!\d) ) \d+ (\.\d+)? ([eE] [+-]? \d+)? ) + (? true | false | null ) + (? " ([^"\\\\]* | \\\\ ["\\\\bfnrt\/] | \\\\ u [0-9a-f]{4} )* " ) + (? \[ (?: (?&json) \s* (?: , (?&json) \s* )* )? \s* \] ) + (? \s* (?&string) \s* : (?&json) \s* ) + (? \{ (?: (?&pair) (?: , (?&pair) )* )? \s* \} ) + (? \s* (?: (?&number) | (?&boolean) | (?&string) | (?&array) | (?&object) ) ) + )'; + + private $contents; + private $newline; + private $indent; + + public function __construct($contents) + { + $contents = trim($contents); + if ($contents === '') { + $contents = '{}'; + } + if (!$this->pregMatch('#^\{(.*)\}$#s', $contents)) { + throw new \InvalidArgumentException('The json file must be an object ({})'); + } + $this->newline = false !== strpos($contents, "\r\n") ? "\r\n" : "\n"; + $this->contents = $contents === '{}' ? '{' . $this->newline . '}' : $contents; + $this->detectIndenting(); + } + + public function getContents() + { + return $this->contents . $this->newline; + } + + public function addLink($type, $package, $constraint, $sortPackages = false, $caseInsensitive = false) // WP_CLI: caseInsensitive. + { + $decoded = JsonFile::parseJson($this->contents); + + // no link of that type yet + if (!isset($decoded[$type])) { + return $this->addMainKey($type, array($package => $constraint)); + } + + $regex = '{'.self::$DEFINES.'^(?P\s*\{\s*(?:(?&string)\s*:\s*(?&json)\s*,\s*)*?)'. + '(?P'.preg_quote(JsonFile::encode($type)).'\s*:\s*)(?P(?&json))(?P.*)}sx'; + if (!$this->pregMatch($regex, $this->contents, $matches)) { + return false; + } + + // WP_CLI: begin caseInsensitive. + if ($caseInsensitive) { + // Just zap any existing packages first in a case insensitive manner. + $this->removeSubNode($type, $package, $caseInsensitive); + return $this->addLink($type, $package, $constraint, $sortPackages); + } + // WP_CLI: end caseInsensitive. + + $links = $matches['value']; + + // try to find existing link + $packageRegex = str_replace('/', '\\\\?/', preg_quote($package)); + $regex = '{'.self::$DEFINES.'"(?P'.$packageRegex.')"(\s*:\s*)(?&string)}ix'; + if ($this->pregMatch($regex, $links, $packageMatches)) { + // update existing link + $existingPackage = $packageMatches['package']; + $packageRegex = str_replace('/', '\\\\?/', preg_quote($existingPackage)); + $links = preg_replace_callback('{'.self::$DEFINES.'"'.$packageRegex.'"(?P\s*:\s*)(?&string)}ix', function ($m) use ($existingPackage, $constraint) { + return JsonFile::encode(str_replace('\\/', '/', $existingPackage)) . $m['separator'] . '"' . $constraint . '"'; + }, $links); + } else { + if ($this->pregMatch('#^\s*\{\s*\S+.*?(\s*\}\s*)$#s', $links, $match)) { + // link missing but non empty links + $links = preg_replace( + '{'.preg_quote($match[1]).'$}', + // addcslashes is used to double up backslashes/$ since preg_replace resolves them as back references otherwise, see #1588 + addcslashes(',' . $this->newline . $this->indent . $this->indent . JsonFile::encode($package).': '.JsonFile::encode($constraint) . $match[1], '\\$'), + $links + ); + } else { + // links empty + $links = '{' . $this->newline . + $this->indent . $this->indent . JsonFile::encode($package).': '.JsonFile::encode($constraint) . $this->newline . + $this->indent . '}'; + } + } + + if (true === $sortPackages) { + $requirements = json_decode($links, true); + $this->sortPackages($requirements); + $links = $this->format($requirements); + } + + $this->contents = $matches['start'] . $matches['property'] . $links . $matches['end']; + + return true; + } + + /** + * Sorts packages by importance (platform packages first, then PHP dependencies) and alphabetically. + * + * @link https://getcomposer.org/doc/02-libraries.md#platform-packages + * + * @param array $packages + */ + private function sortPackages(array &$packages = array()) + { + $prefix = function ($requirement) { + if (preg_match(PlatformRepository::PLATFORM_PACKAGE_REGEX, $requirement)) { + return preg_replace( + array( + '/^php/', + '/^hhvm/', + '/^ext/', + '/^lib/', + '/^\D/', + ), + array( + '0-$0', + '1-$0', + '2-$0', + '3-$0', + '4-$0', + ), + $requirement + ); + } + + return '5-'.$requirement; + }; + + uksort($packages, function ($a, $b) use ($prefix) { + return strnatcmp($prefix($a), $prefix($b)); + }); + } + + public function addRepository($name, $config) + { + return $this->addSubNode('repositories', $name, $config); + } + + public function removeRepository($name) + { + return $this->removeSubNode('repositories', $name); + } + + public function addConfigSetting($name, $value) + { + return $this->addSubNode('config', $name, $value); + } + + public function removeConfigSetting($name) + { + return $this->removeSubNode('config', $name); + } + + public function addProperty($name, $value) + { + if (substr($name, 0, 6) === 'extra.') { + return $this->addSubNode('extra', substr($name, 6), $value); + } + + return $this->addMainKey($name, $value); + } + + public function removeProperty($name) + { + if (substr($name, 0, 6) === 'extra.') { + return $this->removeSubNode('extra', substr($name, 6)); + } + + return $this->removeMainKey($name); + } + + public function addSubNode($mainNode, $name, $value, $caseInsensitive = false) // WP_CLI: caseInsensitive. + { + $decoded = JsonFile::parseJson($this->contents); + + $subName = null; + if (in_array($mainNode, array('config', 'extra')) && false !== strpos($name, '.')) { + list($name, $subName) = explode('.', $name, 2); + } + + // no main node yet + if (!isset($decoded[$mainNode])) { + if ($subName !== null) { + $this->addMainKey($mainNode, array($name => array($subName => $value))); + } else { + $this->addMainKey($mainNode, array($name => $value)); + } + + return true; + } + + // main node content not match-able + $nodeRegex = '{'.self::$DEFINES.'^(?P \s* \{ \s* (?: (?&string) \s* : (?&json) \s* , \s* )*?'. + preg_quote(JsonFile::encode($mainNode)).'\s*:\s*)(?P(?&object))(?P.*)}sx'; + + try { + if (!$this->pregMatch($nodeRegex, $this->contents, $match)) { + return false; + } + } catch (\RuntimeException $e) { + if ($e->getCode() === PREG_BACKTRACK_LIMIT_ERROR) { + return false; + } + throw $e; + } + + // WP_CLI: begin caseInsensitive. + if ($caseInsensitive) { + // Just zap any existing names first in a case insensitive manner. + $this->removeSubNode($mainNode, $name, $caseInsensitive); + return $this->addSubNode($mainNode, $name, $value); + } + // WP_CLI: end caseInsensitive. + + $children = $match['content']; + // invalid match due to un-regexable content, abort + if (!@json_decode($children)) { + return false; + } + + $that = $this; + + // child exists + $childRegex = '{'.self::$DEFINES.'(?P"'.preg_quote($name).'"\s*:\s*)(?P(?&json))(?P,?)}x'; + if ($this->pregMatch($childRegex, $children, $matches)) { + $children = preg_replace_callback($childRegex, function ($matches) use ($name, $subName, $value, $that) { + if ($subName !== null) { + $curVal = json_decode($matches['content'], true); + if (!is_array($curVal)) { + $curVal = array(); + } + $curVal[$subName] = $value; + $value = $curVal; + } + + return $matches['start'] . $that->format($value, 1) . $matches['end']; + }, $children); + } else { + $this->pregMatch('#^{ \s*? (?P\S+.*?)? (?P\s*) }$#sx', $children, $match); + + $whitespace = ''; + if (!empty($match['trailingspace'])) { + $whitespace = $match['trailingspace']; + } + + if (!empty($match['content'])) { + if ($subName !== null) { + $value = array($subName => $value); + } + + // child missing but non empty children + $children = preg_replace( + '#'.$whitespace.'}$#', + addcslashes(',' . $this->newline . $this->indent . $this->indent . JsonFile::encode($name).': '.$this->format($value, 1) . $whitespace . '}', '\\$'), + $children + ); + } else { + if ($subName !== null) { + $value = array($subName => $value); + } + + // children present but empty + $children = '{' . $this->newline . $this->indent . $this->indent . JsonFile::encode($name).': '.$this->format($value, 1) . $whitespace . '}'; + } + } + + $this->contents = preg_replace_callback($nodeRegex, function ($m) use ($children) { + return $m['start'] . $children . $m['end']; + }, $this->contents); + + return true; + } + + public function removeSubNode($mainNode, $name, $caseInsensitive = false) // WP_CLI: caseInsensitive. + { + $decoded = JsonFile::parseJson($this->contents); + + // no node or empty node + if (empty($decoded[$mainNode])) { + return true; + } + + // WP_CLI: begin caseInsensitive. + if ( $caseInsensitive ) { + // This is more or less a copy of the code at the start of `addLink()` above. + $regex = '{'.self::$DEFINES.'^(?P\s*\{\s*(?:(?&string)\s*:\s*(?&json)\s*,\s*)*?)'. + '(?P'.preg_quote(JsonFile::encode($mainNode)).'\s*:\s*)(?P(?&json))(?P.*)}sx'; + if (!$this->pregMatch($regex, $this->contents, $matches)) { + return true; + } + + $value = $matches['value']; // Renamed from `$links` in `addLink()` case above. + + // try to find existing values + $nameRegex = str_replace('/', '\\\\?/', preg_quote($name)); // Renamed from `$packageRegex` in `addLink()` case above. + $regex = '{'.self::$DEFINES.'"(?P'.$nameRegex.')"(\s*:\s*)(?&json)}ix'; // Need `(?&json)` PCRE subroutine here, as opposed to `(?&string)` in `addLink()` case. + if (preg_match_all($regex, $value, $nameMatches)) { + // Just zap them all individually in a case sensitive manner. + foreach ( $nameMatches['name'] as $nameMatch ) { + $this->removeSubNode($mainNode, $nameMatch); + } + } + + return true; + } + // WP_CLI: end caseInsensitive. + + // no node content match-able + $nodeRegex = '{'.self::$DEFINES.'^(?P \s* \{ \s* (?: (?&string) \s* : (?&json) \s* , \s* )*?'. + preg_quote(JsonFile::encode($mainNode)).'\s*:\s*)(?P(?&object))(?P.*)}sx'; + try { + if (!$this->pregMatch($nodeRegex, $this->contents, $match)) { + return false; + } + } catch (\RuntimeException $e) { + if ($e->getCode() === PREG_BACKTRACK_LIMIT_ERROR) { + return false; + } + throw $e; + } + + $children = $match['content']; + + // invalid match due to un-regexable content, abort + if (!@json_decode($children, true)) { + return false; + } + + $subName = null; + if (in_array($mainNode, array('config', 'extra')) && false !== strpos($name, '.')) { + list($name, $subName) = explode('.', $name, 2); + } + + // no node to remove + if (!isset($decoded[$mainNode][$name]) || ($subName && !isset($decoded[$mainNode][$name][$subName]))) { + return true; + } + + // try and find a match for the subkey + if ($this->pregMatch('{"'.preg_quote($name).'"\s*:}i', $children)) { + // find best match for the value of "name" + if (preg_match_all('{'.self::$DEFINES.'"'.preg_quote($name).'"\s*:\s*(?:(?&json))}x', $children, $matches)) { + $bestMatch = ''; + foreach ($matches[0] as $match) { + if (strlen($bestMatch) < strlen($match)) { + $bestMatch = $match; + } + } + $childrenClean = preg_replace('{,\s*'.preg_quote($bestMatch).'}', '', $children, -1, $count); // WP_CLI: As the preg_match_all() above is case-sensitive, so should this be. + if (1 !== $count) { + $childrenClean = preg_replace('{'.preg_quote($bestMatch).'\s*,?\s*}', '', $childrenClean, -1, $count); // WP_CLI: ditto. + if (1 !== $count) { + return false; + } + } + } + } else { + $childrenClean = $children; + } + + // no child data left, $name was the only key in + $this->pregMatch('#^{ \s*? (?P\S+.*?)? (?P\s*) }$#sx', $childrenClean, $match); + if (empty($match['content'])) { + $newline = $this->newline; + $indent = $this->indent; + + $this->contents = preg_replace_callback($nodeRegex, function ($matches) use ($indent, $newline) { + return $matches['start'] . '{' . $newline . $indent . '}' . $matches['end']; + }, $this->contents); + + // we have a subname, so we restore the rest of $name + if ($subName !== null) { + $curVal = json_decode($children, true); + unset($curVal[$name][$subName]); + $this->addSubNode($mainNode, $name, $curVal[$name]); + } + + return true; + } + + $that = $this; + $this->contents = preg_replace_callback($nodeRegex, function ($matches) use ($that, $name, $subName, $childrenClean) { + if ($subName !== null) { + $curVal = json_decode($matches['content'], true); + unset($curVal[$name][$subName]); + $childrenClean = $that->format($curVal, 0); + } + + return $matches['start'] . $childrenClean . $matches['end']; + }, $this->contents); + + return true; + } + + public function addMainKey($key, $content) + { + $decoded = JsonFile::parseJson($this->contents); + $content = $this->format($content); + + // key exists already + $regex = '{'.self::$DEFINES.'^(?P\s*\{\s*(?:(?&string)\s*:\s*(?&json)\s*,\s*)*?)'. + '(?P'.preg_quote(JsonFile::encode($key)).'\s*:\s*(?&json))(?P.*)}sx'; + if (isset($decoded[$key]) && $this->pregMatch($regex, $this->contents, $matches)) { + // invalid match due to un-regexable content, abort + if (!@json_decode('{'.$matches['key'].'}')) { + return false; + } + + $this->contents = $matches['start'] . JsonFile::encode($key).': '.$content . $matches['end']; + + return true; + } + + // append at the end of the file and keep whitespace + if ($this->pregMatch('#[^{\s](\s*)\}$#', $this->contents, $match)) { + $this->contents = preg_replace( + '#'.$match[1].'\}$#', + addcslashes(',' . $this->newline . $this->indent . JsonFile::encode($key). ': '. $content . $this->newline . '}', '\\$'), + $this->contents + ); + + return true; + } + + // append at the end of the file + $this->contents = preg_replace( + '#\}$#', + addcslashes($this->indent . JsonFile::encode($key). ': '.$content . $this->newline . '}', '\\$'), + $this->contents + ); + + return true; + } + + public function removeMainKey($key) + { + $decoded = JsonFile::parseJson($this->contents); + + if (!isset($decoded[$key])) { + return true; + } + + // key exists already + $regex = '{'.self::$DEFINES.'^(?P\s*\{\s*(?:(?&string)\s*:\s*(?&json)\s*,\s*)*?)'. + '(?P'.preg_quote(JsonFile::encode($key)).'\s*:\s*(?&json))\s*,?\s*(?P.*)}sx'; + if ($this->pregMatch($regex, $this->contents, $matches)) { + // invalid match due to un-regexable content, abort + if (!@json_decode('{'.$matches['removal'].'}')) { + return false; + } + + // check that we are not leaving a dangling comma on the previous line if the last line was removed + if (preg_match('#,\s*$#', $matches['start']) && preg_match('#^\}$#', $matches['end'])) { + $matches['start'] = rtrim(preg_replace('#,(\s*)$#', '$1', $matches['start']), $this->indent); + } + + $this->contents = $matches['start'] . $matches['end']; + if (preg_match('#^\{\s*\}\s*$#', $this->contents)) { + $this->contents = "{\n}"; + } + + return true; + } + + return false; + } + + public function format($data, $depth = 0) + { + if (is_array($data)) { + reset($data); + + if (is_numeric(key($data))) { + foreach ($data as $key => $val) { + $data[$key] = $this->format($val, $depth + 1); + } + + return '['.implode(', ', $data).']'; + } + + $out = '{' . $this->newline; + $elems = array(); + foreach ($data as $key => $val) { + $elems[] = str_repeat($this->indent, $depth + 2) . JsonFile::encode($key). ': '.$this->format($val, $depth + 1); + } + + return $out . implode(','.$this->newline, $elems) . $this->newline . str_repeat($this->indent, $depth + 1) . '}'; + } + + return JsonFile::encode($data); + } + + protected function detectIndenting() + { + if ($this->pregMatch('{^([ \t]+)"}m', $this->contents, $match)) { + $this->indent = $match[1]; + } else { + $this->indent = ' '; + } + } + + protected function pregMatch($re, $str, &$matches = array()) + { + $count = preg_match($re, $str, $matches); + + if ($count === false) { + switch (preg_last_error()) { + case PREG_NO_ERROR: + throw new \RuntimeException('Failed to execute regex: PREG_NO_ERROR', PREG_NO_ERROR); + case PREG_INTERNAL_ERROR: + throw new \RuntimeException('Failed to execute regex: PREG_INTERNAL_ERROR', PREG_INTERNAL_ERROR); + case PREG_BACKTRACK_LIMIT_ERROR: + throw new \RuntimeException('Failed to execute regex: PREG_BACKTRACK_LIMIT_ERROR', PREG_BACKTRACK_LIMIT_ERROR); + case PREG_RECURSION_LIMIT_ERROR: + throw new \RuntimeException('Failed to execute regex: PREG_RECURSION_LIMIT_ERROR', PREG_RECURSION_LIMIT_ERROR); + case PREG_BAD_UTF8_ERROR: + throw new \RuntimeException('Failed to execute regex: PREG_BAD_UTF8_ERROR', PREG_BAD_UTF8_ERROR); + case PREG_BAD_UTF8_OFFSET_ERROR: + throw new \RuntimeException('Failed to execute regex: PREG_BAD_UTF8_OFFSET_ERROR', PREG_BAD_UTF8_OFFSET_ERROR); + case 6: // PREG_JIT_STACKLIMIT_ERROR + if (PHP_VERSION_ID > 70000) { + throw new \RuntimeException('Failed to execute regex: PREG_JIT_STACKLIMIT_ERROR', 6); + } + // fallthrough + + default: + throw new \RuntimeException('Failed to execute regex: Unknown error'); + } + } + + return $count; + } +} diff --git a/tests/test-json-manipulator.php b/tests/test-json-manipulator.php new file mode 100644 index 000000000..05916f1be --- /dev/null +++ b/tests/test-json-manipulator.php @@ -0,0 +1,2579 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use WP_CLI\JsonManipulator; // WP_CLI + +class JsonManipulatorTest extends \PHPUnit_Framework_TestCase +{ + + /** + * @dataProvider linkProvider + */ + public function testAddLink($json, $type, $package, $constraint, $expected) + { + $manipulator = new JsonManipulator($json); + $this->assertTrue($manipulator->addLink($type, $package, $constraint)); + $this->assertEquals($expected, $manipulator->getContents()); + } + + public function linkProvider() + { + return array( + array( + '{}', + 'require', + 'vendor/baz', + 'qux', + "{\n". +" \"require\": {\n". +" \"vendor/baz\": \"qux\"\n". +" }\n". +"}\n", + ), + array( + '{ + "foo": "bar" +}', + 'require', + 'vendor/baz', + 'qux', + '{ + "foo": "bar", + "require": { + "vendor/baz": "qux" + } +} +', + ), + array( + '{ + "require": { + } +}', + 'require', + 'vendor/baz', + 'qux', + '{ + "require": { + "vendor/baz": "qux" + } +} +', + ), + array( + '{ + "empty": "", + "require": { + "foo": "bar" + } +}', + 'require', + 'vendor/baz', + 'qux', + '{ + "empty": "", + "require": { + "foo": "bar", + "vendor/baz": "qux" + } +} +', + ), + array( + '{ + "require": + { + "foo": "bar", + "vendor/baz": "baz" + } +}', + 'require', + 'vendor/baz', + 'qux', + '{ + "require": + { + "foo": "bar", + "vendor/baz": "qux" + } +} +', + ), + + + array( + '{ + "require": + { + "foo": "bar", + "vendor/baz": "baz" + } +}', + 'require', + 'vEnDoR/bAz', + 'qux', + '{ + "require": + { + "foo": "bar", + "vendor/baz": "qux" + } +} +', + ), + array( + '{ + "require": + { + "foo": "bar", + "vendor\/baz": "baz" + } +}', + 'require', + 'vendor/baz', + 'qux', + '{ + "require": + { + "foo": "bar", + "vendor/baz": "qux" + } +} +', + ), + array( + '{ + "require": + { + "foo": "bar", + "vendor\/baz": "baz" + } +}', + 'require', + 'vEnDoR/bAz', + 'qux', + '{ + "require": + { + "foo": "bar", + "vendor/baz": "qux" + } +} +', + ), + array( + '{ + "require": { + "foo": "bar" + }, + "repositories": [{ + "type": "package", + "package": { + "require": { + "foo": "bar" + } + } + }] +}', + 'require', + 'foo', + 'qux', + '{ + "require": { + "foo": "qux" + }, + "repositories": [{ + "type": "package", + "package": { + "require": { + "foo": "bar" + } + } + }] +} +', + ), + array( + '{ + "repositories": [{ + "type": "package", + "package": { + "require": { + "foo": "bar" + } + } + }] +}', + 'require', + 'foo', + 'qux', + '{ + "repositories": [{ + "type": "package", + "package": { + "require": { + "foo": "bar" + } + } + }], + "require": { + "foo": "qux" + } +} +', + ), + array( + '{ + "require": { + "php": "5.*" + } +}', + 'require-dev', + 'foo', + 'qux', + '{ + "require": { + "php": "5.*" + }, + "require-dev": { + "foo": "qux" + } +} +', + ), + array( + '{ + "require": { + "php": "5.*" + }, + "require-dev": { + "foo": "bar" + } +}', + 'require-dev', + 'foo', + 'qux', + '{ + "require": { + "php": "5.*" + }, + "require-dev": { + "foo": "qux" + } +} +', + ), + array( + '{ + "repositories": [{ + "type": "package", + "package": { + "bar": "ba[z", + "dist": { + "url": "http...", + "type": "zip" + }, + "autoload": { + "classmap": [ "foo/bar" ] + } + } + }], + "require": { + "php": "5.*" + }, + "require-dev": { + "foo": "bar" + } +}', + 'require-dev', + 'foo', + 'qux', + '{ + "repositories": [{ + "type": "package", + "package": { + "bar": "ba[z", + "dist": { + "url": "http...", + "type": "zip" + }, + "autoload": { + "classmap": [ "foo/bar" ] + } + } + }], + "require": { + "php": "5.*" + }, + "require-dev": { + "foo": "qux" + } +} +', + ), + array( + '{ + "config": { + "cache-files-ttl": 0, + "discard-changes": true + }, + "minimum-stability": "stable", + "prefer-stable": false, + "provide": { + "heroku-sys/cedar": "14.2016.03.22" + }, + "repositories": [ + { + "packagist.org": false + }, + { + "type": "package", + "package": [ + { + "type": "metapackage", + "name": "anthonymartin/geo-location", + "version": "v1.0.0", + "require": { + "heroku-sys/php": ">=5.3.0" + }, + "replace": [], + "provide": [], + "conflict": [] + }, + { + "type": "metapackage", + "name": "aws/aws-sdk-php", + "version": "3.9.4", + "require": { + "heroku-sys/php": ">=5.5" + }, + "replace": [], + "provide": [], + "conflict": [] + }, + { + "type": "metapackage", + "name": "cloudinary/cloudinary_php", + "version": "dev-master", + "require": { + "heroku-sys/ext-curl": "*", + "heroku-sys/ext-json": "*", + "heroku-sys/php": ">=5.3.0" + }, + "replace": [], + "provide": [], + "conflict": [] + }, + { + "type": "metapackage", + "name": "doctrine/annotations", + "version": "v1.2.7", + "require": { + "heroku-sys/php": ">=5.3.2" + }, + "replace": [], + "provide": [], + "conflict": [] + }, + { + "type": "metapackage", + "name": "doctrine/cache", + "version": "v1.6.0", + "require": { + "heroku-sys/php": "~5.5|~7.0" + }, + "replace": [], + "provide": [], + "conflict": [] + }, + { + "type": "metapackage", + "name": "doctrine/collections", + "version": "v1.3.0", + "require": { + "heroku-sys/php": ">=5.3.2" + }, + "replace": [], + "provide": [], + "conflict": [] + }, + { + "type": "metapackage", + "name": "doctrine/common", + "version": "v2.6.1", + "require": { + "heroku-sys/php": "~5.5|~7.0" + }, + "replace": [], + "provide": [], + "conflict": [] + }, + { + "type": "metapackage", + "name": "doctrine/inflector", + "version": "v1.1.0", + "require": { + "heroku-sys/php": ">=5.3.2" + }, + "replace": [], + "provide": [], + "conflict": [] + }, + { + "type": "metapackage", + "name": "doctrine/lexer", + "version": "v1.0.1", + "require": { + "heroku-sys/php": ">=5.3.2" + }, + "replace": [], + "provide": [], + "conflict": [] + }, + { + "type": "metapackage", + "name": "geoip/geoip", + "version": "v1.16", + "require": [], + "replace": [], + "provide": [], + "conflict": { + "heroku-sys/ext-geoip": "*" + } + }, + { + "type": "metapackage", + "name": "giggsey/libphonenumber-for-php", + "version": "7.2.5", + "require": { + "heroku-sys/ext-mbstring": "*" + }, + "replace": [], + "provide": [], + "conflict": [] + }, + { + "type": "metapackage", + "name": "guzzlehttp/guzzle", + "version": "5.3.0", + "require": { + "heroku-sys/php": ">=5.4.0" + }, + "replace": [], + "provide": [], + "conflict": [] + }, + { + "type": "metapackage", + "name": "guzzlehttp/promises", + "version": "1.0.3", + "require": { + "heroku-sys/php": ">=5.5.0" + }, + "replace": [], + "provide": [], + "conflict": [] + }, + { + "type": "metapackage", + "name": "guzzlehttp/psr7", + "version": "1.2.3", + "require": { + "heroku-sys/php": ">=5.4.0" + }, + "replace": [], + "provide": [], + "conflict": [] + }, + { + "type": "metapackage", + "name": "guzzlehttp/ringphp", + "version": "1.1.0", + "require": { + "heroku-sys/php": ">=5.4.0" + }, + "replace": [], + "provide": [], + "conflict": [] + }, + { + "type": "metapackage", + "name": "guzzlehttp/streams", + "version": "3.0.0", + "require": { + "heroku-sys/php": ">=5.4.0" + }, + "replace": [], + "provide": [], + "conflict": [] + }, + { + "type": "metapackage", + "name": "hipchat/hipchat-php", + "version": "v1.4", + "require": { + "heroku-sys/php": ">=5.3.0" + }, + "replace": [], + "provide": [], + "conflict": [] + }, + { + "type": "metapackage", + "name": "kriswallsmith/buzz", + "version": "v0.15", + "require": { + "heroku-sys/php": ">=5.3.0" + }, + "replace": [], + "provide": [], + "conflict": [] + }, + { + "type": "metapackage", + "name": "league/csv", + "version": "8.0.0", + "require": { + "heroku-sys/ext-mbstring": "*", + "heroku-sys/php": ">=5.5.0" + }, + "replace": [], + "provide": [], + "conflict": [] + }, + { + "type": "metapackage", + "name": "league/fractal", + "version": "0.13.0", + "require": { + "heroku-sys/php": ">=5.4" + }, + "replace": [], + "provide": [], + "conflict": [] + }, + { + "type": "metapackage", + "name": "mashape/unirest-php", + "version": "1.2.1", + "require": { + "heroku-sys/ext-curl": "*", + "heroku-sys/ext-json": "*", + "heroku-sys/php": ">=5.3.0" + }, + "replace": [], + "provide": [], + "conflict": [] + }, + { + "type": "metapackage", + "name": "mtdowling/jmespath.php", + "version": "2.3.0", + "require": { + "heroku-sys/php": ">=5.4.0" + }, + "replace": [], + "provide": [], + "conflict": [] + }, + { + "type": "metapackage", + "name": "palex/phpstructureddata", + "version": "v2.0.1", + "require": { + "heroku-sys/php": ">=5.3.0" + }, + "replace": [], + "provide": [], + "conflict": [] + }, + { + "type": "metapackage", + "name": "psr/http-message", + "version": "1.0", + "require": { + "heroku-sys/php": ">=5.3.0" + }, + "replace": [], + "provide": [], + "conflict": [] + }, + { + "type": "metapackage", + "name": "react/promise", + "version": "v2.2.1", + "require": { + "heroku-sys/php": ">=5.4.0" + }, + "replace": [], + "provide": [], + "conflict": [] + }, + { + "type": "metapackage", + "name": "rollbar/rollbar", + "version": "v0.15.0", + "require": { + "heroku-sys/ext-curl": "*" + }, + "replace": [], + "provide": [], + "conflict": [] + }, + { + "type": "metapackage", + "name": "ronanguilloux/isocodes", + "version": "1.2.0", + "require": { + "heroku-sys/ext-bcmath": "*", + "heroku-sys/php": ">=5.4.0" + }, + "replace": [], + "provide": [], + "conflict": [] + }, + { + "type": "metapackage", + "name": "sendgrid/sendgrid", + "version": "2.1.1", + "require": { + "heroku-sys/php": ">=5.3" + }, + "replace": [], + "provide": [], + "conflict": [] + }, + { + "type": "metapackage", + "name": "sendgrid/smtpapi", + "version": "0.0.1", + "require": { + "heroku-sys/php": ">=5.3" + }, + "replace": [], + "provide": [], + "conflict": [] + }, + { + "type": "metapackage", + "name": "symfony/css-selector", + "version": "v2.8.2", + "require": { + "heroku-sys/php": ">=5.3.9" + }, + "replace": [], + "provide": [], + "conflict": [] + }, + { + "type": "metapackage", + "name": "symfony/http-foundation", + "version": "v2.8.2", + "require": { + "heroku-sys/php": ">=5.3.9" + }, + "replace": [], + "provide": [], + "conflict": [] + }, + { + "type": "metapackage", + "name": "symfony/polyfill-php54", + "version": "v1.1.0", + "require": { + "heroku-sys/php": ">=5.3.3" + }, + "replace": [], + "provide": [], + "conflict": [] + }, + { + "type": "metapackage", + "name": "symfony/polyfill-php55", + "version": "v1.1.0", + "require": { + "heroku-sys/php": ">=5.3.3" + }, + "replace": [], + "provide": [], + "conflict": [] + }, + { + "type": "metapackage", + "name": "thepixeldeveloper/sitemap", + "version": "3.0.0", + "require": { + "heroku-sys/php": ">=5.3.0" + }, + "replace": [], + "provide": [], + "conflict": [] + }, + { + "type": "metapackage", + "name": "tijsverkoyen/css-to-inline-styles", + "version": "1.5.5", + "require": { + "heroku-sys/php": ">=5.3.0" + }, + "replace": [], + "provide": [], + "conflict": [] + }, + { + "type": "metapackage", + "name": "yiisoft/yii", + "version": "1.1.17", + "require": { + "heroku-sys/php": ">=5.1.0" + }, + "replace": [], + "provide": [], + "conflict": [] + }, + { + "type": "metapackage", + "name": "composer.json/composer.lock", + "version": "dev-597511d6d51b96e4a8afeba2c79982e5", + "require": { + "heroku-sys/php": "~5.6.0", + "heroku-sys/ext-newrelic": "*", + "heroku-sys/ext-gd": "*", + "heroku-sys/ext-redis": "*" + }, + "replace": [], + "provide": [], + "conflict": [] + } + ] + } + ], + "require": { + "composer.json/composer.lock": "dev-597511d6d51b96e4a8afeba2c79982e5", + "anthonymartin/geo-location": "v1.0.0", + "aws/aws-sdk-php": "3.9.4", + "cloudinary/cloudinary_php": "dev-master", + "doctrine/annotations": "v1.2.7", + "doctrine/cache": "v1.6.0", + "doctrine/collections": "v1.3.0", + "doctrine/common": "v2.6.1", + "doctrine/inflector": "v1.1.0", + "doctrine/lexer": "v1.0.1", + "geoip/geoip": "v1.16", + "giggsey/libphonenumber-for-php": "7.2.5", + "guzzlehttp/guzzle": "5.3.0", + "guzzlehttp/promises": "1.0.3", + "guzzlehttp/psr7": "1.2.3", + "guzzlehttp/ringphp": "1.1.0", + "guzzlehttp/streams": "3.0.0", + "hipchat/hipchat-php": "v1.4", + "kriswallsmith/buzz": "v0.15", + "league/csv": "8.0.0", + "league/fractal": "0.13.0", + "mashape/unirest-php": "1.2.1", + "mtdowling/jmespath.php": "2.3.0", + "palex/phpstructureddata": "v2.0.1", + "psr/http-message": "1.0", + "react/promise": "v2.2.1", + "rollbar/rollbar": "v0.15.0", + "ronanguilloux/isocodes": "1.2.0", + "sendgrid/sendgrid": "2.1.1", + "sendgrid/smtpapi": "0.0.1", + "symfony/css-selector": "v2.8.2", + "symfony/http-foundation": "v2.8.2", + "symfony/polyfill-php54": "v1.1.0", + "symfony/polyfill-php55": "v1.1.0", + "thepixeldeveloper/sitemap": "3.0.0", + "tijsverkoyen/css-to-inline-styles": "1.5.5", + "yiisoft/yii": "1.1.17", + "heroku-sys/apache": "^2.4.10", + "heroku-sys/nginx": "~1.8.0" + } +}', + 'require', + 'foo', + 'qux', + '{ + "config": { + "cache-files-ttl": 0, + "discard-changes": true + }, + "minimum-stability": "stable", + "prefer-stable": false, + "provide": { + "heroku-sys/cedar": "14.2016.03.22" + }, + "repositories": [ + { + "packagist.org": false + }, + { + "type": "package", + "package": [ + { + "type": "metapackage", + "name": "anthonymartin/geo-location", + "version": "v1.0.0", + "require": { + "heroku-sys/php": ">=5.3.0" + }, + "replace": [], + "provide": [], + "conflict": [] + }, + { + "type": "metapackage", + "name": "aws/aws-sdk-php", + "version": "3.9.4", + "require": { + "heroku-sys/php": ">=5.5" + }, + "replace": [], + "provide": [], + "conflict": [] + }, + { + "type": "metapackage", + "name": "cloudinary/cloudinary_php", + "version": "dev-master", + "require": { + "heroku-sys/ext-curl": "*", + "heroku-sys/ext-json": "*", + "heroku-sys/php": ">=5.3.0" + }, + "replace": [], + "provide": [], + "conflict": [] + }, + { + "type": "metapackage", + "name": "doctrine/annotations", + "version": "v1.2.7", + "require": { + "heroku-sys/php": ">=5.3.2" + }, + "replace": [], + "provide": [], + "conflict": [] + }, + { + "type": "metapackage", + "name": "doctrine/cache", + "version": "v1.6.0", + "require": { + "heroku-sys/php": "~5.5|~7.0" + }, + "replace": [], + "provide": [], + "conflict": [] + }, + { + "type": "metapackage", + "name": "doctrine/collections", + "version": "v1.3.0", + "require": { + "heroku-sys/php": ">=5.3.2" + }, + "replace": [], + "provide": [], + "conflict": [] + }, + { + "type": "metapackage", + "name": "doctrine/common", + "version": "v2.6.1", + "require": { + "heroku-sys/php": "~5.5|~7.0" + }, + "replace": [], + "provide": [], + "conflict": [] + }, + { + "type": "metapackage", + "name": "doctrine/inflector", + "version": "v1.1.0", + "require": { + "heroku-sys/php": ">=5.3.2" + }, + "replace": [], + "provide": [], + "conflict": [] + }, + { + "type": "metapackage", + "name": "doctrine/lexer", + "version": "v1.0.1", + "require": { + "heroku-sys/php": ">=5.3.2" + }, + "replace": [], + "provide": [], + "conflict": [] + }, + { + "type": "metapackage", + "name": "geoip/geoip", + "version": "v1.16", + "require": [], + "replace": [], + "provide": [], + "conflict": { + "heroku-sys/ext-geoip": "*" + } + }, + { + "type": "metapackage", + "name": "giggsey/libphonenumber-for-php", + "version": "7.2.5", + "require": { + "heroku-sys/ext-mbstring": "*" + }, + "replace": [], + "provide": [], + "conflict": [] + }, + { + "type": "metapackage", + "name": "guzzlehttp/guzzle", + "version": "5.3.0", + "require": { + "heroku-sys/php": ">=5.4.0" + }, + "replace": [], + "provide": [], + "conflict": [] + }, + { + "type": "metapackage", + "name": "guzzlehttp/promises", + "version": "1.0.3", + "require": { + "heroku-sys/php": ">=5.5.0" + }, + "replace": [], + "provide": [], + "conflict": [] + }, + { + "type": "metapackage", + "name": "guzzlehttp/psr7", + "version": "1.2.3", + "require": { + "heroku-sys/php": ">=5.4.0" + }, + "replace": [], + "provide": [], + "conflict": [] + }, + { + "type": "metapackage", + "name": "guzzlehttp/ringphp", + "version": "1.1.0", + "require": { + "heroku-sys/php": ">=5.4.0" + }, + "replace": [], + "provide": [], + "conflict": [] + }, + { + "type": "metapackage", + "name": "guzzlehttp/streams", + "version": "3.0.0", + "require": { + "heroku-sys/php": ">=5.4.0" + }, + "replace": [], + "provide": [], + "conflict": [] + }, + { + "type": "metapackage", + "name": "hipchat/hipchat-php", + "version": "v1.4", + "require": { + "heroku-sys/php": ">=5.3.0" + }, + "replace": [], + "provide": [], + "conflict": [] + }, + { + "type": "metapackage", + "name": "kriswallsmith/buzz", + "version": "v0.15", + "require": { + "heroku-sys/php": ">=5.3.0" + }, + "replace": [], + "provide": [], + "conflict": [] + }, + { + "type": "metapackage", + "name": "league/csv", + "version": "8.0.0", + "require": { + "heroku-sys/ext-mbstring": "*", + "heroku-sys/php": ">=5.5.0" + }, + "replace": [], + "provide": [], + "conflict": [] + }, + { + "type": "metapackage", + "name": "league/fractal", + "version": "0.13.0", + "require": { + "heroku-sys/php": ">=5.4" + }, + "replace": [], + "provide": [], + "conflict": [] + }, + { + "type": "metapackage", + "name": "mashape/unirest-php", + "version": "1.2.1", + "require": { + "heroku-sys/ext-curl": "*", + "heroku-sys/ext-json": "*", + "heroku-sys/php": ">=5.3.0" + }, + "replace": [], + "provide": [], + "conflict": [] + }, + { + "type": "metapackage", + "name": "mtdowling/jmespath.php", + "version": "2.3.0", + "require": { + "heroku-sys/php": ">=5.4.0" + }, + "replace": [], + "provide": [], + "conflict": [] + }, + { + "type": "metapackage", + "name": "palex/phpstructureddata", + "version": "v2.0.1", + "require": { + "heroku-sys/php": ">=5.3.0" + }, + "replace": [], + "provide": [], + "conflict": [] + }, + { + "type": "metapackage", + "name": "psr/http-message", + "version": "1.0", + "require": { + "heroku-sys/php": ">=5.3.0" + }, + "replace": [], + "provide": [], + "conflict": [] + }, + { + "type": "metapackage", + "name": "react/promise", + "version": "v2.2.1", + "require": { + "heroku-sys/php": ">=5.4.0" + }, + "replace": [], + "provide": [], + "conflict": [] + }, + { + "type": "metapackage", + "name": "rollbar/rollbar", + "version": "v0.15.0", + "require": { + "heroku-sys/ext-curl": "*" + }, + "replace": [], + "provide": [], + "conflict": [] + }, + { + "type": "metapackage", + "name": "ronanguilloux/isocodes", + "version": "1.2.0", + "require": { + "heroku-sys/ext-bcmath": "*", + "heroku-sys/php": ">=5.4.0" + }, + "replace": [], + "provide": [], + "conflict": [] + }, + { + "type": "metapackage", + "name": "sendgrid/sendgrid", + "version": "2.1.1", + "require": { + "heroku-sys/php": ">=5.3" + }, + "replace": [], + "provide": [], + "conflict": [] + }, + { + "type": "metapackage", + "name": "sendgrid/smtpapi", + "version": "0.0.1", + "require": { + "heroku-sys/php": ">=5.3" + }, + "replace": [], + "provide": [], + "conflict": [] + }, + { + "type": "metapackage", + "name": "symfony/css-selector", + "version": "v2.8.2", + "require": { + "heroku-sys/php": ">=5.3.9" + }, + "replace": [], + "provide": [], + "conflict": [] + }, + { + "type": "metapackage", + "name": "symfony/http-foundation", + "version": "v2.8.2", + "require": { + "heroku-sys/php": ">=5.3.9" + }, + "replace": [], + "provide": [], + "conflict": [] + }, + { + "type": "metapackage", + "name": "symfony/polyfill-php54", + "version": "v1.1.0", + "require": { + "heroku-sys/php": ">=5.3.3" + }, + "replace": [], + "provide": [], + "conflict": [] + }, + { + "type": "metapackage", + "name": "symfony/polyfill-php55", + "version": "v1.1.0", + "require": { + "heroku-sys/php": ">=5.3.3" + }, + "replace": [], + "provide": [], + "conflict": [] + }, + { + "type": "metapackage", + "name": "thepixeldeveloper/sitemap", + "version": "3.0.0", + "require": { + "heroku-sys/php": ">=5.3.0" + }, + "replace": [], + "provide": [], + "conflict": [] + }, + { + "type": "metapackage", + "name": "tijsverkoyen/css-to-inline-styles", + "version": "1.5.5", + "require": { + "heroku-sys/php": ">=5.3.0" + }, + "replace": [], + "provide": [], + "conflict": [] + }, + { + "type": "metapackage", + "name": "yiisoft/yii", + "version": "1.1.17", + "require": { + "heroku-sys/php": ">=5.1.0" + }, + "replace": [], + "provide": [], + "conflict": [] + }, + { + "type": "metapackage", + "name": "composer.json/composer.lock", + "version": "dev-597511d6d51b96e4a8afeba2c79982e5", + "require": { + "heroku-sys/php": "~5.6.0", + "heroku-sys/ext-newrelic": "*", + "heroku-sys/ext-gd": "*", + "heroku-sys/ext-redis": "*" + }, + "replace": [], + "provide": [], + "conflict": [] + } + ] + } + ], + "require": { + "composer.json/composer.lock": "dev-597511d6d51b96e4a8afeba2c79982e5", + "anthonymartin/geo-location": "v1.0.0", + "aws/aws-sdk-php": "3.9.4", + "cloudinary/cloudinary_php": "dev-master", + "doctrine/annotations": "v1.2.7", + "doctrine/cache": "v1.6.0", + "doctrine/collections": "v1.3.0", + "doctrine/common": "v2.6.1", + "doctrine/inflector": "v1.1.0", + "doctrine/lexer": "v1.0.1", + "geoip/geoip": "v1.16", + "giggsey/libphonenumber-for-php": "7.2.5", + "guzzlehttp/guzzle": "5.3.0", + "guzzlehttp/promises": "1.0.3", + "guzzlehttp/psr7": "1.2.3", + "guzzlehttp/ringphp": "1.1.0", + "guzzlehttp/streams": "3.0.0", + "hipchat/hipchat-php": "v1.4", + "kriswallsmith/buzz": "v0.15", + "league/csv": "8.0.0", + "league/fractal": "0.13.0", + "mashape/unirest-php": "1.2.1", + "mtdowling/jmespath.php": "2.3.0", + "palex/phpstructureddata": "v2.0.1", + "psr/http-message": "1.0", + "react/promise": "v2.2.1", + "rollbar/rollbar": "v0.15.0", + "ronanguilloux/isocodes": "1.2.0", + "sendgrid/sendgrid": "2.1.1", + "sendgrid/smtpapi": "0.0.1", + "symfony/css-selector": "v2.8.2", + "symfony/http-foundation": "v2.8.2", + "symfony/polyfill-php54": "v1.1.0", + "symfony/polyfill-php55": "v1.1.0", + "thepixeldeveloper/sitemap": "3.0.0", + "tijsverkoyen/css-to-inline-styles": "1.5.5", + "yiisoft/yii": "1.1.17", + "heroku-sys/apache": "^2.4.10", + "heroku-sys/nginx": "~1.8.0", + "foo": "qux" + } +} +', + ), + ); + } + + /** + * @dataProvider providerAddLinkAndSortPackages + */ + public function testAddLinkAndSortPackages($json, $type, $package, $constraint, $sortPackages, $expected) + { + $manipulator = new JsonManipulator($json); + $this->assertTrue($manipulator->addLink($type, $package, $constraint, $sortPackages)); + $this->assertEquals($expected, $manipulator->getContents()); + } + + public function providerAddLinkAndSortPackages() + { + return array( + array( + '{ + "require": { + "vendor/baz": "qux" + } +}', + 'require', + 'foo', + 'bar', + true, + '{ + "require": { + "foo": "bar", + "vendor/baz": "qux" + } +} +', + ), + array( + '{ + "require": { + "vendor/baz": "qux" + } +}', + 'require', + 'foo', + 'bar', + false, + '{ + "require": { + "vendor/baz": "qux", + "foo": "bar" + } +} +', + ), + array( + '{ + "require": { + "foo": "baz", + "ext-10gd": "*", + "ext-2mcrypt": "*", + "lib-foo": "*", + "hhvm": "*", + "php": ">=5.5" + } +}', + 'require', + 'igorw/retry', + '*', + true, + '{ + "require": { + "php": ">=5.5", + "hhvm": "*", + "ext-2mcrypt": "*", + "ext-10gd": "*", + "lib-foo": "*", + "foo": "baz", + "igorw/retry": "*" + } +} +', + ), + ); + } + + /** + * @dataProvider removeSubNodeProvider + */ + public function testRemoveSubNode($json, $name, $expected, $expectedContent = null) + { + $manipulator = new JsonManipulator($json); + + $this->assertEquals($expected, $manipulator->removeSubNode('repositories', $name)); + if (null !== $expectedContent) { + $this->assertEquals($expectedContent, $manipulator->getContents()); + } + } + + public function removeSubNodeProvider() + { + return array( + 'works on simple ones first' => array( + '{ + "repositories": { + "foo": { + "foo": "bar", + "bar": "baz" + }, + "bar": { + "foo": "bar", + "bar": "baz" + } + } +}', + 'foo', + true, + '{ + "repositories": { + "bar": { + "foo": "bar", + "bar": "baz" + } + } +} +', + ), + 'works on simple ones last' => array( + '{ + "repositories": { + "foo": { + "foo": "bar", + "bar": "baz" + }, + "bar": { + "foo": "bar", + "bar": "baz" + } + } +}', + 'bar', + true, + '{ + "repositories": { + "foo": { + "foo": "bar", + "bar": "baz" + } + } +} +', + ), + 'works on simple ones unique' => array( + '{ + "repositories": { + "foo": { + "foo": "bar", + "bar": "baz" + } + } +}', + 'foo', + true, + '{ + "repositories": { + } +} +', + ), + 'works on simple ones middle' => array( + '{ + "repositories": { + "foo": { + "foo": "bar", + "bar": "baz" + }, + "bar": { + "foo": "bar", + "bar": "baz" + }, + "baz": { + "foo": "bar", + "bar": "baz" + } + } +}', + 'bar', + true, + '{ + "repositories": { + "foo": { + "foo": "bar", + "bar": "baz" + }, + "baz": { + "foo": "bar", + "bar": "baz" + } + } +} +', + ), + 'works on undefined ones' => array( + '{ + "repositories": { + "main": { + "foo": "bar", + "bar": "baz" + } + } +}', + 'removenotthere', + true, + '{ + "repositories": { + "main": { + "foo": "bar", + "bar": "baz" + } + } +} +', + ), + 'works on child having unmatched name' => array( + '{ + "repositories": { + "baz": { + "foo": "bar", + "bar": "baz" + } + } +}', + 'bar', + true, + '{ + "repositories": { + "baz": { + "foo": "bar", + "bar": "baz" + } + } +} +', + ), + 'works on child having duplicate name' => array( + '{ + "repositories": { + "foo": { + "baz": "qux" + }, + "baz": { + "foo": "bar", + "bar": "baz" + } + } +}', + 'baz', + true, + '{ + "repositories": { + "foo": { + "baz": "qux" + } + } +} +', + ), + 'works on empty repos' => array( + '{ + "repositories": { + } +}', + 'bar', + true, + ), + 'works on empty repos2' => array( + '{ + "repositories": {} +}', + 'bar', + true, + ), + 'works on missing repos' => array( + "{\n}", + 'bar', + true, + ), + 'works on deep repos' => array( + '{ + "repositories": { + "foo": { + "package": { "bar": "baz" } + } + } +}', + 'foo', + true, + '{ + "repositories": { + } +} +', + ), + 'works on deep repos with borked texts' => array( + '{ + "repositories": { + "foo": { + "package": { "bar": "ba{z" } + } + } +}', + 'bar', + true, + '{ + "repositories": { + "foo": { + "package": { "bar": "ba{z" } + } + } +} +', + + '{ +} +', + ), + 'works on deep repos with borked texts2' => array( + '{ + "repositories": { + "foo": { + "package": { "bar": "ba}z" } + } + } +}', + 'bar', + true, + '{ + "repositories": { + "foo": { + "package": { "bar": "ba}z" } + } + } +} +', + + '{ +} +', + ), + 'fails on deep arrays with borked texts' => array( + '{ + "repositories": [ + { + "package": { "bar": "ba[z" } + } + ] +}', + 'bar', + false, + ), + 'fails on deep arrays with borked texts2' => array( + '{ + "repositories": [ + { + "package": { "bar": "ba]z" } + } + ] +}', + 'bar', + false, + ), + ); + } + + public function testRemoveSubNodeFromRequire() + { + $manipulator = new JsonManipulator('{ + "repositories": [ + { + "package": { + "require": { + "this/should-not-end-up-in-root-require": "~2.0" + }, + "require-dev": { + "this/should-not-end-up-in-root-require-dev": "~2.0" + } + } + } + ], + "require": { + "package/a": "*", + "package/b": "*", + "package/c": "*" + }, + "require-dev": { + "package/d": "*" + } +}'); + + $this->assertTrue($manipulator->removeSubNode('require', 'package/c')); + $this->assertTrue($manipulator->removeSubNode('require-dev', 'package/d')); + $this->assertEquals('{ + "repositories": [ + { + "package": { + "require": { + "this/should-not-end-up-in-root-require": "~2.0" + }, + "require-dev": { + "this/should-not-end-up-in-root-require-dev": "~2.0" + } + } + } + ], + "require": { + "package/a": "*", + "package/b": "*" + }, + "require-dev": { + } +} +', $manipulator->getContents()); + } + + public function testAddSubNodeInRequire() + { + $manipulator = new JsonManipulator('{ + "repositories": [ + { + "package": { + "require": { + "this/should-not-end-up-in-root-require": "~2.0" + }, + "require-dev": { + "this/should-not-end-up-in-root-require-dev": "~2.0" + } + } + } + ], + "require": { + "package/a": "*", + "package/b": "*" + }, + "require-dev": { + "package/d": "*" + } +}'); + + $this->assertTrue($manipulator->addSubNode('require', 'package/c', '*')); + $this->assertTrue($manipulator->addSubNode('require-dev', 'package/e', '*')); + $this->assertEquals('{ + "repositories": [ + { + "package": { + "require": { + "this/should-not-end-up-in-root-require": "~2.0" + }, + "require-dev": { + "this/should-not-end-up-in-root-require-dev": "~2.0" + } + } + } + ], + "require": { + "package/a": "*", + "package/b": "*", + "package/c": "*" + }, + "require-dev": { + "package/d": "*", + "package/e": "*" + } +} +', $manipulator->getContents()); + } + + public function testAddExtraWithPackage() + { + //$this->markTestSkipped(); + $manipulator = new JsonManipulator('{ + "repositories": [ + { + "type": "package", + "package": { + "authors": [], + "extra": { + "package-xml": "package.xml" + } + } + } + ], + "extra": { + "auto-append-gitignore": true + } +}'); + + $this->assertTrue($manipulator->addProperty('extra.foo-bar', true)); + $this->assertEquals('{ + "repositories": [ + { + "type": "package", + "package": { + "authors": [], + "extra": { + "package-xml": "package.xml" + } + } + } + ], + "extra": { + "auto-append-gitignore": true, + "foo-bar": true + } +} +', $manipulator->getContents()); + } + + public function testAddRepositoryCanInitializeEmptyRepositories() + { + $manipulator = new JsonManipulator('{ + "repositories": { + } +}'); + + $this->assertTrue($manipulator->addRepository('bar', array('type' => 'composer'))); + $this->assertEquals('{ + "repositories": { + "bar": { + "type": "composer" + } + } +} +', $manipulator->getContents()); + } + + public function testAddRepositoryCanInitializeFromScratch() + { + $manipulator = new JsonManipulator("{ +\t\"a\": \"b\" +}"); + + $this->assertTrue($manipulator->addRepository('bar2', array('type' => 'composer'))); + $this->assertEquals("{ +\t\"a\": \"b\", +\t\"repositories\": { +\t\t\"bar2\": { +\t\t\t\"type\": \"composer\" +\t\t} +\t} +} +", $manipulator->getContents()); + } + + public function testAddRepositoryCanAdd() + { + $manipulator = new JsonManipulator('{ + "repositories": { + "foo": { + "type": "vcs", + "url": "lala" + } + } +}'); + + $this->assertTrue($manipulator->addRepository('bar', array('type' => 'composer'))); + $this->assertEquals('{ + "repositories": { + "foo": { + "type": "vcs", + "url": "lala" + }, + "bar": { + "type": "composer" + } + } +} +', $manipulator->getContents()); + } + + public function testAddRepositoryCanOverrideDeepRepos() + { + $manipulator = new JsonManipulator('{ + "repositories": { + "baz": { + "type": "package", + "package": {} + } + } +}'); + + $this->assertTrue($manipulator->addRepository('baz', array('type' => 'composer'))); + $this->assertEquals('{ + "repositories": { + "baz": { + "type": "composer" + } + } +} +', $manipulator->getContents()); + } + + public function testAddConfigSettingEscapes() + { + $manipulator = new JsonManipulator('{ + "config": { + } +}'); + + $this->assertTrue($manipulator->addConfigSetting('test', 'a\b')); + $this->assertTrue($manipulator->addConfigSetting('test2', "a\nb\fa")); + $this->assertEquals('{ + "config": { + "test": "a\\\\b", + "test2": "a\nb\fa" + } +} +', $manipulator->getContents()); + } + + public function testAddConfigSettingWorksFromScratch() + { + $manipulator = new JsonManipulator('{ +}'); + + $this->assertTrue($manipulator->addConfigSetting('foo.bar', 'baz')); + $this->assertEquals('{ + "config": { + "foo": { + "bar": "baz" + } + } +} +', $manipulator->getContents()); + } + + public function testAddConfigSettingCanAdd() + { + $manipulator = new JsonManipulator('{ + "config": { + "foo": "bar" + } +}'); + + $this->assertTrue($manipulator->addConfigSetting('bar', 'baz')); + $this->assertEquals('{ + "config": { + "foo": "bar", + "bar": "baz" + } +} +', $manipulator->getContents()); + } + + public function testAddConfigSettingCanOverwrite() + { + $manipulator = new JsonManipulator('{ + "config": { + "foo": "bar", + "bar": "baz" + } +}'); + + $this->assertTrue($manipulator->addConfigSetting('foo', 'zomg')); + $this->assertEquals('{ + "config": { + "foo": "zomg", + "bar": "baz" + } +} +', $manipulator->getContents()); + } + + public function testAddConfigSettingCanOverwriteNumbers() + { + $manipulator = new JsonManipulator('{ + "config": { + "foo": 500 + } +}'); + + $this->assertTrue($manipulator->addConfigSetting('foo', 50)); + $this->assertEquals('{ + "config": { + "foo": 50 + } +} +', $manipulator->getContents()); + } + + public function testAddConfigSettingCanOverwriteArrays() + { + $manipulator = new JsonManipulator('{ + "config": { + "github-oauth": { + "github.com": "foo" + }, + "github-protocols": ["https"] + } +}'); + + $this->assertTrue($manipulator->addConfigSetting('github-protocols', array('https', 'http'))); + $this->assertEquals('{ + "config": { + "github-oauth": { + "github.com": "foo" + }, + "github-protocols": ["https", "http"] + } +} +', $manipulator->getContents()); + + $this->assertTrue($manipulator->addConfigSetting('github-oauth', array('github.com' => 'bar', 'alt.example.org' => 'baz'))); + $this->assertEquals('{ + "config": { + "github-oauth": { + "github.com": "bar", + "alt.example.org": "baz" + }, + "github-protocols": ["https", "http"] + } +} +', $manipulator->getContents()); + } + + public function testAddConfigSettingCanAddSubKeyInEmptyConfig() + { + $manipulator = new JsonManipulator('{ + "config": { + } +}'); + + $this->assertTrue($manipulator->addConfigSetting('github-oauth.bar', 'baz')); + $this->assertEquals('{ + "config": { + "github-oauth": { + "bar": "baz" + } + } +} +', $manipulator->getContents()); + } + + public function testAddConfigSettingCanAddSubKeyInEmptyVal() + { + $manipulator = new JsonManipulator('{ + "config": { + "github-oauth": {}, + "github-oauth2": { + } + } +}'); + + $this->assertTrue($manipulator->addConfigSetting('github-oauth.bar', 'baz')); + $this->assertTrue($manipulator->addConfigSetting('github-oauth2.a.bar', 'baz2')); + $this->assertTrue($manipulator->addConfigSetting('github-oauth3.b', 'c')); + $this->assertEquals('{ + "config": { + "github-oauth": { + "bar": "baz" + }, + "github-oauth2": { + "a.bar": "baz2" + }, + "github-oauth3": { + "b": "c" + } + } +} +', $manipulator->getContents()); + } + + public function testAddConfigSettingCanAddSubKeyInHash() + { + $manipulator = new JsonManipulator('{ + "config": { + "github-oauth": { + "github.com": "foo" + } + } +}'); + + $this->assertTrue($manipulator->addConfigSetting('github-oauth.bar', 'baz')); + $this->assertEquals('{ + "config": { + "github-oauth": { + "github.com": "foo", + "bar": "baz" + } + } +} +', $manipulator->getContents()); + } + + public function testAddRootSettingDoesNotBreakDots() + { + $manipulator = new JsonManipulator('{ + "github-oauth": { + "github.com": "foo" + } +}'); + + $this->assertTrue($manipulator->addSubNode('github-oauth', 'bar', 'baz')); + $this->assertEquals('{ + "github-oauth": { + "github.com": "foo", + "bar": "baz" + } +} +', $manipulator->getContents()); + } + + public function testRemoveConfigSettingCanRemoveSubKeyInHash() + { + $manipulator = new JsonManipulator('{ + "config": { + "github-oauth": { + "github.com": "foo", + "bar": "baz" + } + } +}'); + + $this->assertTrue($manipulator->removeConfigSetting('github-oauth.bar')); + $this->assertEquals('{ + "config": { + "github-oauth": { + "github.com": "foo" + } + } +} +', $manipulator->getContents()); + } + + public function testRemoveConfigSettingCanRemoveSubKeyInHashWithSiblings() + { + $manipulator = new JsonManipulator('{ + "config": { + "foo": "bar", + "github-oauth": { + "github.com": "foo", + "bar": "baz" + } + } +}'); + + $this->assertTrue($manipulator->removeConfigSetting('github-oauth.bar')); + $this->assertEquals('{ + "config": { + "foo": "bar", + "github-oauth": { + "github.com": "foo" + } + } +} +', $manipulator->getContents()); + } + + public function testAddMainKey() + { + $manipulator = new JsonManipulator('{ + "foo": "bar" +}'); + + $this->assertTrue($manipulator->addMainKey('bar', 'baz')); + $this->assertEquals('{ + "foo": "bar", + "bar": "baz" +} +', $manipulator->getContents()); + } + + public function testAddMainKeyWithContentHavingDollarSignFollowedByDigit() + { + $manipulator = new JsonManipulator('{ + "foo": "bar" +}'); + + $this->assertTrue($manipulator->addMainKey('bar', '$1baz')); + $this->assertEquals('{ + "foo": "bar", + "bar": "$1baz" +} +', $manipulator->getContents()); + } + + public function testAddMainKeyWithContentHavingDollarSignFollowedByDigit2() + { + $manipulator = new JsonManipulator('{}'); + + $this->assertTrue($manipulator->addMainKey('foo', '$1bar')); + $this->assertEquals('{ + "foo": "$1bar" +} +', $manipulator->getContents()); + } + + public function testUpdateMainKey() + { + $manipulator = new JsonManipulator('{ + "foo": "bar" +}'); + + $this->assertTrue($manipulator->addMainKey('foo', 'baz')); + $this->assertEquals('{ + "foo": "baz" +} +', $manipulator->getContents()); + } + + public function testUpdateMainKey2() + { + $manipulator = new JsonManipulator('{ + "a": { + "foo": "bar", + "baz": "qux" + }, + "foo": "bar", + "baz": "bar" +}'); + + $this->assertTrue($manipulator->addMainKey('foo', 'baz')); + $this->assertTrue($manipulator->addMainKey('baz', 'quux')); + $this->assertEquals('{ + "a": { + "foo": "bar", + "baz": "qux" + }, + "foo": "baz", + "baz": "quux" +} +', $manipulator->getContents()); + } + + public function testUpdateMainKey3() + { + $manipulator = new JsonManipulator('{ + "require": { + "php": "5.*" + }, + "require-dev": { + "foo": "bar" + } +}'); + + $this->assertTrue($manipulator->addMainKey('require-dev', array('foo' => 'qux'))); + $this->assertEquals('{ + "require": { + "php": "5.*" + }, + "require-dev": { + "foo": "qux" + } +} +', $manipulator->getContents()); + } + + public function testUpdateMainKeyWithContentHavingDollarSignFollowedByDigit() + { + $manipulator = new JsonManipulator('{ + "foo": "bar" +}'); + + $this->assertTrue($manipulator->addMainKey('foo', '$1bar')); + $this->assertEquals('{ + "foo": "$1bar" +} +', $manipulator->getContents()); + } + + public function testRemoveMainKey() + { + $manipulator = new JsonManipulator('{ + "repositories": [ + { + "package": { + "require": { + "this/should-not-end-up-in-root-require": "~2.0" + }, + "require-dev": { + "this/should-not-end-up-in-root-require-dev": "~2.0" + } + } + } + ], + "require": { + "package/a": "*", + "package/b": "*", + "package/c": "*" + }, + "foo": "bar", + "require-dev": { + "package/d": "*" + } +}'); + + $this->assertTrue($manipulator->removeMainKey('repositories')); + $this->assertEquals('{ + "require": { + "package/a": "*", + "package/b": "*", + "package/c": "*" + }, + "foo": "bar", + "require-dev": { + "package/d": "*" + } +} +', $manipulator->getContents()); + + $this->assertTrue($manipulator->removeMainKey('foo')); + $this->assertEquals('{ + "require": { + "package/a": "*", + "package/b": "*", + "package/c": "*" + }, + "require-dev": { + "package/d": "*" + } +} +', $manipulator->getContents()); + + $this->assertTrue($manipulator->removeMainKey('require')); + $this->assertTrue($manipulator->removeMainKey('require-dev')); + $this->assertEquals('{ +} +', $manipulator->getContents()); + } + + public function testIndentDetection() + { + $manipulator = new JsonManipulator('{ + + "require": { + "php": "5.*" + } +}'); + + $this->assertTrue($manipulator->addMainKey('require-dev', array('foo' => 'qux'))); + $this->assertEquals('{ + + "require": { + "php": "5.*" + }, + "require-dev": { + "foo": "qux" + } +} +', $manipulator->getContents()); + } + + public function testRemoveMainKeyAtEndOfFile() + { + $manipulator = new JsonManipulator('{ + "require": { + "package/a": "*" + } +} +'); + $this->assertTrue($manipulator->addMainKey('homepage', 'http...')); + $this->assertTrue($manipulator->addMainKey('license', 'mit')); + $this->assertEquals('{ + "require": { + "package/a": "*" + }, + "homepage": "http...", + "license": "mit" +} +', $manipulator->getContents()); + + $this->assertTrue($manipulator->removeMainKey('homepage')); + $this->assertTrue($manipulator->removeMainKey('license')); + $this->assertEquals('{ + "require": { + "package/a": "*" + } +} +', $manipulator->getContents()); + + } + + // WP_CLI: begin caseInsensitive. + + /** + * @dataProvider providerAddLinkCaseInsensitive + */ + public function testAddLinkCaseInsensitive($json, $type, $package, $constraint, $sortPackages, $expected) + { + $manipulator = new JsonManipulator($json); + $this->assertTrue($manipulator->addLink($type, $package, $constraint, $sortPackages, true /*caseInsensitive*/)); + $this->assertEquals($expected, $manipulator->getContents()); + } + + public function providerAddLinkCaseInsensitive() + { + return array( + array( + '{ + "require": + { + "foo": "bar", + "vendor/baz": "qux", + "vEnDoR/bAz": "qux" + } +}', + 'require', + 'vEnDoR/bAz', + 'qux', + false, + '{ + "require": + { + "foo": "bar", + "vEnDoR/bAz": "qux" + } +} +', + ), + array( + '{ + "require": + { + "vendor/baz": "qux", + "Foo": "bar", + "Vendor/bazz": "qux", + "foo": "bar", + "vendor/bazz": "qux", + "Vendor/baz": "qux" + } +}', + 'require', + 'Vendor/baz', + 'qux', + true, + '{ + "require": + { + "Foo": "bar", + "Vendor/baz": "qux", + "Vendor/bazz": "qux", + "foo": "bar", + "vendor/bazz": "qux" + } +} +', + ), + ); + } + + /** + * @dataProvider providerAddSubNodeCase + */ + public function testAddSubNodeCase($json, $mainNode, $name, $caseInsensitive, $expected) + { + $manipulator = new JsonManipulator($json); + + $this->assertTrue($manipulator->addSubNode($mainNode, $name, "val" /*value*/, $caseInsensitive)); + $this->assertSame($expected, $manipulator->getContents()); + } + + public function providerAddSubNodeCase() + { + return array( + array( + '{ + "require": + { + "foo": "bar", + "vendor/baz": "qux", + "vEnDoR/bAz": "qux" + } +}', + 'require', + 'vendor/BAZ', + false, + '{ + "require": + { + "foo": "bar", + "vendor/baz": "qux", + "vEnDoR/bAz": "qux", + "vendor/BAZ": "val" + } +} +', + ), + array( + '{ + "require": + { + "foo": "bar", + "vendor/baz": "qux", + "vEnDoR/bAz": "qux" + } +}', + 'require', + 'vendor/BAZ', + true, + '{ + "require": + { + "foo": "bar", + "vendor/BAZ": "val" + } +} +', + ), + ); + } + + /** + * @dataProvider providerRemoveSubNodeCaseInsensitive + */ + public function testRemoveSubNodeCaseInsensitive($json, $mainNode, $name, $expected, $expectedContent = null) + { + $manipulator = new JsonManipulator($json); + + $this->assertEquals($expected, $manipulator->removeSubNode($mainNode, $name, true)); + if (null !== $expectedContent) { + $this->assertEquals($expectedContent, $manipulator->getContents()); + } + } + + public function providerRemoveSubNodeCaseInsensitive() + { + return array( + array( + '{ + "repositories": { + "foo": { + "foo": "bar", + "bar": "baz" + }, + "Foo": { + "foo": "bar", + "bar": "baz" + }, + "bar": { + "foo": "bar", + "bar": "baz" + } + } +}', + 'repositories', + 'foo', + true, + '{ + "repositories": { + "bar": { + "foo": "bar", + "bar": "baz" + } + } +} +', + ), + array( + '{ + "require": { + "vEnDoR/bAz": "qux", + "vendor/baz": "qux", + "foo": "bar" + } +}', + 'require', + 'vEnDoR/bAz', + true, + '{ + "require": { + "foo": "bar" + } +} +', + ), + array( + '{ + "require": { + "foo": "bar", + "vendor/baz": "qux", + "vEnDoR/bAz": "qux" + } +}', + 'require', + 'vendor/baz', + true, + '{ + "require": { + "foo": "bar" + } +} +', + ), + ); + } + // WP_CLI: end caseInsensitive. + +}