From e72e17d9c2fe4ad1dc2dbc86b5c40060c3f4039e Mon Sep 17 00:00:00 2001 From: harshadyeola Date: Fri, 30 Oct 2015 12:56:53 +0530 Subject: [PATCH] prepare delete command --- VERSION | 1 + bin/ee | 46 + composer.json | 32 + php/EE_CLI/Configurator.php | 223 ++++ php/EE_CLI/Dispatcher/CommandFactory.php | 93 ++ php/EE_CLI/Dispatcher/CompositeCommand.php | 261 +++++ php/EE_CLI/Dispatcher/RootCommand.php | 60 + php/EE_CLI/Dispatcher/Subcommand.php | 299 +++++ php/EE_CLI/DocParser.php | 129 +++ php/EE_CLI/Loggers/Base.php | 51 + php/EE_CLI/Loggers/Quiet.php | 57 + php/EE_CLI/Loggers/Regular.php | 79 ++ php/EE_CLI/Runner.php | 511 +++++++++ php/EE_CLI/SynopsisParser.php | 110 ++ php/EE_CLI/SynopsisValidator.php | 163 +++ php/Spyc.php | 1035 ++++++++++++++++++ php/boot-fs.php | 17 + php/class-ee-cli-command.php | 11 + php/class-ee-cli.php | 505 +++++++++ php/commands/site.php | 30 + php/config-spec.php | 102 ++ php/dispatcher.php | 19 + php/ee-cli.php | 21 + php/export/class-wp-export-oxymel.php | 25 + php/export/class-wp-export-query.php | 372 +++++++ php/export/class-wp-export-wxr-formatter.php | 261 +++++ php/export/functions.export.php | 41 + php/export/iterators.php | 80 ++ php/export/writers.php | 184 ++++ php/utils.php | 575 ++++++++++ templates/man-params.mustache | 14 + 31 files changed, 5407 insertions(+) create mode 100644 VERSION create mode 100755 bin/ee create mode 100644 composer.json create mode 100644 php/EE_CLI/Configurator.php create mode 100644 php/EE_CLI/Dispatcher/CommandFactory.php create mode 100644 php/EE_CLI/Dispatcher/CompositeCommand.php create mode 100644 php/EE_CLI/Dispatcher/RootCommand.php create mode 100644 php/EE_CLI/Dispatcher/Subcommand.php create mode 100644 php/EE_CLI/DocParser.php create mode 100644 php/EE_CLI/Loggers/Base.php create mode 100644 php/EE_CLI/Loggers/Quiet.php create mode 100644 php/EE_CLI/Loggers/Regular.php create mode 100644 php/EE_CLI/Runner.php create mode 100644 php/EE_CLI/SynopsisParser.php create mode 100644 php/EE_CLI/SynopsisValidator.php create mode 100644 php/Spyc.php create mode 100644 php/boot-fs.php create mode 100644 php/class-ee-cli-command.php create mode 100644 php/class-ee-cli.php create mode 100644 php/commands/site.php create mode 100644 php/config-spec.php create mode 100644 php/dispatcher.php create mode 100644 php/ee-cli.php create mode 100644 php/export/class-wp-export-oxymel.php create mode 100644 php/export/class-wp-export-query.php create mode 100644 php/export/class-wp-export-wxr-formatter.php create mode 100644 php/export/functions.export.php create mode 100644 php/export/iterators.php create mode 100644 php/export/writers.php create mode 100644 php/utils.php create mode 100644 templates/man-params.mustache diff --git a/VERSION b/VERSION new file mode 100644 index 00000000..c6c794a9 --- /dev/null +++ b/VERSION @@ -0,0 +1 @@ +0.1-alpha diff --git a/bin/ee b/bin/ee new file mode 100755 index 00000000..3d9d5982 --- /dev/null +++ b/bin/ee @@ -0,0 +1,46 @@ +#!/usr/bin/env sh +# +# This wrapper script has been adapted from the equivalent drush wrapper +# and 99.9% of all credit should go to the authors of that project: +# http://drupal.org/project/drush +# And 0.09% to the author of this project: +# https://github.com/88mph/wpadmin/blob/master/wpadmin.php + +# Get the absolute path of this executable +ORIGDIR="$(pwd)" +SELF_PATH="$(cd -P -- "$(dirname -- "$0")" && pwd -P)" && SELF_PATH="$SELF_PATH/$(basename -- "$0")" + +# Resolve symlinks - this is the equivalent of "readlink -f", but also works with non-standard OS X readlink. +while [ -h "$SELF_PATH" ]; do + # 1) cd to directory of the symlink + # 2) cd to the directory of where the symlink points + # 3) Get the pwd + # 4) Append the basename + DIR="$(dirname -- "$SELF_PATH")" + SYM="$(readlink "$SELF_PATH")" + SELF_PATH="$(cd "$DIR" && cd "$(dirname -- "$SYM")" && pwd)/$(basename -- "$SYM")" +done +cd "$ORIGDIR" + +# Build the path to the root PHP file +SCRIPT_PATH="$(dirname "$SELF_PATH")/../php/boot-fs.php" + +case $(uname -a) in + CYGWIN*) + SCRIPT_PATH="$(cygpath -w -a -- "$SCRIPT_PATH")" ;; +esac + +if [ ! -z "$EE_CLI_PHP" ] ; then + # Use the EE_CLI_PHP environment variable if it is available. + php="$EE_CLI_PHP" +else + # Default to using the php that we find on the PATH. + # Note that we need the full path to php here for Dreamhost, which behaves oddly. See http://drupal.org/node/662926 + php="`which php`" +fi + +# Pass in the path to php so that wp-cli knows which one +# to use if it re-launches itself to run other commands. +export EE_CLI_PHP_USED="$php" + +exec "$php" $EE_CLI_PHP_ARGS "$SCRIPT_PATH" "$@" diff --git a/composer.json b/composer.json new file mode 100644 index 00000000..421cb7f2 --- /dev/null +++ b/composer.json @@ -0,0 +1,32 @@ +{ + "name": "rtCamp/easyengine", + "description": "A command line interface for EasyEngine", + "keywords": [ "cli", "EasyEngine" ], + "homepage": "http://easyengine.io", + "license": "MIT", + "bin": [ + "bin/ee" + ], + "require": { + "php": ">=5.3.2", + "wp-cli/php-cli-tools": "0.10.5", + "mustache/mustache": "~2.4", + "composer/semver": "1.0.0", + "ramsey/array_column": "~1.1", + "rmccue/requests": "~1.6", + "symfony/finder": "~2.3", + "nb/oxymel": "0.1.0" + }, + "require-dev": { + "phpunit/phpunit": "3.7.*", + "behat/behat": "2.5.*" + }, + "suggest": { + "psy/psysh": "Enhanced `wp shell` functionality" + }, + "autoload": { + "psr-0": { "EE_CLI": "php" }, + "files": [ "php/Spyc.php" ], + "classmap": [ "php/export" ] + } +} diff --git a/php/EE_CLI/Configurator.php b/php/EE_CLI/Configurator.php new file mode 100644 index 00000000..21781cd6 --- /dev/null +++ b/php/EE_CLI/Configurator.php @@ -0,0 +1,223 @@ +spec = include $path; + + $defaults = array( + 'runtime' => false, + 'file' => false, + 'synopsis' => '', + 'default' => null, + 'multiple' => false, + ); + + foreach ( $this->spec as $key => &$details ) { + $details = array_merge( $defaults, $details ); + + $this->config[ $key ] = $details['default']; + } + } + + /** + * Get declared configuration values as an array. + * + * @return array + */ + function to_array() { + return array( $this->config, $this->extra_config ); + } + + /** + * Get configuration specification, i.e. list of accepted keys. + * + * @return array + */ + function get_spec() { + return $this->spec; + } + + /** + * Splits a list of arguments into positional, associative and config. + * + * @param array(string) + * @return array(array) + */ + public function parse_args( $arguments ) { + list( $positional_args, $mixed_args ) = self::extract_assoc( $arguments ); + list( $assoc_args, $runtime_config ) = $this->unmix_assoc_args( $mixed_args ); + return array( $positional_args, $assoc_args, $runtime_config ); + } + + /** + * Splits positional args from associative args. + * + * @param array + * @return array(array) + */ + public static function extract_assoc( $arguments ) { + $positional_args = $assoc_args = array(); + + foreach ( $arguments as $arg ) { + if ( preg_match( '|^--no-([^=]+)$|', $arg, $matches ) ) { + $assoc_args[] = array( $matches[1], false ); + } elseif ( preg_match( '|^--([^=]+)$|', $arg, $matches ) ) { + $assoc_args[] = array( $matches[1], true ); + } elseif ( preg_match( '|^--([^=]+)=(.*)|s', $arg, $matches ) ) { + $assoc_args[] = array( $matches[1], $matches[2] ); + } else { + $positional_args[] = $arg; + } + } + + return array( $positional_args, $assoc_args ); + } + + /** + * Separate runtime parameters from command-specific parameters. + * + * @param array $mixed_args + * @return array + */ + private function unmix_assoc_args( $mixed_args ) { + $assoc_args = $runtime_config = array(); + + foreach ( $mixed_args as $tmp ) { + list( $key, $value ) = $tmp; + + if ( isset( $this->spec[ $key ] ) && $this->spec[ $key ]['runtime'] !== false ) { + $details = $this->spec[ $key ]; + + if ( isset( $details['deprecated'] ) ) { + fwrite( STDERR, "WP-CLI: The --{$key} global parameter is deprecated. {$details['deprecated']}\n" ); + } + + if ( $details['multiple'] ) { + $runtime_config[ $key ][] = $value; + } else { + $runtime_config[ $key ] = $value; + } + } else { + $assoc_args[ $key ] = $value; + } + } + + return array( $assoc_args, $runtime_config ); + } + + /** + * Load a YAML file of parameters into scope. + * + * @param string $path Path to YAML file. + */ + public function merge_yml( $path ) { + foreach ( self::load_yml( $path ) as $key => $value ) { + if ( !isset( $this->spec[ $key ] ) || false === $this->spec[ $key ]['file'] ) { + $this->extra_config[ $key ] = $value; + } elseif ( $this->spec[ $key ]['multiple'] ) { + self::arrayify( $value ); + $this->config[ $key ] = array_merge( $this->config[ $key ], $value ); + } else { + $this->config[ $key ] = $value; + } + } + } + + /** + * Merge an array of values into the configurator config. + * + * @param array $config + */ + public function merge_array( $config ) { + foreach ( $this->spec as $key => $details ) { + if ( false !== $details['runtime'] && isset( $config[ $key ] ) ) { + $value = $config[ $key ]; + + if ( $details['multiple'] ) { + self::arrayify( $value ); + $this->config[ $key ] = array_merge( $this->config[ $key ], $value ); + } else { + $this->config[ $key ] = $value; + } + } + } + } + + /** + * Load values from a YAML file. + * + * @param string $yml_file Path to the YAML file + * @return array $config Declared configuration values + */ + private static function load_yml( $yml_file ) { + if ( !$yml_file ) + return array(); + + $config = spyc_load_file( $yml_file ); + + // Make sure config-file-relative paths are made absolute. + $yml_file_dir = dirname( $yml_file ); + + if ( isset( $config['path'] ) ) + self::absolutize( $config['path'], $yml_file_dir ); + + if ( isset( $config['require'] ) ) { + self::arrayify( $config['require'] ); + foreach ( $config['require'] as &$path ) { + self::absolutize( $path, $yml_file_dir ); + } + } + + return $config; + } + + /** + * Conform a variable to an array. + * + * @param mixed $val A string or an array + */ + private static function arrayify( &$val ) { + if ( !is_array( $val ) ) { + $val = array( $val ); + } + } + + /** + * Make a path absolute. + * + * @param string $path Path to file. + * @param string $base Base path to prepend. + */ + private static function absolutize( &$path, $base ) { + if ( !empty( $path ) && !\EE_CLI\Utils\is_path_absolute( $path ) ) { + $path = $base . DIRECTORY_SEPARATOR . $path; + } + } + +} diff --git a/php/EE_CLI/Dispatcher/CommandFactory.php b/php/EE_CLI/Dispatcher/CommandFactory.php new file mode 100644 index 00000000..2ae08d1a --- /dev/null +++ b/php/EE_CLI/Dispatcher/CommandFactory.php @@ -0,0 +1,93 @@ +hasMethod( '__invoke' ) ) { + $command = self::create_subcommand( $parent, $name, $reflection->name, + $reflection->getMethod( '__invoke' ) ); + } else { + $command = self::create_composite_command( $parent, $name, $reflection ); + } + + return $command; + } + + /** + * Create a new Subcommand instance. + * + * @param mixed $parent The new command's parent Composite command + * @param string $name Represents how the command should be invoked + * @param string $class A subclass of EE_CLI_Command + * @param string $method Class method to be called upon invocation. + */ + private static function create_subcommand( $parent, $name, $class_name, $method ) { + $docparser = new \EE_CLI\DocParser( $method->getDocComment() ); + + if ( !$name ) + $name = $docparser->get_tag( 'subcommand' ); + + if ( !$name ) + $name = $method->name; + + $method_name = $method->name; + + $when_invoked = function ( $args, $assoc_args ) use ( $class_name, $method_name ) { + call_user_func( array( new $class_name, $method_name ), $args, $assoc_args ); + }; + + return new Subcommand( $parent, $name, $docparser, $when_invoked ); + } + + /** + * Create a new Composite command instance. + * + * @param mixed $parent The new command's parent Root or Composite command + * @param string $name Represents how the command should be invoked + * @param ReflectionClass $reflection + */ + private static function create_composite_command( $parent, $name, $reflection ) { + $docparser = new \EE_CLI\DocParser( $reflection->getDocComment() ); + + $container = new CompositeCommand( $parent, $name, $docparser ); + + foreach ( $reflection->getMethods() as $method ) { + if ( !self::is_good_method( $method ) ) + continue; + + $subcommand = self::create_subcommand( $container, false, $reflection->name, $method ); + + $subcommand_name = $subcommand->get_name(); + + $container->add_subcommand( $subcommand_name, $subcommand ); + } + + return $container; + } + + /** + * Check whether a method is actually callable. + * + * @param ReflectionMethod $method + * @return bool + */ + private static function is_good_method( $method ) { + return $method->isPublic() && !$method->isStatic() && 0 !== strpos( $method->getName(), '__' ); + } +} diff --git a/php/EE_CLI/Dispatcher/CompositeCommand.php b/php/EE_CLI/Dispatcher/CompositeCommand.php new file mode 100644 index 00000000..18f1b881 --- /dev/null +++ b/php/EE_CLI/Dispatcher/CompositeCommand.php @@ -0,0 +1,261 @@ +parent = $parent; + + $this->name = $name; + + $this->shortdesc = $docparser->get_shortdesc(); + $this->longdesc = $docparser->get_longdesc(); + $this->longdesc .= $this->get_global_params(); + $this->docparser = $docparser; + + $when_to_invoke = $docparser->get_tag( 'when' ); + if ( $when_to_invoke ) { + \EE_CLI::get_runner()->register_early_invoke( $when_to_invoke, $this ); + } + } + + /** + * Get the parent composite (or root) command + * + * @return mixed + */ + public function get_parent() { + return $this->parent; + } + + /** + * Add a named subcommand to this composite command's + * set of contained subcommands. + * + * @param string $name Represents how subcommand should be invoked + * @param \EE_CLI\Dispatcher\Subcommand + */ + public function add_subcommand( $name, $command ) { + $this->subcommands[ $name ] = $command; + } + + /** + * Composite commands always contain subcommands. + * + * @return true + */ + public function can_have_subcommands() { + return true; + } + + /** + * Get the subcommands contained by this composite + * command. + * + * @return array + */ + public function get_subcommands() { + ksort( $this->subcommands ); + + return $this->subcommands; + } + + /** + * Get the name of this composite command. + * + * @return string + */ + public function get_name() { + return $this->name; + } + + /** + * Get the short description for this composite + * command. + * + * @return string + */ + public function get_shortdesc() { + return $this->shortdesc; + } + + /** + * Get the long description for this composite + * command. + * + * @return string + */ + public function get_longdesc() { + return $this->longdesc; + } + + /** + * Get the synopsis for this composite command. + * As a collection of subcommands, the composite + * command is only intended to invoke those + * subcommands. + * + * @return string + */ + public function get_synopsis() { + return ''; + } + + /** + * Get the usage for this composite command. + * + * @return string + */ + public function get_usage( $prefix ) { + return sprintf( "%s%s %s", + $prefix, + implode( ' ', get_path( $this ) ), + $this->get_synopsis() + ); + } + + /** + * Show the usage for all subcommands contained + * by the composite command. + */ + public function show_usage() { + $methods = $this->get_subcommands(); + + $i = 0; + + foreach ( $methods as $name => $subcommand ) { + $prefix = ( 0 == $i++ ) ? 'usage: ' : ' or: '; + + if ( \EE_CLI::get_runner()->is_command_disabled( $subcommand ) ) { + continue; + } + + \EE_CLI::line( $subcommand->get_usage( $prefix ) ); + } + + $cmd_name = implode( ' ', array_slice( get_path( $this ), 1 ) ); + + \EE_CLI::line(); + \EE_CLI::line( "See 'wp help $cmd_name ' for more information on a specific command." ); + } + + /** + * When a composite command is invoked, it shows usage + * docs for its subcommands. + * + * @param array $args + * @param array $assoc_args + * @param array $extra_args + */ + public function invoke( $args, $assoc_args, $extra_args ) { + $this->show_usage(); + } + + /** + * Given supplied arguments, find a contained + * subcommand + * + * @param array $args + * @return \EE_CLI\Dispatcher\Subcommand|false + */ + public function find_subcommand( &$args ) { + $name = array_shift( $args ); + + $subcommands = $this->get_subcommands(); + + if ( !isset( $subcommands[ $name ] ) ) { + $aliases = self::get_aliases( $subcommands ); + + if ( isset( $aliases[ $name ] ) ) { + $name = $aliases[ $name ]; + } + } + + if ( !isset( $subcommands[ $name ] ) ) + return false; + + return $subcommands[ $name ]; + } + + /** + * Get any registered aliases for this composite command's + * subcommands. + * + * @param array $subcommands + * @return array + */ + private static function get_aliases( $subcommands ) { + $aliases = array(); + + foreach ( $subcommands as $name => $subcommand ) { + $alias = $subcommand->get_alias(); + if ( $alias ) + $aliases[ $alias ] = $name; + } + + return $aliases; + } + + /** + * Composite commands can only be known by one name. + * + * @return false + */ + public function get_alias() { + return false; + } + + /*** + * Get the list of global parameters + * + * @param string $root_command whether to include or not root command specific description + * @return string + */ + protected function get_global_params( $root_command = false ) { + $binding = array(); + $binding['root_command'] = $root_command; + + foreach ( \EE_CLI::get_configurator()->get_spec() as $key => $details ) { + if ( false === $details['runtime'] ) + continue; + + if ( isset( $details['deprecated'] ) ) + continue; + + if ( isset( $details['hidden'] ) ) + continue; + + if ( true === $details['runtime'] ) + $synopsis = "--[no-]$key"; + else + $synopsis = "--$key" . $details['runtime']; + + $binding['parameters'][] = array( + 'synopsis' => $synopsis, + 'desc' => $details['desc'] + ); + } + + return Utils\mustache_render( 'man-params.mustache', $binding ); + } +} diff --git a/php/EE_CLI/Dispatcher/RootCommand.php b/php/EE_CLI/Dispatcher/RootCommand.php new file mode 100644 index 00000000..9de88782 --- /dev/null +++ b/php/EE_CLI/Dispatcher/RootCommand.php @@ -0,0 +1,60 @@ +parent = false; + + $this->name = 'wp'; + + $this->shortdesc = 'Manage WordPress through the command-line.'; + } + + /** + * Get the human-readable long description. + * + * @return string + */ + public function get_longdesc() { + return $this->get_global_params( true ); + } + + /** + * Find a subcommand registered on the root + * command. + * + * @param array $args + * @return \EE_CLI\Dispatcher\Subcommand|false + */ + public function find_subcommand( &$args ) { + $command = array_shift( $args ); + + Utils\load_command( $command ); + + if ( !isset( $this->subcommands[ $command ] ) ) { + return false; + } + + return $this->subcommands[ $command ]; + } + + /** + * Get all registered subcommands. + * + * @return array + */ + public function get_subcommands() { + Utils\load_all_commands(); + + return parent::get_subcommands(); + } +} diff --git a/php/EE_CLI/Dispatcher/Subcommand.php b/php/EE_CLI/Dispatcher/Subcommand.php new file mode 100644 index 00000000..2b2681c6 --- /dev/null +++ b/php/EE_CLI/Dispatcher/Subcommand.php @@ -0,0 +1,299 @@ +when_invoked = $when_invoked; + + $this->alias = $docparser->get_tag( 'alias' ); + + $this->synopsis = $docparser->get_synopsis(); + if ( !$this->synopsis && $this->longdesc ) { + $this->synopsis = self::extract_synopsis( $this->longdesc ); + } + } + + /** + * Extract the synopsis from PHPdoc string. + * + * @param string $longdesc Command docs via PHPdoc + * @return string + */ + private static function extract_synopsis( $longdesc ) { + preg_match_all( '/(.+?)[\r\n]+:/', $longdesc, $matches ); + return implode( ' ', $matches[1] ); + } + + /** + * Subcommands can't have subcommands because they + * represent code to be executed. + * + * @return bool + */ + function can_have_subcommands() { + return false; + } + + /** + * Get the synopsis string for this subcommand. + * A synopsis defines what runtime arguments are + * expected, useful to humans and argument validation. + * + * @return string + */ + function get_synopsis() { + return $this->synopsis; + } + + /** + * If an alias is set, grant access to it. + * Aliases permit subcommands to be instantiated + * with a secondary identity. + * + * @return string + */ + function get_alias() { + return $this->alias; + } + + /** + * Print the usage details to the end user. + * + * @param string $prefix + */ + function show_usage( $prefix = 'usage: ' ) { + \EE_CLI::line( $this->get_usage( $prefix ) ); + } + + /** + * Get the usage of the subcommand as a formatted string. + * + * @param string $prefix + * @return string + */ + function get_usage( $prefix ) { + return sprintf( "%s%s %s", + $prefix, + implode( ' ', get_path( $this ) ), + $this->get_synopsis() + ); + } + + /** + * Wrapper for CLI Tools' prompt() method. + * + * @param string $question + * @param string $default + * @return string|false + */ + private function prompt( $question, $default ) { + + try { + $response = \cli\prompt( $question, $default ); + } catch( \Exception $e ) { + \EE_CLI::line(); + return false; + } + + return $response; + } + + /** + * Interactively prompt the user for input + * based on defined synopsis and passed arguments. + * + * @param array $args + * @param array $assoc_args + * @return array + */ + private function prompt_args( $args, $assoc_args ) { + + $synopsis = $this->get_synopsis(); + + if ( ! $synopsis ) + return array( $args, $assoc_args ); + + $spec = array_filter( \EE_CLI\SynopsisParser::parse( $synopsis ), function( $spec_arg ) { + return in_array( $spec_arg['type'], array( 'generic', 'positional', 'assoc', 'flag' ) ); + }); + + $spec = array_values( $spec ); + + // 'positional' arguments are positional (aka zero-indexed) + // so $args needs to be reset before prompting for new arguments + $args = array(); + foreach( $spec as $key => $spec_arg ) { + + $current_prompt = ( $key + 1 ) . '/' . count( $spec ) . ' '; + $default = ( $spec_arg['optional'] ) ? '' : false; + + // 'generic' permits arbitrary key=value (e.g. [--=] ) + if ( 'generic' == $spec_arg['type'] ) { + + list( $key_token, $value_token ) = explode( '=', $spec_arg['token'] ); + + $repeat = false; + do { + if ( ! $repeat ) + $key_prompt = $current_prompt . $key_token; + else + $key_prompt = str_repeat( " ", strlen( $current_prompt ) ) . $key_token; + + $key = $this->prompt( $key_prompt, $default ); + if ( false === $key ) + return array( $args, $assoc_args ); + + if ( $key ) { + $key_prompt_count = strlen( $key_prompt ) - strlen( $value_token ) - 1; + $value_prompt = str_repeat( " ", $key_prompt_count ) . '=' . $value_token; + + $value = $this->prompt( $value_prompt, $default ); + if ( false === $value ) + return array( $args, $assoc_args ); + + $assoc_args[$key] = $value; + + $repeat = true; + $required = false; + } else { + $repeat = false; + } + + } while( $required || $repeat ); + + } else { + + $prompt = $current_prompt . $spec_arg['token']; + if ( 'flag' == $spec_arg['type'] ) + $prompt .= ' (Y/n)'; + + $response = $this->prompt( $prompt, $default ); + if ( false === $response ) + return array( $args, $assoc_args ); + + if ( $response ) { + switch ( $spec_arg['type'] ) { + case 'positional': + if ( $spec_arg['repeating'] ) + $response = explode( ' ', $response ); + else + $response = array( $response ); + $args = array_merge( $args, $response ); + break; + case 'assoc': + $assoc_args[$spec_arg['name']] = $response; + break; + case 'flag': + if ( 'Y' == $response ) + $assoc_args[$spec_arg['name']] = true; + break; + } + } + } + } + + return array( $args, $assoc_args ); + } + + /** + * Validate the supplied arguments to the command. + * Throws warnings or errors if arguments are missing + * or invalid. + * + * @param array $args + * @param array $assoc_args + * @param array $extra_args + * @return array list of invalid $assoc_args keys to unset + */ + private function validate_args( $args, $assoc_args, $extra_args ) { + $synopsis = $this->get_synopsis(); + if ( !$synopsis ) + return array(); + + $validator = new \EE_CLI\SynopsisValidator( $synopsis ); + + $cmd_path = implode( ' ', get_path( $this ) ); + foreach ( $validator->get_unknown() as $token ) { + \EE_CLI::warning( sprintf( + "The `%s` command has an invalid synopsis part: %s", + $cmd_path, $token + ) ); + } + + if ( !$validator->enough_positionals( $args ) ) { + $this->show_usage(); + exit(1); + } + + $unknown_positionals = $validator->unknown_positionals( $args ); + if ( !empty( $unknown_positionals ) ) { + \EE_CLI::error( 'Too many positional arguments: ' . + implode( ' ', $unknown_positionals ) ); + } + + list( $errors, $to_unset ) = $validator->validate_assoc( + array_merge( \EE_CLI::get_config(), $extra_args, $assoc_args ) + ); + + if ( $this->name != 'help' ) { + foreach ( $validator->unknown_assoc( $assoc_args ) as $key ) { + $errors['fatal'][] = "unknown --$key parameter"; + } + } + + if ( !empty( $errors['fatal'] ) ) { + $out = 'Parameter errors:'; + foreach ( $errors['fatal'] as $key => $error ) { + $out .= "\n {$error}"; + if ( $desc = $this->docparser->get_param_desc( $key ) ) { + $out .= " ({$desc})"; + } + } + + \EE_CLI::error( $out ); + } + + array_map( '\\EE_CLI::warning', $errors['warning'] ); + + return $to_unset; + } + + /** + * Invoke the subcommand with the supplied arguments. + * Given a --prompt argument, interactively request input + * from the end user. + * + * @param array $args + * @param array $assoc_args + */ + public function invoke( $args, $assoc_args, $extra_args ) { + if ( \EE_CLI::get_config( 'prompt' ) ) + list( $args, $assoc_args ) = $this->prompt_args( $args, $assoc_args ); + + $to_unset = $this->validate_args( $args, $assoc_args, $extra_args ); + + foreach ( $to_unset as $key ) { + unset( $assoc_args[ $key ] ); + } + + $path = get_path( $this->get_parent() ); + \EE_CLI::do_hook( 'before_invoke:' . implode( ' ', array_slice( $path, 1 ) ) ); + + call_user_func( $this->when_invoked, $args, array_merge( $extra_args, $assoc_args ) ); + + \EE_CLI::do_hook( 'after_invoke:' . implode( ' ', array_slice( $path, 1 ) ) ); + } +} diff --git a/php/EE_CLI/DocParser.php b/php/EE_CLI/DocParser.php new file mode 100644 index 00000000..dfef5416 --- /dev/null +++ b/php/EE_CLI/DocParser.php @@ -0,0 +1,129 @@ +docComment = self::remove_decorations( $docComment ); + } + + /** + * Remove unused cruft from PHPdoc comment. + * + * @param string $comment PHPdoc comment. + * @return string + */ + private static function remove_decorations( $comment ) { + $comment = preg_replace( '|^/\*\*[\r\n]+|', '', $comment ); + $comment = preg_replace( '|\n[\t ]*\*/$|', '', $comment ); + $comment = preg_replace( '|^[\t ]*\* ?|m', '', $comment ); + + return $comment; + } + + /** + * Get the command's short description (e.g. summary). + * + * @return string + */ + public function get_shortdesc() { + if ( !preg_match( '|^([^@][^\n]+)\n*|', $this->docComment, $matches ) ) + return ''; + + return $matches[1]; + } + + /** + * Get the command's full description + * + * @return string + */ + public function get_longdesc() { + $shortdesc = $this->get_shortdesc(); + if ( !$shortdesc ) + return ''; + + $longdesc = substr( $this->docComment, strlen( $shortdesc ) ); + + $lines = array(); + foreach ( explode( "\n", $longdesc ) as $line ) { + if ( 0 === strpos( $line, '@' ) ) + break; + + $lines[] = $line; + } + $longdesc = trim( implode( $lines, "\n" ) ); + + return $longdesc; + } + + /** + * Get the value for a given tag (e.g. "@alias" or "@subcommand") + * + * @param string $name Name for the tag, without '@' + * @return string + */ + public function get_tag( $name ) { + if ( preg_match( '|^@' . $name . '\s+([a-z-_]+)|m', $this->docComment, $matches ) ) + return $matches[1]; + + return ''; + } + + /** + * Get the command's synopsis. + * + * @return string + */ + public function get_synopsis() { + if ( !preg_match( '|^@synopsis\s+(.+)|m', $this->docComment, $matches ) ) + return ''; + + return $matches[1]; + } + + /** + * Get the description for a given argument. + * + * @param string $name Argument's doc name. + * @return string + */ + public function get_arg_desc( $name ) { + + if ( preg_match( "/\[?<{$name}>.+\n: (.+?)(\n|$)/", $this->docComment, $matches ) ) { + return $matches[1]; + } + + return ''; + + } + + /** + * Get the description for a given parameter. + * + * @param string $key Parameter's key. + * @return string + */ + public function get_param_desc( $key ) { + + if ( preg_match( "/\[?--{$key}=.+\n: (.+?)(\n|$)/", $this->docComment, $matches ) ) { + return $matches[1]; + } + + return ''; + } + +} diff --git a/php/EE_CLI/Loggers/Base.php b/php/EE_CLI/Loggers/Base.php new file mode 100644 index 00000000..1c11fdf7 --- /dev/null +++ b/php/EE_CLI/Loggers/Base.php @@ -0,0 +1,51 @@ +config['debug'] ) { + $time = round( microtime( true ) - EE_CLI_START_MICROTIME, 3 ); + $this->_line( "$message ({$time}s)", 'Debug', '%B', STDERR ); + } + } + + /** + * Write a string to a resource. + * + * @param resource $handle Commonly STDOUT or STDERR. + * @param string $str Message to write. + */ + protected function write( $handle, $str ) { + fwrite( $handle, $str ); + } + + /** + * Output one line of message to a resource. + * + * @param string $message Message to write. + * @param string $label Prefix message with a label. + * @param string $color Colorize label with a given color. + * @param resource $handle Resource to write to. Defaults to STDOUT. + */ + protected function _line( $message, $label, $color, $handle = STDOUT ) { + $label = \cli\Colors::colorize( "$color$label:%n", $this->in_color ); + $this->write( $handle, "$label $message\n" ); + } + +} diff --git a/php/EE_CLI/Loggers/Quiet.php b/php/EE_CLI/Loggers/Quiet.php new file mode 100644 index 00000000..685673ef --- /dev/null +++ b/php/EE_CLI/Loggers/Quiet.php @@ -0,0 +1,57 @@ +write( STDERR, \EE_CLI::colorize( "%RError:%n $message\n" ) ); + } + + /** + * Similar to error( $message ), but outputs $message in a red box + * + * @param array $message Message to write. + */ + public function error_multi_line( $message_lines ) { + $message = implode( "\n", $message_lines ); + + $this->write( STDERR, \EE_CLI::colorize( "%RError:%n\n$message\n" ) ); + $this->write( STDERR, \EE_CLI::colorize( "%R---------%n\n\n" ) ); + } +} diff --git a/php/EE_CLI/Loggers/Regular.php b/php/EE_CLI/Loggers/Regular.php new file mode 100644 index 00000000..615963db --- /dev/null +++ b/php/EE_CLI/Loggers/Regular.php @@ -0,0 +1,79 @@ +in_color = $in_color; + } + + /** + * Write an informational message to STDOUT. + * + * @param string $message Message to write. + */ + public function info( $message ) { + $this->write( STDOUT, $message . "\n" ); + } + + /** + * Write a success message, prefixed with "Success: ". + * + * @param string $message Message to write. + */ + public function success( $message ) { + $this->_line( $message, 'Success', '%G' ); + } + + /** + * Write a warning message to STDERR, prefixed with "Warning: ". + * + * @param string $message Message to write. + */ + public function warning( $message ) { + $this->_line( $message, 'Warning', '%C', STDERR ); + } + + /** + * Write an message to STDERR, prefixed with "Error: ". + * + * @param string $message Message to write. + */ + public function error( $message ) { + $this->_line( $message, 'Error', '%R', STDERR ); + } + + /** + * Similar to error( $message ), but outputs $message in a red box + * + * @param array $message Message to write. + */ + public function error_multi_line( $message_lines ) { + // convert tabs to four spaces, as some shells will output the tabs as variable-length + $message_lines = array_map( function( $line ) { + return str_replace( "\t", ' ', $line ); + } , $message_lines ); + + $longest = max( array_map( 'strlen', $message_lines ) ); + + // write an empty line before the message + $empty_line = \cli\Colors::colorize( '%w%1 ' . str_repeat( ' ', $longest ) . ' %n' ); + $this->write( STDERR, "\n\t$empty_line\n" ); + + foreach ( $message_lines as $line ) { + $padding = str_repeat( ' ', $longest - strlen( $line ) ); + $line = \cli\Colors::colorize( "%w%1 $line $padding%n" ); + $this->write( STDERR, "\t$line\n" ); + } + + // write an empty line after the message + $this->write( STDERR, "\t$empty_line\n\n" ); + } +} diff --git a/php/EE_CLI/Runner.php b/php/EE_CLI/Runner.php new file mode 100644 index 00000000..88fed1cd --- /dev/null +++ b/php/EE_CLI/Runner.php @@ -0,0 +1,511 @@ +$key; + } + + /** + * Register a command for early invocation, generally before WordPress loads. + * + * @param string $when Named execution hook + * @param EE_CLI\Dispatcher\Subcommand $command + */ + public function register_early_invoke( $when, $command ) { + $this->_early_invoke[ $when ][] = array_slice( Dispatcher\get_path( $command ), 1 ); + } + + /** + * Perform the early invocation of a command. + * + * @param string $when Named execution hook + */ + private function do_early_invoke( $when ) { + if ( !isset( $this->_early_invoke[ $when ] ) ) + return; + + foreach ( $this->_early_invoke[ $when ] as $path ) { + if ( $this->cmd_starts_with( $path ) ) { + $this->_run_command(); + exit; + } + } + } + + /** + * Get the path to the global configuration YAML file. + * + * @return string|false + */ + private function get_global_config_path() { + + if ( isset( $runtime_config['config'] ) ) { + $config_path = $runtime_config['config']; + $this->_global_config_path_debug = 'Using global config from config runtime arg: ' . $config_path; + } else if ( getenv( 'EE_CLI_CONFIG_PATH' ) ) { + $config_path = getenv( 'EE_CLI_CONFIG_PATH' ); + $this->_global_config_path_debug = 'Using global config from EE_CLI_CONFIG_PATH env var: ' . $config_path; + } else { + $config_path = getenv( 'HOME' ) . '/.wp-cli/config.yml'; + $this->_global_config_path_debug = 'Using default global config: ' . $config_path; + } + + if ( is_readable( $config_path ) ) { + return $config_path; + } else { + $this->_global_config_path_debug = 'No readable global config found'; + return false; + } + } + + /** + * Get the path to the project-specific configuration + * YAML file. + * wp-cli.local.yml takes priority over wp-cli.yml. + * + * @return string|false + */ + private function get_project_config_path() { + $config_files = array( + 'wp-cli.local.yml', + 'wp-cli.yml' + ); + + // Stop looking upward when we find we have emerged from a subdirectory + // install into a parent install + $project_config_path = Utils\find_file_upward( $config_files, getcwd(), function ( $dir ) { + static $wp_load_count = 0; + $wp_load_path = $dir . DIRECTORY_SEPARATOR . 'wp-load.php'; + if ( file_exists( $wp_load_path ) ) { + $wp_load_count += 1; + } + return $wp_load_count > 1; + } ); + if ( ! empty( $project_config_path ) ) { + $this->_project_config_path_debug = 'Using project config: ' . $project_config_path; + } else { + $this->_project_config_path_debug = 'No project config found'; + } + return $project_config_path; + } + + /** + * Attempts to find the path to the WP install inside index.php + * + * @param string $index_path + * @return string|false + */ + private static function extract_subdir_path( $index_path ) { + $index_code = file_get_contents( $index_path ); + + if ( !preg_match( '|^\s*require\s*\(?\s*(.+?)/wp-blog-header\.php([\'"])|m', $index_code, $matches ) ) { + return false; + } + + $wp_path_src = $matches[1] . $matches[2]; + $wp_path_src = Utils\replace_path_consts( $wp_path_src, $index_path ); + $wp_path = eval( "return $wp_path_src;" ); + + if ( !Utils\is_path_absolute( $wp_path ) ) { + $wp_path = dirname( $index_path ) . "/$wp_path"; + } + + return $wp_path; + } + + + /** + * Guess which URL context WP-CLI has been invoked under. + * + * @param array $assoc_args + * @return string|false + */ + private static function guess_url( $assoc_args ) { + if ( isset( $assoc_args['blog'] ) ) { + $assoc_args['url'] = $assoc_args['blog']; + } + + if ( isset( $assoc_args['url'] ) ) { + $url = $assoc_args['url']; + if ( true === $url ) { + EE_CLI::warning( 'The --url parameter expects a value.' ); + } + } + + if ( isset( $url ) ) { + return $url; + } + + return false; + } + + private function cmd_starts_with( $prefix ) { + return $prefix == array_slice( $this->arguments, 0, count( $prefix ) ); + } + + /** + * Given positional arguments, find the command to execute. + * + * @param array $args + * @return array|string Command, args, and path on success; error message on failure + */ + public function find_command_to_run( $args ) { + $command = \EE_CLI::get_root_command(); + + $cmd_path = array(); + + while ( !empty( $args ) && $command->can_have_subcommands() ) { + $cmd_path[] = $args[0]; + $full_name = implode( ' ', $cmd_path ); + + $subcommand = $command->find_subcommand( $args ); + + if ( !$subcommand ) { + if ( count( $cmd_path ) > 1 ) { + $child = array_pop( $cmd_path ); + $parent_name = implode( ' ', $cmd_path ); + return sprintf( + "'%s' is not a registered subcommand of '%s'. See 'wp help %s'.", + $child, + $parent_name, + $parent_name + ); + } else { + return sprintf( + "'%s' is not a registered wp command. See 'wp help'.", + $full_name + ); + } + } + + /* if ( $this->is_command_disabled( $subcommand ) ) { + return sprintf( + "The '%s' command has been disabled from the config file.", + $full_name + ); + } + */ + + $command = $subcommand; + } + + return array( $command, $args, $cmd_path ); + } + + /** + * Find the WP-CLI command to run given arguments, + * and invoke it. + * + * @param array $args Positional arguments including command name + * @param array $assoc_args + */ + public function run_command( $args, $assoc_args = array() ) { + $r = $this->find_command_to_run( $args ); + if ( is_string( $r ) ) { + EE_CLI::error( $r ); + } + + list( $command, $final_args, $cmd_path ) = $r; + + $name = implode( ' ', $cmd_path ); + + if ( isset( $this->extra_config[ $name ] ) ) { + $extra_args = $this->extra_config[ $name ]; + } else { + $extra_args = array(); + } + + EE_CLI::debug( 'Running command: ' . $name ); + try { + $command->invoke( $final_args, $assoc_args, $extra_args ); + } catch ( EE_CLI\Iterators\Exception $e ) { + EE_CLI::error( $e->getMessage() ); + } + } + + private function _run_command() { + $this->run_command( $this->arguments, $this->assoc_args ); + } + + /** + * Check whether a given command is disabled by the config + * + * @return bool + */ + /*public function is_command_disabled( $command ) { + $path = implode( ' ', array_slice( \EE_CLI\Dispatcher\get_path( $command ), 1 ) ); + return in_array( $path, $this->config['disabled_commands'] ); + }*/ + + /** + * Returns wp-config.php code, skipping the loading of wp-settings.php + * + * @return string + */ + + + public function get_wp_config_code() { + $wp_config_path = Utils\locate_wp_config(); + + // $wp_config_code = explode( "\n", file_get_contents( $wp_config_path ) ); + + $found_wp_settings = false; + + // $lines_to_run = array(); + + // foreach ( $wp_config_code as $line ) { + // if ( preg_match( '/^\s*require.+wp-settings\.php/', $line ) ) { + // $found_wp_settings = true; + // continue; + // } + + $lines_to_run[] = $line; + } + + // if ( !$found_wp_settings ) { + // EE_CLI::error( 'Strange wp-config.php file: wp-settings.php is not loaded directly.' ); + // } + + // $source = implode( "\n", $lines_to_run ); + // $source = Utils\replace_path_consts( $source, $wp_config_path ); + // return preg_replace( '|^\s*\<\?php\s*|', '', $source ); + //} + + /** + * Transparently convert deprecated syntaxes + * + * @param array $args + * @param array $assoc_args + * @return array + */ + private static function back_compat_conversions( $args, $assoc_args ) { + $top_level_aliases = array( + 'sql' => 'db', + 'blog' => 'site' + ); + if ( count( $args ) > 0 ) { + foreach ( $top_level_aliases as $old => $new ) { + if ( $old == $args[0] ) { + $args[0] = $new; + break; + } + } + } + + // --json -> --format=json + if ( isset( $assoc_args['json'] ) ) { + $assoc_args['format'] = 'json'; + unset( $assoc_args['json'] ); + } + + // --{version|info} -> cli {version|info} + if ( empty( $args ) ) { + $special_flags = array( 'version', 'info' ); + foreach ( $special_flags as $key ) { + if ( isset( $assoc_args[ $key ] ) ) { + $args = array( 'cli', $key ); + unset( $assoc_args[ $key ] ); + break; + } + } + } + + return array( $args, $assoc_args ); + } + + /** + * Whether or not the output should be rendered in color + * + * @return bool + */ + public function in_color() { + return $this->colorize; + } + + private function init_colorization() { + if ( 'auto' === $this->config['color'] ) { + $this->colorize = ( !\cli\Shell::isPiped() && !\EE_CLI\Utils\is_windows() ); + } else { + $this->colorize = $this->config['color']; + } + } + + private function init_logger() { + if ( $this->config['quiet'] ) + $logger = new \EE_CLI\Loggers\Quiet; + else + $logger = new \EE_CLI\Loggers\Regular( $this->in_color() ); + + EE_CLI::set_logger( $logger ); + } + + + private function init_config() { + $configurator = \EE_CLI::get_configurator(); + + // File config + { + $this->global_config_path = $this->get_global_config_path(); + $this->project_config_path = $this->get_project_config_path(); + + $configurator->merge_yml( $this->global_config_path ); + $configurator->merge_yml( $this->project_config_path ); + } + + // Runtime config and args + { + list( $args, $assoc_args, $runtime_config ) = $configurator->parse_args( + array_slice( $GLOBALS['argv'], 1 ) ); + + list( $this->arguments, $this->assoc_args ) = self::back_compat_conversions( + $args, $assoc_args ); + + $configurator->merge_array( $runtime_config ); + } + + list( $this->config, $this->extra_config ) = $configurator->to_array(); + } + + public function start() { + $this->init_config(); + $this->init_colorization(); + $this->init_logger(); + + EE_CLI::debug( $this->_global_config_path_debug ); + EE_CLI::debug( $this->_project_config_path_debug ); + + // $this->check_root(); + + if ( empty( $this->arguments ) ) + $this->arguments[] = 'help'; + + // Protect 'cli info' from most of the runtime + if ( 'cli' === $this->arguments[0] && ! empty( $this->arguments[1] ) && 'info' === $this->arguments[1] ) { + $this->_run_command(); + exit; + } + + // Load bundled commands early, so that they're forced to use the same + // APIs as non-bundled commands. + Utils\load_command( $this->arguments[0] ); + + if ( isset( $this->config['require'] ) ) { + foreach ( $this->config['require'] as $path ) { + if ( ! file_exists( $path ) ) { + EE_CLI::error( sprintf( "Required file '%s' doesn't exist", basename( $path ) ) ); + } + Utils\load_file( $path ); + EE_CLI::debug( 'Required file from config: ' . $path ); + } + } + + // Show synopsis if it's a composite command. + $r = $this->find_command_to_run( $this->arguments ); + if ( is_array( $r ) ) { + list( $command ) = $r; + + if ( $command->can_have_subcommands() ) { + $command->show_usage(); + exit; + } + } + + // First try at showing man page + if ( 'help' === $this->arguments[0] && ! $this->wp_exists() ) { + $this->_run_command(); + } + + // Handle --url parameter + $url = self::guess_url( $this->config ); + if ( $url ) + \EE_CLI::set_url( $url ); + + $this->do_early_invoke( 'before_wp_load' ); + + //$this->check_wp_version(); + + if ( $this->cmd_starts_with( array( 'core', 'config' ) ) ) { + $this->_run_command(); + exit; + } + + //if ( !Utils\locate_wp_config() ) { + // EE_CLI::error( + // "wp-config.php not found.\n" . + // "Either create one manually or use `wp core config`." ); + //} + + if ( $this->cmd_starts_with( array( 'db' ) ) && !$this->cmd_starts_with( array( 'db', 'tables' ) ) ) { + eval( $this->get_wp_config_code() ); + $this->_run_command(); + exit; + } + + if ( $this->cmd_starts_with( array( 'core', 'is-installed' ) ) ) { + define( 'WP_INSTALLING', true ); + } + + if ( + count( $this->arguments ) >= 2 && + $this->arguments[0] == 'core' && + in_array( $this->arguments[1], array( 'install', 'multisite-install' ) ) + ) { + define( 'WP_INSTALLING', true ); + + // We really need a URL here + if ( !isset( $_SERVER['HTTP_HOST'] ) ) { + $url = 'http://example.com'; + \EE_CLI::set_url( $url ); + } + + if ( 'multisite-install' == $this->arguments[1] ) { + // need to fake some globals to skip the checks in wp-includes/ms-settings.php + $url_parts = Utils\parse_url( $url ); + self::fake_current_site_blog( $url_parts ); + + if ( !defined( 'COOKIEHASH' ) ) { + define( 'COOKIEHASH', md5( $url_parts['host'] ) ); + } + } + } + + if ( $this->cmd_starts_with( array( 'import') ) ) { + define( 'WP_LOAD_IMPORTERS', true ); + define( 'WP_IMPORTING', true ); + } + + if ( $this->cmd_starts_with( array( 'plugin' ) ) ) { + $GLOBALS['pagenow'] = 'plugins.php'; + } + + $this->_run_command(); + + } +} diff --git a/php/EE_CLI/SynopsisParser.php b/php/EE_CLI/SynopsisParser.php new file mode 100644 index 00000000..a9098fad --- /dev/null +++ b/php/EE_CLI/SynopsisParser.php @@ -0,0 +1,110 @@ +..." + * into [ optional=>false, type=>positional, repeating=>true, name=>object-id ] + */ +class SynopsisParser { + + /** + * @param string A synopsis + * @return array List of parameters + */ + public static function parse( $synopsis ) { + $tokens = array_filter( preg_split( '/[\s\t]+/', $synopsis ) ); + + $params = array(); + foreach ( $tokens as $token ) { + $param = self::classify_token( $token ); + + // Some types of parameters shouldn't be mandatory + if ( isset( $param['optional'] ) && !$param['optional'] ) { + if ( 'flag' === $param['type'] || ( 'assoc' === $param['type'] && $param['value']['optional'] ) ) { + $param['type'] = 'unknown'; + } + } + + $param['token'] = $token; + $params[] = $param; + } + + return $params; + } + + /** + * Classify argument attributes based on its syntax. + * + * @param string $token + * @return array $param + */ + private static function classify_token( $token ) { + $param = array(); + + list( $param['optional'], $token ) = self::is_optional( $token ); + list( $param['repeating'], $token ) = self::is_repeating( $token ); + + $p_name = '([a-z-_]+)'; + $p_value = '([a-zA-Z-_|,]+)'; + + if ( '--=' === $token ) { + $param['type'] = 'generic'; + } elseif ( preg_match( "/^<($p_value)>$/", $token, $matches ) ) { + $param['type'] = 'positional'; + $param['name'] = $matches[1]; + } elseif ( preg_match( "/^--(?:\\[no-\\])?$p_name/", $token, $matches ) ) { + $param['name'] = $matches[1]; + + $value = substr( $token, strlen( $matches[0] ) ); + + // substr returns false <= PHP 5.6, and '' PHP 7+ + if ( false === $value || '' === $value ) { + $param['type'] = 'flag'; + } else { + $param['type'] = 'assoc'; + + list( $param['value']['optional'], $value ) = self::is_optional( $value ); + + if ( preg_match( "/^=<$p_value>$/", $value, $matches ) ) { + $param['value']['name'] = $matches[1]; + } else { + $param = array( 'type' => 'unknown' ); + } + } + } else { + $param['type'] = 'unknown'; + } + + return $param; + } + + /** + * An optional parameter is surrounded by square brackets. + * + * @param string $token + * @return array + */ + private static function is_optional( $token ) { + if ( '[' == substr( $token, 0, 1 ) && ']' == substr( $token, -1 ) ) { + return array( true, substr( $token, 1, -1 ) ); + } else { + return array( false, $token ); + } + } + + /** + * A repeating parameter is followed by an ellipsis. + * + * @param string $token + * @return array + */ + private static function is_repeating( $token ) { + if ( '...' === substr( $token, -3 ) ) { + return array( true, substr( $token, 0, -3 ) ); + } else { + return array( false, $token ); + } + } +} diff --git a/php/EE_CLI/SynopsisValidator.php b/php/EE_CLI/SynopsisValidator.php new file mode 100644 index 00000000..dbaee5a8 --- /dev/null +++ b/php/EE_CLI/SynopsisValidator.php @@ -0,0 +1,163 @@ +spec = SynopsisParser::parse( $synopsis ); + } + + /** + * Get any unknown arugments. + * + * @return array + */ + public function get_unknown() { + return array_column( $this->query_spec( array( + 'type' => 'unknown', + ) ), 'token' ); + } + + /** + * Check whether there are enough positional arguments. + * + * @param array $args Positional arguments. + * @return bool + */ + public function enough_positionals( $args ) { + $positional = $this->query_spec( array( + 'type' => 'positional', + 'optional' => false, + ) ); + + return count( $args ) >= count( $positional ); + } + + /** + * Check for any unknown positionals. + * + * @param array $args Positional arguments. + * @return array + */ + public function unknown_positionals( $args ) { + $positional_repeating = $this->query_spec( array( + 'type' => 'positional', + 'repeating' => true, + ) ); + + // At least one positional supports as many as possible. + if ( !empty( $positional_repeating ) ) + return array(); + + $positional = $this->query_spec( array( + 'type' => 'positional', + 'repeating' => false, + ) ); + + return array_slice( $args, count( $positional ) ); + } + + /** + * Check that all required keys are present and that they have values. + * + * @param array $assoc_args Parameters passed to command. + * @return array + */ + public function validate_assoc( $assoc_args ) { + $assoc_spec = $this->query_spec( array( + 'type' => 'assoc', + ) ); + + $errors = array( + 'fatal' => array(), + 'warning' => array() + ); + + $to_unset = array(); + + foreach ( $assoc_spec as $param ) { + $key = $param['name']; + + if ( !isset( $assoc_args[ $key ] ) ) { + if ( !$param['optional'] ) { + $errors['fatal'][$key] = "missing --$key parameter"; + } + } else { + if ( true === $assoc_args[ $key ] && !$param['value']['optional'] ) { + $error_type = ( !$param['optional'] ) ? 'fatal' : 'warning'; + $errors[ $error_type ][$key] = "--$key parameter needs a value"; + + $to_unset[] = $key; + } + } + } + + return array( $errors, $to_unset ); + } + + /** + * Check whether there are unknown parameters supplied. + * + * @param array $assoc_args Parameters passed to command. + * @return array|false + */ + public function unknown_assoc( $assoc_args ) { + $generic = $this->query_spec( array( + 'type' => 'generic', + ) ); + + if ( count( $generic ) ) + return array(); + + $known_assoc = array(); + + foreach ( $this->spec as $param ) { + if ( in_array( $param['type'], array( 'assoc', 'flag' ) ) ) + $known_assoc[] = $param['name']; + } + + return array_diff( array_keys( $assoc_args ), $known_assoc ); + } + + /** + * Filters a list of associative arrays, based on a set of key => value arguments. + * + * @param array $args An array of key => value arguments to match against + * @param string $operator + * @return array + */ + private function query_spec( $args, $operator = 'AND' ) { + $operator = strtoupper( $operator ); + $count = count( $args ); + $filtered = array(); + + foreach ( $this->spec as $key => $to_match ) { + $matched = 0; + foreach ( $args as $m_key => $m_value ) { + if ( array_key_exists( $m_key, $to_match ) && $m_value == $to_match[ $m_key ] ) + $matched++; + } + + if ( ( 'AND' == $operator && $matched == $count ) + || ( 'OR' == $operator && $matched > 0 ) + || ( 'NOT' == $operator && 0 == $matched ) ) { + $filtered[$key] = $to_match; + } + } + + return $filtered; + } + +} diff --git a/php/Spyc.php b/php/Spyc.php new file mode 100644 index 00000000..7a0c2828 --- /dev/null +++ b/php/Spyc.php @@ -0,0 +1,1035 @@ + + * @author Chris Wanstrath + * @link http://code.google.com/p/spyc/ + * @copyright Copyright 2005-2006 Chris Wanstrath, 2006-2011 Vlad Andersen + * @license http://www.opensource.org/licenses/mit-license.php MIT License + * @package Spyc + */ + +if (!function_exists('spyc_load')) : + /** + * Parses YAML to array. + * @param string $string YAML string. + * @return array + */ + function spyc_load ($string) { + return Spyc::YAMLLoadString($string); + } +endif; + +if (!function_exists('spyc_load_file')) : + /** + * Parses YAML to array. + * @param string $file Path to YAML file. + * @return array + */ + function spyc_load_file ($file) { + return Spyc::YAMLLoad($file); + } +endif; + +if ( ! class_exists( 'Spyc' ) ) : +/** + * The Simple PHP YAML Class. + * + * This class can be used to read a YAML file and convert its contents + * into a PHP array. It currently supports a very limited subsection of + * the YAML spec. + * + * Usage: + * + * $Spyc = new Spyc; + * $array = $Spyc->load($file); + * + * or: + * + * $array = Spyc::YAMLLoad($file); + * + * or: + * + * $array = spyc_load_file($file); + * + * @package Spyc + */ +class Spyc { + + // SETTINGS + + const REMPTY = "\0\0\0\0\0"; + + /** + * Setting this to true will force YAMLDump to enclose any string value in + * quotes. False by default. + * + * @var bool + */ + public $setting_dump_force_quotes = false; + + /** + * Setting this to true will forse YAMLLoad to use syck_load function when + * possible. False by default. + * @var bool + */ + public $setting_use_syck_is_possible = false; + + + + /**#@+ + * @access private + * @var mixed + */ + private $_dumpIndent; + private $_dumpWordWrap; + private $_containsGroupAnchor = false; + private $_containsGroupAlias = false; + private $path; + private $result; + private $LiteralPlaceHolder = '___YAML_Literal_Block___'; + private $SavedGroups = array(); + private $indent; + /** + * Path modifier that should be applied after adding current element. + * @var array + */ + private $delayedPath = array(); + + /**#@+ + * @access public + * @var mixed + */ + public $_nodeId; + +/** + * Load a valid YAML string to Spyc. + * @param string $input + * @return array + */ + public function load ($input) { + return $this->__loadString($input); + } + + /** + * Load a valid YAML file to Spyc. + * @param string $file + * @return array + */ + public function loadFile ($file) { + return $this->__load($file); + } + + /** + * Load YAML into a PHP array statically + * + * The load method, when supplied with a YAML stream (string or file), + * will do its best to convert YAML in a file into a PHP array. Pretty + * simple. + * Usage: + * + * $array = Spyc::YAMLLoad('lucky.yaml'); + * print_r($array); + * + * @access public + * @return array + * @param string $input Path of YAML file or string containing YAML + */ + public static function YAMLLoad($input) { + $Spyc = new Spyc; + return $Spyc->__load($input); + } + + /** + * Load a string of YAML into a PHP array statically + * + * The load method, when supplied with a YAML string, will do its best + * to convert YAML in a string into a PHP array. Pretty simple. + * + * Note: use this function if you don't want files from the file system + * loaded and processed as YAML. This is of interest to people concerned + * about security whose input is from a string. + * + * Usage: + * + * $array = Spyc::YAMLLoadString("---\n0: hello world\n"); + * print_r($array); + * + * @access public + * @return array + * @param string $input String containing YAML + */ + public static function YAMLLoadString($input) { + $Spyc = new Spyc; + return $Spyc->__loadString($input); + } + + /** + * Dump YAML from PHP array statically + * + * The dump method, when supplied with an array, will do its best + * to convert the array into friendly YAML. Pretty simple. Feel free to + * save the returned string as nothing.yaml and pass it around. + * + * Oh, and you can decide how big the indent is and what the wordwrap + * for folding is. Pretty cool -- just pass in 'false' for either if + * you want to use the default. + * + * Indent's default is 2 spaces, wordwrap's default is 40 characters. And + * you can turn off wordwrap by passing in 0. + * + * @access public + * @return string + * @param array $array PHP array + * @param int $indent Pass in false to use the default, which is 2 + * @param int $wordwrap Pass in 0 for no wordwrap, false for default (40) + */ + public static function YAMLDump($array,$indent = false,$wordwrap = false) { + $spyc = new Spyc; + return $spyc->dump($array,$indent,$wordwrap); + } + + + /** + * Dump PHP array to YAML + * + * The dump method, when supplied with an array, will do its best + * to convert the array into friendly YAML. Pretty simple. Feel free to + * save the returned string as tasteful.yaml and pass it around. + * + * Oh, and you can decide how big the indent is and what the wordwrap + * for folding is. Pretty cool -- just pass in 'false' for either if + * you want to use the default. + * + * Indent's default is 2 spaces, wordwrap's default is 40 characters. And + * you can turn off wordwrap by passing in 0. + * + * @access public + * @return string + * @param array $array PHP array + * @param int $indent Pass in false to use the default, which is 2 + * @param int $wordwrap Pass in 0 for no wordwrap, false for default (40) + */ + public function dump($array,$indent = false,$wordwrap = false) { + // Dumps to some very clean YAML. We'll have to add some more features + // and options soon. And better support for folding. + + // New features and options. + if ($indent === false or !is_numeric($indent)) { + $this->_dumpIndent = 2; + } else { + $this->_dumpIndent = $indent; + } + + if ($wordwrap === false or !is_numeric($wordwrap)) { + $this->_dumpWordWrap = 40; + } else { + $this->_dumpWordWrap = $wordwrap; + } + + // New YAML document + $string = "---\n"; + + // Start at the base of the array and move through it. + if ($array) { + $array = (array)$array; + $previous_key = -1; + foreach ($array as $key => $value) { + if (!isset($first_key)) $first_key = $key; + $string .= $this->_yamlize($key,$value,0,$previous_key, $first_key, $array); + $previous_key = $key; + } + } + return $string; + } + + /** + * Attempts to convert a key / value array item to YAML + * @access private + * @return string + * @param $key The name of the key + * @param $value The value of the item + * @param $indent The indent of the current node + */ + private function _yamlize($key,$value,$indent, $previous_key = -1, $first_key = 0, $source_array = null) { + if (is_array($value)) { + if (empty ($value)) + return $this->_dumpNode($key, array(), $indent, $previous_key, $first_key, $source_array); + // It has children. What to do? + // Make it the right kind of item + $string = $this->_dumpNode($key, self::REMPTY, $indent, $previous_key, $first_key, $source_array); + // Add the indent + $indent += $this->_dumpIndent; + // Yamlize the array + $string .= $this->_yamlizeArray($value,$indent); + } elseif (!is_array($value)) { + // It doesn't have children. Yip. + $string = $this->_dumpNode($key, $value, $indent, $previous_key, $first_key, $source_array); + } + return $string; + } + + /** + * Attempts to convert an array to YAML + * @access private + * @return string + * @param $array The array you want to convert + * @param $indent The indent of the current level + */ + private function _yamlizeArray($array,$indent) { + if (is_array($array)) { + $string = ''; + $previous_key = -1; + foreach ($array as $key => $value) { + if (!isset($first_key)) $first_key = $key; + $string .= $this->_yamlize($key, $value, $indent, $previous_key, $first_key, $array); + $previous_key = $key; + } + return $string; + } else { + return false; + } + } + + /** + * Returns YAML from a key and a value + * @access private + * @return string + * @param $key The name of the key + * @param $value The value of the item + * @param $indent The indent of the current node + */ + private function _dumpNode($key, $value, $indent, $previous_key = -1, $first_key = 0, $source_array = null) { + // do some folding here, for blocks + if (is_string ($value) && ((strpos($value,"\n") !== false || strpos($value,": ") !== false || strpos($value,"- ") !== false || + strpos($value,"*") !== false || strpos($value,"#") !== false || strpos($value,"<") !== false || strpos($value,">") !== false || strpos ($value, ' ') !== false || + strpos($value,"[") !== false || strpos($value,"]") !== false || strpos($value,"{") !== false || strpos($value,"}") !== false) || strpos($value,"&") !== false || strpos($value, "'") !== false || strpos($value, "!") === 0 || + substr ($value, -1, 1) == ':') + ) { + $value = $this->_doLiteralBlock($value,$indent); + } else { + $value = $this->_doFolding($value,$indent); + } + + if ($value === array()) $value = '[ ]'; + if (in_array ($value, array ('true', 'TRUE', 'false', 'FALSE', 'y', 'Y', 'n', 'N', 'null', 'NULL'), true)) { + $value = $this->_doLiteralBlock($value,$indent); + } + if (trim ($value) != $value) + $value = $this->_doLiteralBlock($value,$indent); + + if (is_bool($value)) { + $value = ($value) ? "true" : "false"; + } + + if ($value === null) $value = 'null'; + if ($value === "'" . self::REMPTY . "'") $value = null; + + $spaces = str_repeat(' ',$indent); + + //if (is_int($key) && $key - 1 == $previous_key && $first_key===0) { + if (is_array ($source_array) && array_keys($source_array) === range(0, count($source_array) - 1)) { + // It's a sequence + $string = $spaces.'- '.$value."\n"; + } else { + // if ($first_key===0) throw new Exception('Keys are all screwy. The first one was zero, now it\'s "'. $key .'"'); + // It's mapped + if (strpos($key, ":") !== false || strpos($key, "#") !== false) { $key = '"' . $key . '"'; } + $string = rtrim ($spaces.$key.': '.$value)."\n"; + } + return $string; + } + + /** + * Creates a literal block for dumping + * @access private + * @return string + * @param $value + * @param $indent int The value of the indent + */ + private function _doLiteralBlock($value,$indent) { + if ($value === "\n") return '\n'; + if (strpos($value, "\n") === false && strpos($value, "'") === false) { + return sprintf ("'%s'", $value); + } + if (strpos($value, "\n") === false && strpos($value, '"') === false) { + return sprintf ('"%s"', $value); + } + $exploded = explode("\n",$value); + $newValue = '|'; + $indent += $this->_dumpIndent; + $spaces = str_repeat(' ',$indent); + foreach ($exploded as $line) { + $newValue .= "\n" . $spaces . ($line); + } + return $newValue; + } + + /** + * Folds a string of text, if necessary + * @access private + * @return string + * @param $value The string you wish to fold + */ + private function _doFolding($value,$indent) { + // Don't do anything if wordwrap is set to 0 + + if ($this->_dumpWordWrap !== 0 && is_string ($value) && strlen($value) > $this->_dumpWordWrap) { + $indent += $this->_dumpIndent; + $indent = str_repeat(' ',$indent); + $wrapped = wordwrap($value,$this->_dumpWordWrap,"\n$indent"); + $value = ">\n".$indent.$wrapped; + } else { + if ($this->setting_dump_force_quotes && is_string ($value) && $value !== self::REMPTY) + $value = '"' . $value . '"'; + } + + + return $value; + } + +// LOADING FUNCTIONS + + private function __load($input) { + $Source = $this->loadFromSource($input); + return $this->loadWithSource($Source); + } + + private function __loadString($input) { + $Source = $this->loadFromString($input); + return $this->loadWithSource($Source); + } + + private function loadWithSource($Source) { + if (empty ($Source)) return array(); + if ($this->setting_use_syck_is_possible && function_exists ('syck_load')) { + $array = syck_load (implode ('', $Source)); + return is_array($array) ? $array : array(); + } + + $this->path = array(); + $this->result = array(); + + $cnt = count($Source); + for ($i = 0; $i < $cnt; $i++) { + $line = $Source[$i]; + + $this->indent = strlen($line) - strlen(ltrim($line)); + $tempPath = $this->getParentPathByIndent($this->indent); + $line = self::stripIndent($line, $this->indent); + if (self::isComment($line)) continue; + if (self::isEmpty($line)) continue; + $this->path = $tempPath; + + $literalBlockStyle = self::startsLiteralBlock($line); + if ($literalBlockStyle) { + $line = rtrim ($line, $literalBlockStyle . " \n"); + $literalBlock = ''; + $line .= $this->LiteralPlaceHolder; + $literal_block_indent = strlen($Source[$i+1]) - strlen(ltrim($Source[$i+1])); + while (++$i < $cnt && $this->literalBlockContinues($Source[$i], $this->indent)) { + $literalBlock = $this->addLiteralLine($literalBlock, $Source[$i], $literalBlockStyle, $literal_block_indent); + } + $i--; + } + + while (++$i < $cnt && self::greedilyNeedNextLine($line)) { + $line = rtrim ($line, " \n\t\r") . ' ' . ltrim ($Source[$i], " \t"); + } + $i--; + + + + if (strpos ($line, '#')) { + if (strpos ($line, '"') === false && strpos ($line, "'") === false) + $line = preg_replace('/\s+#(.+)$/','',$line); + } + + $lineArray = $this->_parseLine($line); + + if ($literalBlockStyle) + $lineArray = $this->revertLiteralPlaceHolder ($lineArray, $literalBlock); + + $this->addArray($lineArray, $this->indent); + + foreach ($this->delayedPath as $indent => $delayedPath) + $this->path[$indent] = $delayedPath; + + $this->delayedPath = array(); + + } + return $this->result; + } + + private function loadFromSource ($input) { + if (!empty($input) && strpos($input, "\n") === false && file_exists($input)) + return file($input); + + return $this->loadFromString($input); + } + + private function loadFromString ($input) { + $lines = explode("\n",$input); + foreach ($lines as $k => $_) { + $lines[$k] = rtrim ($_, "\r"); + } + return $lines; + } + + /** + * Parses YAML code and returns an array for a node + * @access private + * @return array + * @param string $line A line from the YAML file + */ + private function _parseLine($line) { + if (!$line) return array(); + $line = trim($line); + if (!$line) return array(); + + $array = array(); + + $group = $this->nodeContainsGroup($line); + if ($group) { + $this->addGroup($line, $group); + $line = $this->stripGroup ($line, $group); + } + + if ($this->startsMappedSequence($line)) + return $this->returnMappedSequence($line); + + if ($this->startsMappedValue($line)) + return $this->returnMappedValue($line); + + if ($this->isArrayElement($line)) + return $this->returnArrayElement($line); + + if ($this->isPlainArray($line)) + return $this->returnPlainArray($line); + + + return $this->returnKeyValuePair($line); + + } + + /** + * Finds the type of the passed value, returns the value as the new type. + * @access private + * @param string $value + * @return mixed + */ + private function _toType($value) { + if ($value === '') return null; + $first_character = $value[0]; + $last_character = substr($value, -1, 1); + + $is_quoted = false; + do { + if (!$value) break; + if ($first_character != '"' && $first_character != "'") break; + if ($last_character != '"' && $last_character != "'") break; + $is_quoted = true; + } while (0); + + if ($is_quoted) + return strtr(substr ($value, 1, -1), array ('\\"' => '"', '\'\'' => '\'', '\\\'' => '\'')); + + if (strpos($value, ' #') !== false && !$is_quoted) + $value = preg_replace('/\s+#(.+)$/','',$value); + + if (!$is_quoted) $value = str_replace('\n', "\n", $value); + + if ($first_character == '[' && $last_character == ']') { + // Take out strings sequences and mappings + $innerValue = trim(substr ($value, 1, -1)); + if ($innerValue === '') return array(); + $explode = $this->_inlineEscape($innerValue); + // Propagate value array + $value = array(); + foreach ($explode as $v) { + $value[] = $this->_toType($v); + } + return $value; + } + + if (strpos($value,': ')!==false && $first_character != '{') { + $array = explode(': ',$value); + $key = trim($array[0]); + array_shift($array); + $value = trim(implode(': ',$array)); + $value = $this->_toType($value); + return array($key => $value); + } + + if ($first_character == '{' && $last_character == '}') { + $innerValue = trim(substr ($value, 1, -1)); + if ($innerValue === '') return array(); + // Inline Mapping + // Take out strings sequences and mappings + $explode = $this->_inlineEscape($innerValue); + // Propagate value array + $array = array(); + foreach ($explode as $v) { + $SubArr = $this->_toType($v); + if (empty($SubArr)) continue; + if (is_array ($SubArr)) { + $array[key($SubArr)] = $SubArr[key($SubArr)]; continue; + } + $array[] = $SubArr; + } + return $array; + } + + if ($value == 'null' || $value == 'NULL' || $value == 'Null' || $value == '' || $value == '~') { + return null; + } + + if ( is_numeric($value) && preg_match ('/^(-|)[1-9]+[0-9]*$/', $value) ){ + $intvalue = (int)$value; + if ($intvalue != PHP_INT_MAX) + $value = $intvalue; + return $value; + } + + if (in_array($value, + array('true', 'on', '+', 'yes', 'y', 'True', 'TRUE', 'On', 'ON', 'YES', 'Yes', 'Y'))) { + return true; + } + + if (in_array(strtolower($value), + array('false', 'off', '-', 'no', 'n'))) { + return false; + } + + if (is_numeric($value)) { + if ($value === '0') return 0; + if (rtrim ($value, 0) === $value) + $value = (float)$value; + return $value; + } + + return $value; + } + + /** + * Used in inlines to check for more inlines or quoted strings + * @access private + * @return array + */ + private function _inlineEscape($inline) { + // There's gotta be a cleaner way to do this... + // While pure sequences seem to be nesting just fine, + // pure mappings and mappings with sequences inside can't go very + // deep. This needs to be fixed. + + $seqs = array(); + $maps = array(); + $saved_strings = array(); + + // Check for strings + $regex = '/(?:(")|(?:\'))((?(1)[^"]+|[^\']+))(?(1)"|\')/'; + if (preg_match_all($regex,$inline,$strings)) { + $saved_strings = $strings[0]; + $inline = preg_replace($regex,'YAMLString',$inline); + } + unset($regex); + + $i = 0; + do { + + // Check for sequences + while (preg_match('/\[([^{}\[\]]+)\]/U',$inline,$matchseqs)) { + $seqs[] = $matchseqs[0]; + $inline = preg_replace('/\[([^{}\[\]]+)\]/U', ('YAMLSeq' . (count($seqs) - 1) . 's'), $inline, 1); + } + + // Check for mappings + while (preg_match('/{([^\[\]{}]+)}/U',$inline,$matchmaps)) { + $maps[] = $matchmaps[0]; + $inline = preg_replace('/{([^\[\]{}]+)}/U', ('YAMLMap' . (count($maps) - 1) . 's'), $inline, 1); + } + + if ($i++ >= 10) break; + + } while (strpos ($inline, '[') !== false || strpos ($inline, '{') !== false); + + $explode = explode(', ',$inline); + $stringi = 0; $i = 0; + + while (1) { + + // Re-add the sequences + if (!empty($seqs)) { + foreach ($explode as $key => $value) { + if (strpos($value,'YAMLSeq') !== false) { + foreach ($seqs as $seqk => $seq) { + $explode[$key] = str_replace(('YAMLSeq'.$seqk.'s'),$seq,$value); + $value = $explode[$key]; + } + } + } + } + + // Re-add the mappings + if (!empty($maps)) { + foreach ($explode as $key => $value) { + if (strpos($value,'YAMLMap') !== false) { + foreach ($maps as $mapk => $map) { + $explode[$key] = str_replace(('YAMLMap'.$mapk.'s'), $map, $value); + $value = $explode[$key]; + } + } + } + } + + + // Re-add the strings + if (!empty($saved_strings)) { + foreach ($explode as $key => $value) { + while (strpos($value,'YAMLString') !== false) { + $explode[$key] = preg_replace('/YAMLString/',$saved_strings[$stringi],$value, 1); + unset($saved_strings[$stringi]); + ++$stringi; + $value = $explode[$key]; + } + } + } + + $finished = true; + foreach ($explode as $key => $value) { + if (strpos($value,'YAMLSeq') !== false) { + $finished = false; break; + } + if (strpos($value,'YAMLMap') !== false) { + $finished = false; break; + } + if (strpos($value,'YAMLString') !== false) { + $finished = false; break; + } + } + if ($finished) break; + + $i++; + if ($i > 10) + break; // Prevent infinite loops. + } + + return $explode; + } + + private function literalBlockContinues ($line, $lineIndent) { + if (!trim($line)) return true; + if (strlen($line) - strlen(ltrim($line)) > $lineIndent) return true; + return false; + } + + private function referenceContentsByAlias ($alias) { + do { + if (!isset($this->SavedGroups[$alias])) { echo "Bad group name: $alias."; break; } + $groupPath = $this->SavedGroups[$alias]; + $value = $this->result; + foreach ($groupPath as $k) { + $value = $value[$k]; + } + } while (false); + return $value; + } + + private function addArrayInline ($array, $indent) { + $CommonGroupPath = $this->path; + if (empty ($array)) return false; + + foreach ($array as $k => $_) { + $this->addArray(array($k => $_), $indent); + $this->path = $CommonGroupPath; + } + return true; + } + + private function addArray ($incoming_data, $incoming_indent) { + + // print_r ($incoming_data); + + if (count ($incoming_data) > 1) + return $this->addArrayInline ($incoming_data, $incoming_indent); + + $key = key ($incoming_data); + $value = \WP_CLI\Utils\get_flag_value( $incoming_data, $key ); + if ($key === '__!YAMLZero') $key = '0'; + + if ($incoming_indent == 0 && !$this->_containsGroupAlias && !$this->_containsGroupAnchor) { // Shortcut for root-level values. + if ($key || $key === '' || $key === '0') { + $this->result[$key] = $value; + } else { + $this->result[] = $value; end ($this->result); $key = key ($this->result); + } + $this->path[$incoming_indent] = $key; + return; + } + + + + $history = array(); + // Unfolding inner array tree. + $history[] = $_arr = $this->result; + foreach ($this->path as $k) { + $history[] = $_arr = $_arr[$k]; + } + + if ($this->_containsGroupAlias) { + $value = $this->referenceContentsByAlias($this->_containsGroupAlias); + $this->_containsGroupAlias = false; + } + + + // Adding string or numeric key to the innermost level or $this->arr. + if (is_string($key) && $key == '<<') { + if (!is_array ($_arr)) { $_arr = array (); } + + $_arr = array_merge ($_arr, $value); + } else if ($key || $key === '' || $key === '0') { + if (!is_array ($_arr)) + $_arr = array ($key=>$value); + else + $_arr[$key] = $value; + } else { + if (!is_array ($_arr)) { $_arr = array ($value); $key = 0; } + else { $_arr[] = $value; end ($_arr); $key = key ($_arr); } + } + + $reverse_path = array_reverse($this->path); + $reverse_history = array_reverse ($history); + $reverse_history[0] = $_arr; + $cnt = count($reverse_history) - 1; + for ($i = 0; $i < $cnt; $i++) { + $reverse_history[$i+1][$reverse_path[$i]] = $reverse_history[$i]; + } + $this->result = $reverse_history[$cnt]; + + $this->path[$incoming_indent] = $key; + + if ($this->_containsGroupAnchor) { + $this->SavedGroups[$this->_containsGroupAnchor] = $this->path; + if (is_array ($value)) { + $k = key ($value); + if (!is_int ($k)) { + $this->SavedGroups[$this->_containsGroupAnchor][$incoming_indent + 2] = $k; + } + } + $this->_containsGroupAnchor = false; + } + + } + + private static function startsLiteralBlock ($line) { + $lastChar = substr (trim($line), -1); + if ($lastChar != '>' && $lastChar != '|') return false; + if ($lastChar == '|') return $lastChar; + // HTML tags should not be counted as literal blocks. + if (preg_match ('#<.*?>$#', $line)) return false; + return $lastChar; + } + + private static function greedilyNeedNextLine($line) { + $line = trim ($line); + if (!strlen($line)) return false; + if (substr ($line, -1, 1) == ']') return false; + if ($line[0] == '[') return true; + if (preg_match ('#^[^:]+?:\s*\[#', $line)) return true; + return false; + } + + private function addLiteralLine ($literalBlock, $line, $literalBlockStyle, $indent = -1) { + $line = self::stripIndent($line, $indent); + if ($literalBlockStyle !== '|') { + $line = self::stripIndent($line); + } + $line = rtrim ($line, "\r\n\t ") . "\n"; + if ($literalBlockStyle == '|') { + return $literalBlock . $line; + } + if (strlen($line) == 0) + return rtrim($literalBlock, ' ') . "\n"; + if ($line == "\n" && $literalBlockStyle == '>') { + return rtrim ($literalBlock, " \t") . "\n"; + } + if ($line != "\n") + $line = trim ($line, "\r\n ") . " "; + return $literalBlock . $line; + } + + function revertLiteralPlaceHolder ($lineArray, $literalBlock) { + foreach ($lineArray as $k => $_) { + if (is_array($_)) + $lineArray[$k] = $this->revertLiteralPlaceHolder ($_, $literalBlock); + else if (substr($_, -1 * strlen ($this->LiteralPlaceHolder)) == $this->LiteralPlaceHolder) + $lineArray[$k] = rtrim ($literalBlock, " \r\n"); + } + return $lineArray; + } + + private static function stripIndent ($line, $indent = -1) { + if ($indent == -1) $indent = strlen($line) - strlen(ltrim($line)); + return substr ($line, $indent); + } + + private function getParentPathByIndent ($indent) { + if ($indent == 0) return array(); + $linePath = $this->path; + do { + end($linePath); $lastIndentInParentPath = key($linePath); + if ($indent <= $lastIndentInParentPath) array_pop ($linePath); + } while ($indent <= $lastIndentInParentPath); + return $linePath; + } + + + private function clearBiggerPathValues ($indent) { + + + if ($indent == 0) $this->path = array(); + if (empty ($this->path)) return true; + + foreach ($this->path as $k => $_) { + if ($k > $indent) unset ($this->path[$k]); + } + + return true; + } + + + private static function isComment ($line) { + if (!$line) return false; + if ($line[0] == '#') return true; + if (trim($line, " \r\n\t") == '---') return true; + return false; + } + + private static function isEmpty ($line) { + return (trim ($line) === ''); + } + + + private function isArrayElement ($line) { + if (!$line) return false; + if ($line[0] != '-') return false; + if (strlen ($line) > 3) + if (substr($line,0,3) == '---') return false; + + return true; + } + + private function isHashElement ($line) { + return strpos($line, ':'); + } + + private function isLiteral ($line) { + if ($this->isArrayElement($line)) return false; + if ($this->isHashElement($line)) return false; + return true; + } + + + private static function unquote ($value) { + if (!$value) return $value; + if (!is_string($value)) return $value; + if ($value[0] == '\'') return trim ($value, '\''); + if ($value[0] == '"') return trim ($value, '"'); + return $value; + } + + private function startsMappedSequence ($line) { + return ($line[0] == '-' && substr ($line, -1, 1) == ':'); + } + + private function returnMappedSequence ($line) { + $array = array(); + $key = self::unquote(trim(substr($line,1,-1))); + $array[$key] = array(); + $this->delayedPath = array(strpos ($line, $key) + $this->indent => $key); + return array($array); + } + + private function returnMappedValue ($line) { + $array = array(); + $key = self::unquote (trim(substr($line,0,-1))); + $array[$key] = ''; + return $array; + } + + private function startsMappedValue ($line) { + return (substr ($line, -1, 1) == ':'); + } + + private function isPlainArray ($line) { + return ($line[0] == '[' && substr ($line, -1, 1) == ']'); + } + + private function returnPlainArray ($line) { + return $this->_toType($line); + } + + private function returnKeyValuePair ($line) { + $array = array(); + $key = ''; + if (strpos ($line, ':')) { + // It's a key/value pair most likely + // If the key is in double quotes pull it out + if (($line[0] == '"' || $line[0] == "'") && preg_match('/^(["\'](.*)["\'](\s)*:)/',$line,$matches)) { + $value = trim(str_replace($matches[1],'',$line)); + $key = $matches[2]; + } else { + // Do some guesswork as to the key and the value + $explode = explode(':',$line); + $key = trim($explode[0]); + array_shift($explode); + $value = trim(implode(':',$explode)); + } + // Set the type of the value. Int, string, etc + $value = $this->_toType($value); + if ($key === '0') $key = '__!YAMLZero'; + $array[$key] = $value; + } else { + $array = array ($line); + } + return $array; + + } + + + private function returnArrayElement ($line) { + if (strlen($line) <= 1) return array(array()); // Weird %) + $array = array(); + $value = trim(substr($line,1)); + $value = $this->_toType($value); + $array[] = $value; + return $array; + } + + + private function nodeContainsGroup ($line) { + $symbolsForReference = 'A-z0-9_\-'; + if (strpos($line, '&') === false && strpos($line, '*') === false) return false; // Please die fast ;-) + if ($line[0] == '&' && preg_match('/^(&['.$symbolsForReference.']+)/', $line, $matches)) return $matches[1]; + if ($line[0] == '*' && preg_match('/^(\*['.$symbolsForReference.']+)/', $line, $matches)) return $matches[1]; + if (preg_match('/(&['.$symbolsForReference.']+)$/', $line, $matches)) return $matches[1]; + if (preg_match('/(\*['.$symbolsForReference.']+$)/', $line, $matches)) return $matches[1]; + if (preg_match ('#^\s*<<\s*:\s*(\*[^\s]+).*$#', $line, $matches)) return $matches[1]; + return false; + + } + + private function addGroup ($line, $group) { + if ($group[0] == '&') $this->_containsGroupAnchor = substr ($group, 1); + if ($group[0] == '*') $this->_containsGroupAlias = substr ($group, 1); + //print_r ($this->path); + } + + private function stripGroup ($line, $group) { + $line = trim(str_replace($group, '', $line)); + return $line; + } +} + +endif; diff --git a/php/boot-fs.php b/php/boot-fs.php new file mode 100644 index 00000000..6b4a88e1 --- /dev/null +++ b/php/boot-fs.php @@ -0,0 +1,17 @@ +clean(); + } ); + } + } + + return $cache; + } + + /** + * Set the context in which EE-CLI should be run + */ + public static function set_url( $url ) { + EE_CLI::debug( 'Set URL: ' . $url ); + $url_parts = Utils\parse_url( $url ); + self::set_url_params( $url_parts ); + } + + private static function set_url_params( $url_parts ) { + $f = function( $key ) use ( $url_parts ) { + return \EE_CLI\Utils\get_flag_value( $url_parts, $key, '' ); + }; + + if ( isset( $url_parts['host'] ) ) { + if ( isset( $url_parts['scheme'] ) && 'https' === strtolower( $url_parts['scheme'] ) ) { + $_SERVER['HTTPS'] = 'on'; + } + + $_SERVER['HTTP_HOST'] = $url_parts['host']; + if ( isset( $url_parts['port'] ) ) { + $_SERVER['HTTP_HOST'] .= ':' . $url_parts['port']; + } + + $_SERVER['SERVER_NAME'] = $url_parts['host']; + } + + $_SERVER['REQUEST_URI'] = $f('path') . ( isset( $url_parts['query'] ) ? '?' . $url_parts['query'] : '' ); + $_SERVER['SERVER_PORT'] = \EE_CLI\Utils\get_flag_value( $url_parts, 'port', '80' ); + $_SERVER['QUERY_STRING'] = $f('query'); + } + + /** + * @return WpHttpCacheManager + */ + public static function get_http_cache_manager() { + static $http_cacher; + + if ( !$http_cacher ) { + $http_cacher = new WpHttpCacheManager( self::get_cache() ); + } + + return $http_cacher; + } + + public static function colorize( $string ) { + return \cli\Colors::colorize( $string, self::get_runner()->in_color() ); + } + + /** + * Schedule a callback to be executed at a certain point (before WP is loaded). + */ + public static function add_hook( $when, $callback ) { + if ( in_array( $when, self::$hooks_passed ) ) + call_user_func( $callback ); + + self::$hooks[ $when ][] = $callback; + } + + /** + * Execute registered callbacks. + */ + public static function do_hook( $when ) { + self::$hooks_passed[] = $when; + + if ( !isset( self::$hooks[ $when ] ) ) + return; + + foreach ( self::$hooks[ $when ] as $callback ) { + call_user_func( $callback ); + } + } + + /** + * Add a command to the EE-CLI list of commands + * + * @param string $name The name of the command that will be used in the CLI + * @param string $class The command implementation + * @param array $args An associative array with additional parameters: + * 'before_invoke' => callback to execute before invoking the command + */ + public static function add_command( $name, $class, $args = array() ) { + if ( is_string( $class ) && ! class_exists( (string) $class ) ) { + EE_CLI::error( sprintf( "Class '%s' does not exist.", $class ) ); + } + + if ( isset( $args['before_invoke'] ) ) { + self::add_hook( "before_invoke:$name", $args['before_invoke'] ); + } + + $path = preg_split( '/\s+/', $name ); + + $leaf_name = array_pop( $path ); + $full_path = $path; + + $command = self::get_root_command(); + + while ( !empty( $path ) ) { + $subcommand_name = $path[0]; + $subcommand = $command->find_subcommand( $path ); + + // create an empty container + if ( !$subcommand ) { + $subcommand = new Dispatcher\CompositeCommand( $command, $subcommand_name, + new \EE_CLI\DocParser( '' ) ); + $command->add_subcommand( $subcommand_name, $subcommand ); + } + + $command = $subcommand; + } + + $leaf_command = Dispatcher\CommandFactory::create( $leaf_name, $class, $command ); + + if ( ! $command->can_have_subcommands() ) { + throw new Exception( sprintf( "'%s' can't have subcommands.", + implode( ' ' , Dispatcher\get_path( $command ) ) ) ); + } + + $command->add_subcommand( $leaf_name, $leaf_command ); + } + + /** + * Display a message in the CLI and end with a newline + * + * @param string $message + */ + public static function line( $message = '' ) { + echo $message . "\n"; + } + + /** + * Log an informational message. + * + * @param string $message + */ + public static function log( $message ) { + self::$logger->info( $message ); + } + + /** + * Display a success in the CLI and end with a newline + * + * @param string $message + */ + public static function success( $message ) { + self::$logger->success( $message ); + } + + /** + * Log debug information + * + * @param string $message + */ + public static function debug( $message ) { + self::$logger->debug( self::error_to_string( $message ) ); + } + + /** + * Display a warning in the CLI and end with a newline + * + * @param string $message + */ + public static function warning( $message ) { + self::$logger->warning( self::error_to_string( $message ) ); + } + + /** + * Display an error in the CLI and end with a newline + * + * @param string|EE_Error $message + * @param bool $exit if true, the script will exit() + */ + public static function error( $message, $exit = true ) { + if ( ! isset( self::get_runner()->assoc_args[ 'completions' ] ) ) { + self::$logger->error( self::error_to_string( $message ) ); + } + + if ( $exit ) { + exit(1); + } + } + + /** + * Display an error in the CLI and end with a newline + * + * @param array $message each element from the array will be printed on its own line + */ + public static function error_multi_line( $message_lines ) { + if ( ! isset( self::get_runner()->assoc_args[ 'completions' ] ) && is_array( $message_lines ) ) { + self::$logger->error_multi_line( array_map( array( __CLASS__, 'error_to_string' ), $message_lines ) ); + } + } + + /** + * Ask for confirmation before running a destructive operation. + */ + public static function confirm( $question, $assoc_args = array() ) { + if ( ! \EE_CLI\Utils\get_flag_value( $assoc_args, 'yes' ) ) { + fwrite( STDOUT, $question . " [y/n] " ); + + $answer = trim( fgets( STDIN ) ); + + if ( 'y' != $answer ) + exit; + } + } + + /** + * Read value from a positional argument or from STDIN. + * + * @param array $args The list of positional arguments. + * @param int $index At which position to check for the value. + * + * @return string + */ + public static function get_value_from_arg_or_stdin( $args, $index ) { + if ( isset( $args[ $index ] ) ) { + $raw_value = $args[ $index ]; + } else { + // We don't use file_get_contents() here because it doesn't handle + // Ctrl-D properly, when typing in the value interactively. + $raw_value = ''; + while ( ( $line = fgets( STDIN ) ) !== false ) { + $raw_value .= $line; + } + } + + return $raw_value; + } + + /** + * Read a value, from various formats. + * + * @param mixed $value + * @param array $assoc_args + */ + public static function read_value( $raw_value, $assoc_args = array() ) { + if ( \EE_CLI\Utils\get_flag_value( $assoc_args, 'format' ) === 'json' ) { + $value = json_decode( $raw_value, true ); + if ( null === $value ) { + EE_CLI::error( sprintf( 'Invalid JSON: %s', $raw_value ) ); + } + } else { + $value = $raw_value; + } + + return $value; + } + + /** + * Display a value, in various formats + * + * @param mixed $value + * @param array $assoc_args + */ + public static function print_value( $value, $assoc_args = array() ) { + if ( \EE_CLI\Utils\get_flag_value( $assoc_args, 'format' ) === 'json' ) { + $value = json_encode( $value ); + } elseif ( is_array( $value ) || is_object( $value ) ) { + $value = var_export( $value ); + } + + echo $value . "\n"; + } + + /** + * Convert a ee_error into a string + * + * @param mixed $errors + * @return string + */ + public static function error_to_string( $errors ) { + if ( is_string( $errors ) ) { + return $errors; + } + + if ( is_object( $errors ) && is_a( $errors, 'EE_Error' ) ) { + foreach ( $errors->get_error_messages() as $message ) { + if ( $errors->get_error_data() ) + return $message . ' ' . $errors->get_error_data(); + else + return $message; + } + } + } + + /** + * Launch an external process that takes over I/O. + * + * @param string Command to call + * @param bool Whether to exit if the command returns an error status + * @param bool Whether to return an exit status (default) or detailed execution results + * + * @return int|ProcessRun The command exit status, or a ProcessRun instance + */ + public static function launch( $command, $exit_on_error = true, $return_detailed = false ) { + + $proc = Process::create( $command ); + $results = $proc->run(); + + if ( $results->return_code && $exit_on_error ) + exit( $results->return_code ); + + if ( $return_detailed ) { + return $results; + } else { + return $results->return_code; + } + } + + /** + * Launch another EE-CLI command using the runtime arguments for the current process + * + * @param string Command to call + * @param array $args Positional arguments to use + * @param array $assoc_args Associative arguments to use + * @param bool Whether to exit if the command returns an error status + * @param bool Whether to return an exit status (default) or detailed execution results + * @param array $runtime_args Override one or more global args (path,url,user,allow-root) + * + * @return int|ProcessRun The command exit status, or a ProcessRun instance + */ + public static function launch_self( $command, $args = array(), $assoc_args = array(), $exit_on_error = true, $return_detailed = false, $runtime_args = array() ) { + $reused_runtime_args = array( + 'path', + 'url', + 'user', + 'allow-root', + ); + + foreach ( $reused_runtime_args as $key ) { + if ( isset( $runtime_args[ $key ] ) ) { + $assoc_args[ $key ] = $runtime_args[ $key ]; + } else if ( $value = self::get_runner()->config[ $key ] ) + $assoc_args[ $key ] = $value; + } + + $php_bin = self::get_php_binary(); + + $script_path = $GLOBALS['argv'][0]; + + $args = implode( ' ', array_map( 'escapeshellarg', $args ) ); + $assoc_args = \EE_CLI\Utils\assoc_args_to_str( $assoc_args ); + + $full_command = "{$php_bin} {$script_path} {$command} {$args} {$assoc_args}"; + + return self::launch( $full_command, $exit_on_error, $return_detailed ); + } + + /** + * Get the path to the PHP binary used when executing EE-CLI. + * Environment values permit specific binaries to be indicated. + * + * @return string + */ + public static function get_php_binary() { + if ( defined( 'PHP_BINARY' ) ) + return PHP_BINARY; + + if ( getenv( 'EE_CLI_PHP_USED' ) ) + return getenv( 'EE_CLI_PHP_USED' ); + + if ( getenv( 'EE_CLI_PHP' ) ) + return getenv( 'EE_CLI_PHP' ); + + return 'php'; + } + + public static function get_config( $key = null ) { + if ( null === $key ) { + return self::get_runner()->config; + } + + if ( !isset( self::get_runner()->config[ $key ] ) ) { + self::warning( "Unknown config option '$key'." ); + return null; + } + + return self::get_runner()->config[ $key ]; + } + + /** + * Run a given command within the current process using the same global parameters. + * + * To run a command using a new process with the same global parameters, use EE_CLI::launch_self() + * To run a command using a new process with different global parameters, use EE_CLI::launch() + * + * @param array + * @param array + */ + public static function run_command( $args, $assoc_args = array() ) { + self::get_runner()->run_command( $args, $assoc_args ); + } + + + + // DEPRECATED STUFF + + public static function add_man_dir() { + trigger_error( 'EE_CLI::add_man_dir() is deprecated. Add docs inline.', E_USER_WARNING ); + } + + // back-compat + public static function out( $str ) { + fwrite( STDOUT, $str ); + } + + // back-compat + public static function addCommand( $name, $class ) { + trigger_error( sprintf( 'ee %s: %s is deprecated. use EE_CLI::add_command() instead.', + $name, __FUNCTION__ ), E_USER_WARNING ); + self::add_command( $name, $class ); + } +} diff --git a/php/commands/site.php b/php/commands/site.php new file mode 100644 index 00000000..43c9de34 --- /dev/null +++ b/php/commands/site.php @@ -0,0 +1,30 @@ + + * : Cache key. + *[--files] + * : Webroot + * [--db=] + * : Method for grouping data within the cache which allows the same key to be used across groups. + */ + public function delete( $args, $assoc_args ) { + EE_CLI::success( 'Object deleted.' ); + } + +} + +EE_CLI::add_command( 'site', 'Site_Command' ); diff --git a/php/config-spec.php b/php/config-spec.php new file mode 100644 index 00000000..8df3d1a6 --- /dev/null +++ b/php/config-spec.php @@ -0,0 +1,102 @@ + array( + 'runtime' => '=', + 'file' => '', + 'desc' => 'Path to the WordPress files', + ), + + 'url' => array( + 'runtime' => '=', + 'file' => '', + 'desc' => 'Pretend request came from given URL. In multisite, this argument is how the target site is specified.', + ), + 'blog' => array( + 'deprecated' => 'Use --url instead.', + 'runtime' => '=', + ), + + 'config' => array( + 'deprecated' => 'Use the WP_CLI_CONFIG_PATH environment variable instead.', + 'runtime' => '=', + ), + + 'user' => array( + 'runtime' => '=', + 'file' => '', + 'desc' => 'Set the WordPress user', + ), + + 'skip-plugins' => array( + 'runtime' => '[=]', + 'file' => '', + 'desc' => 'Skip loading all or some plugins', + 'default' => '', + ), + + 'skip-themes' => array( + 'runtime' => '[=]', + 'file' => '', + 'desc' => 'Skip loading all or some themes', + 'default' => '', + ), + + 'require' => array( + 'runtime' => '=', + 'file' => '', + 'desc' => 'Load PHP file before running the command (may be used more than once)', + 'multiple' => true, + 'default' => array(), + ), + + 'disabled_commands' => array( + 'file' => '', + 'default' => array(), + 'desc' => '(Sub)commands to disable', + ), + + 'color' => array( + 'runtime' => true, + 'file' => '', + 'default' => 'auto', + 'desc' => 'Whether to colorize the output', + ), + + 'debug' => array( + 'runtime' => '', + 'file' => '', + 'default' => false, + 'desc' => 'Show all PHP errors; add verbosity to WP-CLI bootstrap', + ), + + 'prompt' => array( + 'runtime' => '', + 'file' => false, + 'default' => false, + 'desc' => 'Prompt the user to enter values for all command arguments', + ), + + 'quiet' => array( + 'runtime' => '', + 'file' => '', + 'default' => false, + 'desc' => 'Suppress informational messages', + ), + + 'apache_modules' => array( + 'file' => '', + 'desc' => 'List of Apache Modules that are to be reported as loaded', + 'multiple' => true, + 'default' => array(), + ), + + # --allow-root => (NOT RECOMMENDED) Allow wp-cli to run as root. This poses + # a security risk, so you probably do not want to do this. + 'allow-root' => array( + 'file' => false, # Explicit. Just in case the default changes. + 'runtime' => '', + 'hidden' => true, + ), + +); diff --git a/php/dispatcher.php b/php/dispatcher.php new file mode 100644 index 00000000..daf48ead --- /dev/null +++ b/php/dispatcher.php @@ -0,0 +1,19 @@ +get_name() ); + } while ( $command = $command->get_parent() ); + + return $path; +} diff --git a/php/ee-cli.php b/php/ee-cli.php new file mode 100644 index 00000000..0a0e7388 --- /dev/null +++ b/php/ee-cli.php @@ -0,0 +1,21 @@ +start(); diff --git a/php/export/class-wp-export-oxymel.php b/php/export/class-wp-export-oxymel.php new file mode 100644 index 00000000..74c1a7cf --- /dev/null +++ b/php/export/class-wp-export-oxymel.php @@ -0,0 +1,25 @@ +$tag_name( $contents ); + } + return $this; + } + + public function optional_cdata( $tag_name, $contents ) { + if ( $contents ) { + $this->$tag_name->contains->cdata( $contents )->end; + } + return $this; + } + + public function cdata( $text ) { + if ( !seems_utf8( $text ) ) { + $text = utf8_encode( $text ); + } + return parent::cdata( $text ); + } +} + diff --git a/php/export/class-wp-export-query.php b/php/export/class-wp-export-query.php new file mode 100644 index 00000000..4d271b48 --- /dev/null +++ b/php/export/class-wp-export-query.php @@ -0,0 +1,372 @@ + null, + 'post_type' => null, + 'status' => null, + 'author' => null, + 'start_date' => null, + 'end_date' => null, + 'start_id' => null, + 'category' => null, + ); + + private $post_ids; + private $filters; + private $xml_gen; + + private $wheres = array(); + private $joins = array(); + + private $author; + private $category; + + public $missing_parents = false; + + public function __construct( $filters = array() ) { + $this->filters = wp_parse_args( $filters, self::$defaults ); + $this->post_ids = $this->calculate_post_ids(); + } + + public function post_ids() { + return $this->post_ids; + } + + public function charset() { + return get_bloginfo( 'charset' ); + } + + public function site_metadata() { + $metadata = array( + 'name' => $this->bloginfo_rss( 'name' ), + 'url' => $this->bloginfo_rss( 'url' ), + 'language' => $this->bloginfo_rss( 'language' ), + 'description' => $this->bloginfo_rss( 'description' ), + 'pubDate' => date( 'D, d M Y H:i:s +0000' ), + 'site_url' => is_multisite()? network_home_url() : $this->bloginfo_rss( 'url' ), + 'blog_url' => $this->bloginfo_rss( 'url' ), + ); + return $metadata; + } + + public function wp_generator_tag() { + return apply_filters( 'the_generator', get_the_generator( 'export' ), 'export' ); + } + + public function authors() { + global $wpdb; + $authors = array(); + $author_ids = $wpdb->get_col( "SELECT DISTINCT post_author FROM $wpdb->posts WHERE post_status != 'auto-draft'" ); + foreach ( (array) $author_ids as $author_id ) { + $authors[] = get_userdata( $author_id ); + } + $authors = array_filter( $authors ); + return $authors; + } + + public function categories() { + if ( $this->category ) { + return array( $this->category ); + } + if ( $this->filters['post_type'] ) { + return array(); + } + $categories = (array) get_categories( array( 'get' => 'all' ) ); + + $this->check_for_orphaned_terms( $categories ); + + $categories = self::topologically_sort_terms( $categories ); + + return $categories; + } + + public function tags() { + if ( $this->filters['post_type'] ) { + return array(); + } + $tags = (array) get_tags( array( 'get' => 'all' ) ); + + $this->check_for_orphaned_terms( $tags ); + + return $tags; + } + + public function custom_taxonomies_terms() { + if ( $this->filters['post_type'] ) { + return array(); + } + $custom_taxonomies = get_taxonomies( array( '_builtin' => false ) ); + $custom_terms = (array) get_terms( $custom_taxonomies, array( 'get' => 'all' ) ); + $this->check_for_orphaned_terms( $custom_terms ); + $custom_terms = self::topologically_sort_terms( $custom_terms ); + return $custom_terms; + } + + public function nav_menu_terms() { + $nav_menus = wp_get_nav_menus(); + foreach( $nav_menus as &$term ) { + $term->description = ''; + } + return $nav_menus; + } + + public function exportify_post( $post ) { + $GLOBALS['wp_query']->in_the_loop = true; + $previous_global_post = \WP_CLI\Utils\get_flag_value( $GLOBALS, 'post' ); + $GLOBALS['post'] = $post; + setup_postdata( $post ); + $post->post_content = apply_filters( 'the_content_export', $post->post_content ); + $post->post_excerpt = apply_filters( 'the_excerpt_export', $post->post_excerpt ); + $post->is_sticky = is_sticky( $post->ID ) ? 1 : 0; + $post->terms = self::get_terms_for_post( $post ); + $post->meta = self::get_meta_for_post( $post ); + $post->comments = self::get_comments_for_post( $post ); + $GLOBALS['post'] = $previous_global_post; + return $post; + } + + public function posts() { + $posts_iterator = new WP_Post_IDs_Iterator( $this->post_ids, self::QUERY_CHUNK ); + return new WP_Map_Iterator( $posts_iterator, array( $this, 'exportify_post' ) ); + } + + private function calculate_post_ids() { + global $wpdb; + if ( is_array( $this->filters['post_ids'] ) ) { + return $this->filters['post_ids']; + } + $this->post_type_where(); + $this->status_where(); + $this->author_where(); + $this->start_date_where(); + $this->end_date_where(); + $this->start_id_where(); + $this->category_where(); + + $where = implode( ' AND ', array_filter( $this->wheres ) ); + if ( $where ) $where = "WHERE $where"; + $join = implode( ' ', array_filter( $this->joins ) ); + + $post_ids = $wpdb->get_col( "SELECT ID FROM {$wpdb->posts} AS p $join $where" ); + $post_ids = array_merge( $post_ids, $this->attachments_for_specific_post_types( $post_ids ) ); + return $post_ids; + } + + private function post_type_where() { + global $wpdb; + $post_types_filters = array( 'can_export' => true ); + if ( $this->filters['post_type'] ) { + $post_types = $this->filters['post_type']; + + // Flatten single post types + if ( is_array( $post_types ) && 1 === count( $post_types ) ) { + $post_types = array_shift( $post_types ); + } + $post_types_filters = array_merge( $post_types_filters, array( 'name' => $post_types ) ); + } + + // Multiple post types + if ( is_array( $post_types_filters['name'] ) ) { + $post_types = array(); + foreach ( $post_types_filters['name'] as $post_type ) { + if ( post_type_exists( $post_type ) ) { + $post_types[] = $post_type; + } + } + } else { + $post_types = get_post_types( $post_types_filters ); + } + + if ( ! $post_types ) { + $this->wheres[] = 'p.post_type IS NULL'; + return; + } + $this->wheres[] = _wp_export_build_IN_condition( 'p.post_type', $post_types ); + } + + private function status_where() { + global $wpdb; + if ( !$this->filters['status'] ) { + $this->wheres[] = "p.post_status != 'auto-draft'"; + return; + } + $this->wheres[] = $wpdb->prepare( 'p.post_status = %s', $this->filters['status'] ); + } + + private function author_where() { + global $wpdb; + $user = $this->find_user_from_any_object( $this->filters['author'] ); + if ( !$user || is_wp_error( $user ) ) { + return; + } + $this->author = $user; + $this->wheres[] = $wpdb->prepare( 'p.post_author = %d', $user->ID ); + } + + private function start_date_where() { + global $wpdb; + $timestamp = strtotime( $this->filters['start_date'] ); + if ( !$timestamp ) { + return; + } + $this->wheres[] = $wpdb->prepare( 'p.post_date >= %s', date( 'Y-m-d 00:00:00', $timestamp ) ); + } + + private function end_date_where() { + global $wpdb; + if ( preg_match( '/^\d{4}-\d{2}$/', $this->filters['end_date'] ) ) { + $timestamp = $this->get_timestamp_for_the_last_day_of_a_month( $this->filters['end_date'] ); + } else { + $timestamp = strtotime( $this->filters['end_date'] ); + } + if ( !$timestamp ) { + return; + } + $this->wheres[] = $wpdb->prepare( 'p.post_date <= %s', date( 'Y-m-d 23:59:59', $timestamp ) ); + } + + private function start_id_where() { + global $wpdb; + + $start_id = absint( $this->filters['start_id'] ); + if ( 0 === $start_id ) { + return; + } + $this->wheres[] = $wpdb->prepare( 'p.ID >= %d', $start_id ); + } + + private function get_timestamp_for_the_last_day_of_a_month( $yyyy_mm ) { + return strtotime( "$yyyy_mm +1month -1day" ); + } + + private function category_where() { + global $wpdb; + if ( 'post' != $this->filters['post_type'] && ! in_array( 'post', (array) $this->filters['post_type'] ) ) { + return; + } + $category = $this->find_category_from_any_object( $this->filters['category'] ); + if ( !$category ) { + return; + } + $this->category = $category; + $this->joins[] = "INNER JOIN {$wpdb->term_relationships} AS tr ON (p.ID = tr.object_id)"; + $this->wheres[] = $wpdb->prepare( 'tr.term_taxonomy_id = %d', $category->term_taxonomy_id ); + } + + private function attachments_for_specific_post_types( $post_ids ) { + global $wpdb; + if ( !$this->filters['post_type'] ) { + return array(); + } + $attachment_ids = array(); + while ( $batch_of_post_ids = array_splice( $post_ids, 0, self::QUERY_CHUNK ) ) { + $post_parent_condition = _wp_export_build_IN_condition( 'post_parent', $batch_of_post_ids ); + $attachment_ids = array_merge( $attachment_ids, (array)$wpdb->get_col( "SELECT ID FROM {$wpdb->posts} WHERE post_type = 'attachment' AND $post_parent_condition" ) ); + } + return array_map( 'intval', $attachment_ids ); + } + + private function bloginfo_rss( $section ) { + return apply_filters( 'bloginfo_rss', get_bloginfo_rss( $section ), $section ); + } + + private function find_user_from_any_object( $user ) { + if ( is_numeric( $user ) ) { + return get_user_by( 'id', $user ); + } elseif ( is_string( $user ) ) { + return get_user_by( 'login', $user ); + } elseif ( isset( $user->ID ) ) { + return get_user_by( 'id', $user->ID ); + } + return false; + } + + private function find_category_from_any_object( $category ) { + if ( is_numeric( $category ) ) { + return get_term( $category, 'category' ); + } elseif ( is_string( $category ) ) { + $term = term_exists( $category, 'category' ); + return isset( $term['term_id'] )? get_term( $term['term_id'], 'category' ) : false; + } elseif ( isset( $category->term_id ) ) { + return get_term( $category->term_id, 'category' ); + } + return false; + } + + private static function topologically_sort_terms( $terms ) { + $sorted = array(); + while ( $term = array_shift( $terms ) ) { + if ( $term->parent == 0 || isset( $sorted[$term->parent] ) ) + $sorted[$term->term_id] = $term; + else + $terms[] = $term; + } + return $sorted; + } + + private function check_for_orphaned_terms( $terms ) { + $term_ids = array(); + $have_parent = array(); + + foreach ( $terms as $term ) { + $term_ids[ $term->term_id ] = true; + if ( $term->parent != 0 ) + $have_parent[] = $term; + } + + foreach ( $have_parent as $has_parent ) { + if ( ! isset( $term_ids[ $has_parent->parent ] ) ) { + $this->missing_parents = $has_parent; + throw new WP_Export_Term_Exception( sprintf( __( 'Term is missing a parent: %s (%d)' ), $has_parent->slug, $has_parent->term_taxonomy_id ) ); + } + } + } + + private static function get_terms_for_post( $post ) { + $taxonomies = get_object_taxonomies( $post->post_type ); + if ( empty( $taxonomies ) ) + return array(); + $terms = wp_get_object_terms( $post->ID, $taxonomies ); + $terms = $terms? $terms : array(); + return $terms; + } + + private static function get_meta_for_post( $post ) { + global $wpdb; + $meta_for_export = array(); + $meta_from_db = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM $wpdb->postmeta WHERE post_id = %d", $post->ID ) ); + foreach ( $meta_from_db as $meta ) { + if ( apply_filters( 'wxr_export_skip_postmeta', false, $meta->meta_key, $meta ) ) + continue; + if ( in_array( $meta->meta_key, array( '_edit_lock', '_wp_attachment_metadata', '_wp_attached_file' ) ) ) { + continue; + } + $meta_for_export[] = $meta; + } + return $meta_for_export; + } + + private static function get_comments_for_post( $post ) { + global $wpdb; + $comments = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM $wpdb->comments WHERE comment_post_ID = %d AND comment_approved <> 'spam'", $post->ID ) ); + foreach( $comments as $comment ) { + $meta = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM $wpdb->commentmeta WHERE comment_id = %d", $comment->comment_ID ) ); + $meta = $meta? $meta : array(); + $comment->meta = $meta; + } + return $comments; + } +} + +class WP_Export_Exception extends RuntimeException { +} + +class WP_Export_Term_Exception extends RuntimeException { +} diff --git a/php/export/class-wp-export-wxr-formatter.php b/php/export/class-wp-export-wxr-formatter.php new file mode 100644 index 00000000..ca874c6f --- /dev/null +++ b/php/export/class-wp-export-wxr-formatter.php @@ -0,0 +1,261 @@ +export = $export; + $this->wxr_version = WXR_VERSION; + } + + public function before_posts() { + $before_posts_xml = ''; + $before_posts_xml .= $this->header(); + $before_posts_xml .= $this->site_metadata(); + $before_posts_xml .= $this->authors(); + $before_posts_xml .= $this->categories(); + $before_posts_xml .= $this->tags(); + $before_posts_xml .= $this->nav_menu_terms(); + $before_posts_xml .= $this->custom_taxonomies_terms(); + $before_posts_xml .= $this->rss2_head_action(); + return $before_posts_xml; + } + + public function posts() { + return new WP_Map_Iterator( $this->export->posts(), array( $this, 'post' ) ); + } + + public function after_posts() { + return $this->footer(); + } + + public function header() { + $oxymel = new Oxymel; + $charset = $this->export->charset(); + $wp_generator_tag = $this->export->wp_generator_tag(); + $comment = <<xml + ->comment( $comment ) + ->raw( $wp_generator_tag ) + ->open_rss( array( + 'version' => '2.0', + 'xmlns:excerpt' => "http://wordpress.org/export/{$this->wxr_version}/excerpt/", + 'xmlns:content' => "http://purl.org/rss/1.0/modules/content/", + 'xmlns:wfw' => "http://wellformedweb.org/CommentAPI/", + 'xmlns:dc' => "http://purl.org/dc/elements/1.1/", + 'xmlns:wp' => "http://wordpress.org/export/{$this->wxr_version}/", + ) ) + ->open_channel + ->to_string(); + + } + + public function site_metadata() { + $oxymel = new Oxymel; + $metadata = $this->export->site_metadata(); + return $oxymel + ->title( $metadata['name'] ) + ->link( $metadata['url'] ) + ->description( $metadata['description'] ) + ->pubDate( $metadata['pubDate'] ) + ->language( $metadata['language'] ) + ->tag( 'wp:wxr_version', $this->wxr_version ) + ->tag( 'wp:base_site_url', $metadata['site_url'] ) + ->tag( 'wp:base_blog_url', $metadata['blog_url'] ) + ->to_string(); + } + + public function authors() { + $oxymel = new Oxymel; + $authors = $this->export->authors(); + foreach ( $authors as $author ) { + $oxymel + ->tag( 'wp:author' )->contains + ->tag( 'wp:author_id', $author->ID ) + ->tag( 'wp:author_login', $author->user_login ) + ->tag( 'wp:author_email', $author->user_email ) + ->tag( 'wp:author_display_name' )->contains->cdata( $author->display_name )->end + ->tag( 'wp:author_first_name' )->contains->cdata( $author->user_firstname )->end + ->tag( 'wp:author_last_name' )->contains->cdata( $author->user_lastname )->end + ->end; + } + return $oxymel->to_string(); + } + + public function categories() { + $oxymel = new WP_Export_Oxymel; + $categories = $this->export->categories(); + foreach( $categories as $term_id => $category ) { + $category->parent_slug = $category->parent? $categories[$category->parent]->slug : ''; + $oxymel->tag( 'wp:category' )->contains + ->tag( 'wp:term_id', $category->term_id ) + ->tag( 'wp:category_nicename', $category->slug ) + ->tag( 'wp:category_parent', $category->parent_slug ) + ->optional_cdata( 'wp:cat_name', $category->name ) + ->optional_cdata( 'wp:category_description', $category->description ) + ->end; + } + return $oxymel->to_string(); + } + + public function tags() { + $oxymel = new WP_Export_Oxymel; + $tags = $this->export->tags(); + foreach( $tags as $tag ) { + $oxymel->tag( 'wp:tag' )->contains + ->tag( 'wp:term_id', $tag->term_id ) + ->tag( 'wp:tag_slug', $tag->slug ) + ->optional_cdata( 'wp:tag_name', $tag->name ) + ->optional_cdata( 'wp:tag_description', $tag->description ) + ->end; + } + return $oxymel->to_string(); + } + + public function nav_menu_terms() { + return $this->terms( $this->export->nav_menu_terms() ); + } + + public function custom_taxonomies_terms() { + return $this->terms( $this->export->custom_taxonomies_terms() ); + } + + public function rss2_head_action() { + ob_start(); + do_action( 'rss2_head' ); + $action_output = ob_get_clean(); + return $action_output; + } + + public function post( $post ) { + $oxymel = new WP_Export_Oxymel; + $GLOBALS['wp_query']->in_the_loop = true; + $GLOBALS['post'] = $post; + setup_postdata( $post ); + + $oxymel->item->contains + ->title( apply_filters( 'the_title_rss', $post->post_title ) ) + ->link( esc_url( apply_filters('the_permalink_rss', get_permalink() ) ) ) + ->pubDate( mysql2date( 'D, d M Y H:i:s +0000', get_post_time( 'Y-m-d H:i:s', true ), false ) ) + ->tag( 'dc:creator', get_the_author_meta( 'login' ) ) + ->guid( esc_url( get_the_guid() ), array( 'isPermaLink' => 'false' ) ) + ->description( '' ) + ->tag( 'content:encoded' )->contains->cdata( $post->post_content )->end + ->tag( 'excerpt:encoded' )->contains->cdata( $post->post_excerpt )->end + ->tag( 'wp:post_id', $post->ID ) + ->tag( 'wp:post_date', $post->post_date ) + ->tag( 'wp:post_date_gmt', $post->post_date_gmt ) + ->tag( 'wp:comment_status', $post->comment_status ) + ->tag( 'wp:ping_status', $post->ping_status ) + ->tag( 'wp:post_name', $post->post_name ) + ->tag( 'wp:status', $post->post_status ) + ->tag( 'wp:post_parent', $post->post_parent ) + ->tag( 'wp:menu_order', $post->menu_order ) + ->tag( 'wp:post_type', $post->post_type ) + ->tag( 'wp:post_password', $post->post_password ) + ->tag( 'wp:is_sticky', $post->is_sticky ) + ->optional( 'wp:attachment_url', wp_get_attachment_url( $post->ID ) ); + foreach( $post->terms as $term ) { + $oxymel + ->category( array( 'domain' => $term->taxonomy, 'nicename' => $term->slug ) )->contains->cdata( $term->name )->end; + } + foreach( $post->meta as $meta ) { + $oxymel + ->tag( 'wp:postmeta' )->contains + ->tag( 'wp:meta_key', $meta->meta_key ) + ->tag( 'wp:meta_value' )->contains->cdata( $meta->meta_value )->end + ->end; + } + foreach( $post->comments as $comment ) { + $oxymel + ->tag( 'wp:comment' )->contains + ->tag( 'wp:comment_id', $comment->comment_ID ) + ->tag( 'wp:comment_author' )->contains->cdata( $comment->comment_author )->end + ->tag( 'wp:comment_author_email', $comment->comment_author_email ) + ->tag( 'wp:comment_author_url', esc_url( $comment->comment_author_url ) ) + ->tag( 'wp:comment_author_IP', $comment->comment_author_IP ) + ->tag( 'wp:comment_date', $comment->comment_date ) + ->tag( 'wp:comment_date_gmt', $comment->comment_date_gmt ) + ->tag( 'wp:comment_content' )->contains->cdata( $comment->comment_content )->end + ->tag( 'wp:comment_approved', $comment->comment_approved ) + ->tag( 'wp:comment_type', $comment->comment_type ) + ->tag( 'wp:comment_parent', $comment->comment_parent ) + ->tag( 'wp:comment_user_id', $comment->user_id ) + ->oxymel( $this->comment_meta( $comment ) ) + ->end; + } + $oxymel + ->end; + return $oxymel->to_string(); + } + + public function footer() { + $oxymel = new Oxymel; + return $oxymel->close_channel->close_rss->to_string(); + } + + protected function terms( $terms ) { + $oxymel = new WP_Export_Oxymel; + foreach( $terms as $term ) { + $term->parent_slug = $term->parent? $terms[$term->parent]->slug : ''; + $oxymel->tag( 'wp:term' )->contains + ->tag( 'wp:term_id', $term->term_id ) + ->tag( 'wp:term_taxonomy', $term->taxonomy ) + ->tag( 'wp:term_slug', $term->slug ); + if ( 'nav_menu' != $term->taxonomy ) { + $oxymel + ->tag( 'wp:term_parent', $term->parent_slug ); + } + $oxymel + ->optional_cdata( 'wp:term_name', $term->name ) + ->optional_cdata( 'wp:term_description', $term->description ) + ->end; + } + return $oxymel->to_string(); + } + + protected function comment_meta( $comment ) { + global $wpdb; + $metas = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM $wpdb->commentmeta WHERE comment_id = %d", $comment->comment_ID ) ); + if ( !$metas ) { + return new Oxymel; + } + $oxymel = new WP_Export_Oxymel; + foreach( $metas as $meta ) { + $oxymel->tag( 'wp:commentmeta' )->contains + ->tag( 'wp:meta_key', $meta->meta_key ) + ->tag( 'wp:meta_value' )->contains->cdata( $meta->meta_value )->end + ->end; + } + return $oxymel; + } +} diff --git a/php/export/functions.export.php b/php/export/functions.export.php new file mode 100644 index 00000000..492e97e9 --- /dev/null +++ b/php/export/functions.export.php @@ -0,0 +1,41 @@ + array(), + 'format' => 'WP_Export_WXR_Formatter', + 'writer' => 'WP_Export_Returner', + 'writer_args' => null, + ); + $args = wp_parse_args( $args, $defaults ); + $export_query = new WP_Export_Query( $args['filters'] ); + $formatter = new $args['format']( $export_query ); + $writer = new $args['writer']( $formatter, $args['writer_args'] ); + try { + return $writer->export(); + } catch ( WP_Export_Exception $e ) { + return new WP_Error( 'wp-export-error', $e->getMessage() ); + } +} + +function wp_export_new_style_args_from_old_style_args( $args ) { + if ( isset( $args['content'] ) ) { + if ( 'all' == $args['content'] ) { + unset( $args['content'] ); + } else { + $args['post_type'] = $args['content']; + } + } + return $args; +} + +// TEMPORARY +function _wp_export_build_IN_condition( $column_name, $values, $format = '%s' ) { + global $wpdb; + + if ( !is_array( $values ) || empty( $values ) ) { + return ''; + } + $formats = implode( ', ', array_fill( 0, count( $values ), $format ) ); + return $wpdb->prepare( "$column_name IN ($formats)", $values ); +} diff --git a/php/export/iterators.php b/php/export/iterators.php new file mode 100644 index 00000000..86572f56 --- /dev/null +++ b/php/export/iterators.php @@ -0,0 +1,80 @@ +callback = $callback; + parent::__construct( $iterator ); + } + + function current() { + $original_current = parent::current(); + return call_user_func( $this->callback, $original_current ); + } +} + +class WP_Post_IDs_Iterator implements Iterator { + private $limit = 100; + private $post_ids; + private $ids_left; + private $results = array(); + + public function __construct( $post_ids, $limit = null ) { + $this->db = $GLOBALS['wpdb']; + $this->post_ids = $post_ids; + $this->ids_left = $post_ids; + if ( !is_null( $limit ) ) { + $this->limit = $limit; + } + } + + public function current() { + return $this->results[$this->index_in_results]; + } + + public function key() { + return $this->global_index; + } + + public function next() { + $this->index_in_results++; + $this->global_index++; + } + + public function rewind() { + $this->results = array(); + $this->global_index = 0; + $this->index_in_results = 0; + $this->ids_left = $this->post_ids; + } + + public function valid() { + if ( isset( $this->results[$this->index_in_results] ) ) { + return true; + } + if ( empty( $this->ids_left ) ) { + return false; + } + $has_posts = $this->load_next_posts_from_db(); + if ( !$has_posts ) { + return false; + } + $this->index_in_results = 0; + return true; + } + + private function load_next_posts_from_db() { + $next_batch_post_ids = array_splice( $this->ids_left, 0, $this->limit ); + $in_post_ids_sql = _wp_export_build_IN_condition( 'ID', $next_batch_post_ids ); + $this->results = $this->db->get_results( "SELECT * FROM {$this->db->posts} WHERE $in_post_ids_sql" ); + if ( !$this->results ) { + if ( $this->db->last_error ) { + throw new WP_Iterator_Exception( 'Database error: ' . $this->db->last_error ); + } else { + return false; + } + } + return true; + } +} + +class WP_Iterator_Exception extends Exception { +} diff --git a/php/export/writers.php b/php/export/writers.php new file mode 100644 index 00000000..989dd18e --- /dev/null +++ b/php/export/writers.php @@ -0,0 +1,184 @@ +formatter = $formatter; + } + + public function export() { + $this->write( $this->formatter->before_posts() ); + foreach( $this->formatter->posts() as $post_in_wxr ) { + $this->write( $post_in_wxr ); + } + $this->write( $this->formatter->after_posts() ); + } + + abstract protected function write( $xml ); +} + +class WP_Export_XML_Over_HTTP extends WP_Export_Base_Writer { + private $file_name; + + function __construct( $formatter, $file_name ) { + parent::__construct( $formatter ); + $this->file_name = $file_name; + } + + public function export() { + try { + $export = $this->get_export(); + $this->send_headers(); + echo $export; + } catch ( WP_Export_Exception $e ) { + $message = apply_filters( 'export_error_message', $e->getMessage() ); + wp_die( $message, __( 'Export Error' ), array( 'back_link' => true ) ); + } catch ( WP_Export_Term_Exception $e ) { + do_action( 'export_term_orphaned', $this->formatter->export->missing_parents ); + $message = apply_filters( 'export_term_error_message', $e->getMessage() ); + wp_die( $message, __( 'Export Error' ), array( 'back_link' => true ) ); + } + } + + protected function write( $xml ) { + $this->result .= $xml; + } + + protected function get_export() { + $this->result = ''; + parent::export(); + return $this->result; + } + + protected function send_headers() { + header( 'Content-Description: File Transfer' ); + header( 'Content-Disposition: attachment; filename=' . $this->file_name ); + header( 'Content-Type: text/xml; charset=' . get_option( 'blog_charset' ), true ); + } +} + +class WP_Export_Returner extends WP_Export_Base_Writer { + private $result = ''; + + public function export() { + $this->private = ''; + try { + parent::export(); + } catch ( WP_Export_Exception $e ) { + $message = apply_filters( 'export_error_message', $e->getMessage() ); + return new WP_Error( 'wp-export-error', $message ); + + } catch ( WP_Export_Term_Exception $e ) { + do_action( 'export_term_orphaned', $this->formatter->export->missing_parents ); + $message = apply_filters( 'export_term_error_message', $e->getMessage() ); + return new WP_Error( 'wp-export-error', $message ); + } + return $this->result; + } + protected function write( $xml ) { + $this->result .= $xml; + } +} + +class WP_Export_File_Writer extends WP_Export_Base_Writer { + private $f; + private $file_name; + + public function __construct( $formatter, $file_name ) { + parent::__construct( $formatter ); + $this->file_name = $file_name; + } + + public function export() { + $this->f = fopen( $this->file_name, 'w' ); + if ( !$this->f ) { + throw new WP_Export_Exception( sprintf( __( 'WP Export: error opening %s for writing.' ), $this->file_name ) ); + } + + try { + parent::export(); + } catch ( WP_Export_Exception $e ) { + throw $e; + } catch ( WP_Export_Term_Exception $e ) { + throw $e; + } + + fclose( $this->f ); + } + + protected function write( $xml ) { + $res = fwrite( $this->f, $xml); + if ( false === $res ) { + throw new WP_Export_Exception( __( 'WP Export: error writing to export file.' ) ); + } + } +} + +class WP_Export_Split_Files_Writer extends WP_Export_Base_Writer { + private $result = ''; + private $f; + private $next_file_number = 0; + private $current_file_size = 0; + + function __construct( $formatter, $writer_args = array() ) { + parent::__construct( $formatter ); + //TODO: check if args are not missing + $this->max_file_size = is_null( $writer_args['max_file_size'] ) ? 15 * MB_IN_BYTES : $writer_args['max_file_size']; + $this->destination_directory = $writer_args['destination_directory']; + $this->filename_template = $writer_args['filename_template']; + $this->before_posts_xml = $this->formatter->before_posts(); + $this->after_posts_xml = $this->formatter->after_posts(); + } + + public function export() { + $this->start_new_file(); + foreach( $this->formatter->posts() as $post_xml ) { + if ( $this->current_file_size + strlen( $post_xml ) > $this->max_file_size ) { + $this->start_new_file(); + } + $this->write( $post_xml ); + } + $this->close_current_file(); + } + + protected function write( $xml ) { + $res = fwrite( $this->f, $xml); + if ( false === $res ) { + throw new WP_Export_Exception( __( 'WP Export: error writing to export file.' ) ); + } + $this->current_file_size += strlen( $xml ); + } + + private function start_new_file() { + if ( $this->f ) { + $this->close_current_file(); + } + $file_path = $this->next_file_path(); + $this->f = fopen( $file_path, 'w' ); + if ( !$this->f ) { + throw new WP_Export_Exception( sprintf( __( 'WP Export: error opening %s for writing.' ), $file_path ) ); + } + do_action( 'wp_export_new_file', $file_path ); + $this->current_file_size = 0; + $this->write( $this->before_posts_xml ); + } + + private function close_current_file() { + if ( !$this->f ) { + return; + } + $this->write( $this->after_posts_xml ); + fclose( $this->f ); + } + + private function next_file_name() { + $next_file_name = sprintf( $this->filename_template, $this->next_file_number ); + $this->next_file_number++; + return $next_file_name; + } + + private function next_file_path() { + return untrailingslashit( $this->destination_directory ) . DIRECTORY_SEPARATOR . $this->next_file_name(); + } + +} diff --git a/php/utils.php b/php/utils.php new file mode 100644 index 00000000..f4051d67 --- /dev/null +++ b/php/utils.php @@ -0,0 +1,575 @@ +{'vendor-dir'} ) ) { + array_unshift( $vendor_paths, EE_CLI_ROOT . '/../../../' . $composer->{'vendor-dir'} ); + } + } + return $vendor_paths; +} + +// Using require() directly inside a class grants access to private methods to the loaded code +function load_file( $path ) { + require_once $path; +} + +function load_command( $name ) { + $path = EE_CLI_ROOT . "/php/commands/$name.php"; + + if ( is_readable( $path ) ) { + include_once $path; + } +} + +function load_all_commands() { + $cmd_dir = EE_CLI_ROOT . '/php/commands'; + + $iterator = new \DirectoryIterator( $cmd_dir ); + + foreach ( $iterator as $filename ) { + if ( '.php' != substr( $filename, -4 ) ) + continue; + + include_once "$cmd_dir/$filename"; + } +} + +/** + * Like array_map(), except it returns a new iterator, instead of a modified array. + * + * Example: + * + * $arr = array('Football', 'Socker'); + * + * $it = iterator_map($arr, 'strtolower', function($val) { + * return str_replace('foo', 'bar', $val); + * }); + * + * foreach ( $it as $val ) { + * var_dump($val); + * } + * + * @param array|object Either a plain array or another iterator + * @param callback The function to apply to an element + * @return object An iterator that applies the given callback(s) + */ +function iterator_map( $it, $fn ) { + if ( is_array( $it ) ) { + $it = new \ArrayIterator( $it ); + } + + if ( !method_exists( $it, 'add_transform' ) ) { + $it = new Transform( $it ); + } + + foreach ( array_slice( func_get_args(), 1 ) as $fn ) { + $it->add_transform( $fn ); + } + + return $it; +} + +/** + * Search for file by walking up the directory tree until the first file is found or until $stop_check($dir) returns true + * @param string|array The files (or file) to search for + * @param string|null The directory to start searching from; defaults to CWD + * @param callable Function which is passed the current dir each time a directory level is traversed + * @return null|string Null if the file was not found + */ +function find_file_upward( $files, $dir = null, $stop_check = null ) { + $files = (array) $files; + if ( is_null( $dir ) ) { + $dir = getcwd(); + } + while ( is_readable( $dir ) ) { + // Stop walking up when the supplied callable returns true being passed the $dir + if ( is_callable( $stop_check ) && call_user_func( $stop_check, $dir ) ) { + return null; + } + + foreach ( $files as $file ) { + $path = $dir . DIRECTORY_SEPARATOR . $file; + if ( file_exists( $path ) ) { + return $path; + } + } + + $parent_dir = dirname( $dir ); + if ( empty($parent_dir) || $parent_dir === $dir ) { + break; + } + $dir = $parent_dir; + } + return null; +} + +function is_path_absolute( $path ) { + // Windows + if ( isset($path[1]) && ':' === $path[1] ) + return true; + + return $path[0] === '/'; +} + +/** + * Composes positional arguments into a command string. + * + * @param array + * @return string + */ +function args_to_str( $args ) { + return ' ' . implode( ' ', array_map( 'escapeshellarg', $args ) ); +} + +/** + * Composes associative arguments into a command string. + * + * @param array + * @return string + */ +function assoc_args_to_str( $assoc_args ) { + $str = ''; + + foreach ( $assoc_args as $key => $value ) { + if ( true === $value ) + $str .= " --$key"; + else + $str .= " --$key=" . escapeshellarg( $value ); + } + + return $str; +} + +/** + * Given a template string and an arbitrary number of arguments, + * returns the final command, with the parameters escaped. + */ +function esc_cmd( $cmd ) { + if ( func_num_args() < 2 ) + trigger_error( 'esc_cmd() requires at least two arguments.', E_USER_WARNING ); + + $args = func_get_args(); + + $cmd = array_shift( $args ); + + return vsprintf( $cmd, array_map( 'escapeshellarg', $args ) ); +} + +function locate_wp_config() { + static $path; + + if ( null === $path ) { + if ( file_exists( ABSPATH . 'wp-config.php' ) ) + $path = ABSPATH . 'wp-config.php'; + elseif ( file_exists( ABSPATH . '../wp-config.php' ) && ! file_exists( ABSPATH . '/../wp-settings.php' ) ) + $path = ABSPATH . '../wp-config.php'; + else + $path = false; + + if ( $path ) + $path = realpath( $path ); + } + + return $path; +} + +/** + * Output items in a table, JSON, CSV, ids, or the total count + * + * @param string $format Format to use: 'table', 'json', 'csv', 'ids', 'count' + * @param array $items Data to output + * @param array|string $fields Named fields for each item of data. Can be array or comma-separated list + */ +function format_items( $format, $items, $fields ) { + $assoc_args = compact( 'format', 'fields' ); + $formatter = new \EE_CLI\Formatter( $assoc_args ); + $formatter->display_items( $items ); +} + +/** + * Write data as CSV to a given file. + * + * @param resource $fd File descriptor + * @param array $rows Array of rows to output + * @param array $headers List of CSV columns (optional) + */ +function write_csv( $fd, $rows, $headers = array() ) { + if ( ! empty( $headers ) ) { + fputcsv( $fd, $headers ); + } + + foreach ( $rows as $row ) { + if ( ! empty( $headers ) ) { + $row = pick_fields( $row, $headers ); + } + + fputcsv( $fd, array_values( $row ) ); + } +} + +/** + * Pick fields from an associative array or object. + * + * @param array|object Associative array or object to pick fields from + * @param array List of fields to pick + * @return array + */ +function pick_fields( $item, $fields ) { + $item = (object) $item; + + $values = array(); + + foreach ( $fields as $field ) { + $values[ $field ] = isset( $item->$field ) ? $item->$field : null; + } + + return $values; +} + +/** + * Launch system's $EDITOR to edit text + * + * @param str $content Text to edit (eg post content) + * @return str|bool Edited text, if file is saved from editor + * False, if no change to file + */ +function launch_editor_for_input( $input, $title = 'ee-cli' ) { + + $tmpfile = wp_tempnam( $title ); + + if ( !$tmpfile ) + \EE_CLI::error( 'Error creating temporary file.' ); + + $output = ''; + file_put_contents( $tmpfile, $input ); + + $editor = getenv( 'EDITOR' ); + if ( !$editor ) { + if ( isset( $_SERVER['OS'] ) && false !== strpos( $_SERVER['OS'], 'indows' ) ) + $editor = 'notepad'; + else + $editor = 'vi'; + } + + $descriptorspec = array( STDIN, STDOUT, STDERR ); + $process = proc_open( "$editor " . escapeshellarg( $tmpfile ), $descriptorspec, $pipes ); + $r = proc_close( $process ); + if ( $r ) { + exit( $r ); + } + + $output = file_get_contents( $tmpfile ); + + unlink( $tmpfile ); + + if ( $output === $input ) + return false; + + return $output; +} + +/** + * @param string MySQL host string, as defined in wp-config.php + * @return array + */ +function mysql_host_to_cli_args( $raw_host ) { + $assoc_args = array(); + + $host_parts = explode( ':', $raw_host ); + if ( count( $host_parts ) == 2 ) { + list( $assoc_args['host'], $extra ) = $host_parts; + $extra = trim( $extra ); + if ( is_numeric( $extra ) ) { + $assoc_args['port'] = intval( $extra ); + $assoc_args['protocol'] = 'tcp'; + } else if ( $extra !== '' ) { + $assoc_args['socket'] = $extra; + } + } else { + $assoc_args['host'] = $raw_host; + } + + return $assoc_args; +} + +function run_mysql_command( $cmd, $assoc_args, $descriptors = null ) { + if ( !$descriptors ) + $descriptors = array( STDIN, STDOUT, STDERR ); + + if ( isset( $assoc_args['host'] ) ) { + $assoc_args = array_merge( $assoc_args, mysql_host_to_cli_args( $assoc_args['host'] ) ); + } + + $pass = $assoc_args['pass']; + unset( $assoc_args['pass'] ); + + $old_pass = getenv( 'MYSQL_PWD' ); + putenv( 'MYSQL_PWD=' . $pass ); + + $final_cmd = $cmd . assoc_args_to_str( $assoc_args ); + + $proc = proc_open( $final_cmd, $descriptors, $pipes ); + if ( !$proc ) + exit(1); + + $r = proc_close( $proc ); + + putenv( 'MYSQL_PWD=' . $old_pass ); + + if ( $r ) exit( $r ); +} + +/** + * Render PHP or other types of files using Mustache templates. + * + * IMPORTANT: Automatic HTML escaping is disabled! + */ +function mustache_render( $template_name, $data ) { + if ( ! file_exists( $template_name ) ) + $template_name = EE_CLI_ROOT . "/templates/$template_name"; + + $template = file_get_contents( $template_name ); + + $m = new \Mustache_Engine( array( + 'escape' => function ( $val ) { return $val; } + ) ); + + return $m->render( $template, $data ); +} + +function make_progress_bar( $message, $count ) { + if ( \cli\Shell::isPiped() ) + return new \EE_CLI\NoOp; + + return new \cli\progress\Bar( $message, $count ); +} + +function parse_url( $url ) { + $url_parts = \parse_url( $url ); + + if ( !isset( $url_parts['scheme'] ) ) { + $url_parts = parse_url( 'http://' . $url ); + } + + return $url_parts; +} + +/** + * Check if we're running in a Windows environment (cmd.exe). + */ +function is_windows() { + return strtoupper(substr(PHP_OS, 0, 3)) === 'WIN'; +} + +/** + * Replace magic constants in some PHP source code. + * + * @param string $source The PHP code to manipulate. + * @param string $path The path to use instead of the magic constants + */ +function replace_path_consts( $source, $path ) { + $replacements = array( + '__FILE__' => "'$path'", + '__DIR__' => "'" . dirname( $path ) . "'" + ); + + $old = array_keys( $replacements ); + $new = array_values( $replacements ); + + return str_replace( $old, $new, $source ); +} + +/** + * Make a HTTP request to a remote URL + * + * @param string $method + * @param string $url + * @param array $headers + * @param array $options + * @return object + */ +function http_request( $method, $url, $data = null, $headers = array(), $options = array() ) { + + $cert_path = '/rmccue/requests/library/Requests/Transport/cacert.pem'; + if ( inside_phar() ) { + // cURL can't read Phar archives + $options['verify'] = extract_from_phar( + EE_CLI_ROOT . '/vendor' . $cert_path ); + } else { + foreach( get_vendor_paths() as $vendor_path ) { + if ( file_exists( $vendor_path . $cert_path ) ) { + $options['verify'] = $vendor_path . $cert_path; + break; + } + } + if ( empty( $options['verify'] ) ){ + EE_CLI::error_log( "Cannot find SSL certificate." ); + } + } + + try { + $request = \Requests::request( $url, $headers, $data, $method, $options ); + return $request; + } catch( \Requests_Exception $ex ) { + // Handle SSL certificate issues gracefully + \EE_CLI::warning( $ex->getMessage() ); + $options['verify'] = false; + try { + return \Requests::request( $url, $headers, $data, $method, $options ); + } catch( \Requests_Exception $ex ) { + \EE_CLI::error( $ex->getMessage() ); + } + } +} + +/** + * Increments a version string using the "x.y.z-pre" format + * + * Can increment the major, minor or patch number by one + * If $new_version == "same" the version string is not changed + * If $new_version is not a known keyword, it will be used as the new version string directly + * + * @param string $current_version + * @param string $new_version + * @return string + */ +function increment_version( $current_version, $new_version ) { + // split version assuming the format is x.y.z-pre + $current_version = explode( '-', $current_version, 2 ); + $current_version[0] = explode( '.', $current_version[0] ); + + switch ( $new_version ) { + case 'same': + // do nothing + break; + + case 'patch': + $current_version[0][2]++; + + $current_version = array( $current_version[0] ); // drop possible pre-release info + break; + + case 'minor': + $current_version[0][1]++; + $current_version[0][2] = 0; + + $current_version = array( $current_version[0] ); // drop possible pre-release info + break; + + case 'major': + $current_version[0][0]++; + $current_version[0][1] = 0; + $current_version[0][2] = 0; + + $current_version = array( $current_version[0] ); // drop possible pre-release info + break; + + default: // not a keyword + $current_version = array( array( $new_version ) ); + break; + } + + // reconstruct version string + $current_version[0] = implode( '.', $current_version[0] ); + $current_version = implode( '-', $current_version ); + + return $current_version; +} + +/** + * Compare two version strings to get the named semantic version + * + * @param string $new_version + * @param string $original_version + * @return string $name 'major', 'minor', 'patch' + */ +function get_named_sem_ver( $new_version, $original_version ) { + + if ( ! Comparator::greaterThan( $new_version, $original_version ) ) { + return ''; + } + + $parts = explode( '-', $original_version ); + list( $major, $minor, $patch ) = explode( '.', $parts[0] ); + + if ( Semver::satisfies( $new_version, "{$major}.{$minor}.x" ) ) { + return 'patch'; + } else if ( Semver::satisfies( $new_version, "{$major}.x.x" ) ) { + return 'minor'; + } else { + return 'major'; + } +} + +/** + * Return the flag value or, if it's not set, the $default value. + * + * @param array $args Arguments array. + * @param string $flag Flag to get the value. + * @param mixed $default Default value for the flag. Default: NULL + * @return mixed + */ +function get_flag_value( $args, $flag, $default = null ) { + return isset( $args[ $flag ] ) ? $args[ $flag ] : $default; +} diff --git a/templates/man-params.mustache b/templates/man-params.mustache new file mode 100644 index 00000000..a27f4d84 --- /dev/null +++ b/templates/man-params.mustache @@ -0,0 +1,14 @@ +{{^root_command}} + + +{{/root_command}} +## GLOBAL PARAMETERS + +{{#parameters}} + {{synopsis}} + {{desc}} + +{{/parameters}} +{{#root_command}} + Run 'wp help ' to get more information on a specific command. +{{/root_command}}