' . static::class . '',
'init'
),
diff --git a/inc/admin-pages/class-site-edit-admin-page.php b/inc/admin-pages/class-site-edit-admin-page.php
index fae2697c..0873038d 100644
--- a/inc/admin-pages/class-site-edit-admin-page.php
+++ b/inc/admin-pages/class-site-edit-admin-page.php
@@ -299,6 +299,15 @@ public function register_widgets(): void {
parent::register_widgets();
+ /**
+ * Allows other components to register widgets on the Site edit page.
+ *
+ * @since 2.5.0
+ *
+ * @param Site_Edit_Admin_Page $page The current admin page instance.
+ */
+ do_action('wu_edit_site_page_register_widgets', $this);
+
$label = $this->get_object()->get_type_label();
$class = $this->get_object()->get_type_class();
diff --git a/inc/admin-pages/class-site-list-admin-page.php b/inc/admin-pages/class-site-list-admin-page.php
index 2eb7d60a..bfd72460 100644
--- a/inc/admin-pages/class-site-list-admin-page.php
+++ b/inc/admin-pages/class-site-list-admin-page.php
@@ -547,7 +547,7 @@ public function get_submenu_title() {
*/
public function action_links() {
- return [
+ $links = [
[
'label' => __('Add Site', 'ultimate-multisite'),
'icon' => 'wu-circle-with-plus',
@@ -555,6 +555,16 @@ public function action_links() {
'url' => wu_get_form_url('add_new_site'),
],
];
+
+ /**
+ * Filters the action links for the Sites list page.
+ *
+ * @since 2.5.0
+ *
+ * @param array $links The action links.
+ * @return array
+ */
+ return apply_filters('wu_site_list_page_action_links', $links);
}
/**
diff --git a/inc/admin-pages/class-template-library-admin-page.php b/inc/admin-pages/class-template-library-admin-page.php
new file mode 100644
index 00000000..5a1cd6df
--- /dev/null
+++ b/inc/admin-pages/class-template-library-admin-page.php
@@ -0,0 +1,673 @@
+ 'wu_read_settings',
+ ];
+
+ /**
+ * Should we hide admin notices on this page?
+ *
+ * @since 2.5.0
+ * @var bool
+ */
+ protected $hide_admin_notices = false;
+
+ /**
+ * Should we force the admin menu into a folded state?
+ *
+ * @since 2.5.0
+ * @var bool
+ */
+ protected $fold_menu = false;
+
+ /**
+ * Holds the section slug for the URLs.
+ *
+ * @since 2.5.0
+ * @var string
+ */
+ protected $section_slug = 'tab';
+
+ /**
+ * Defines if the step links on the side are clickable or not.
+ *
+ * @since 2.5.0
+ * @var bool
+ */
+ protected $clickable_navigation = true;
+
+ /**
+ * Template Repository instance.
+ *
+ * @since 2.5.0
+ * @var Template_Repository|null
+ */
+ protected ?Template_Repository $repository = null;
+
+ /**
+ * Allow child classes to add hooks to be run once the page is loaded.
+ *
+ * @since 2.5.0
+ * @return void
+ */
+ public function init(): void {
+
+ parent::init();
+
+ add_action('wp_ajax_serve_templates_list', [$this, 'serve_templates_list']);
+ }
+
+ /**
+ * Register forms.
+ *
+ * @since 2.5.0
+ * @return void
+ */
+ public function register_forms(): void {
+
+ wu_register_form(
+ 'template_more_info',
+ [
+ 'render' => [$this, 'display_more_info'],
+ 'handler' => [$this, 'install_template'],
+ ]
+ );
+
+ wu_register_form(
+ 'upload_template',
+ [
+ 'render' => [$this, 'render_upload_template_modal'],
+ 'handler' => [$this, 'handle_upload_template_modal'],
+ 'capability' => 'manage_network',
+ ]
+ );
+ }
+
+ /**
+ * Renders the upload template modal.
+ *
+ * @since 2.5.0
+ * @return void
+ */
+ public function render_upload_template_modal(): void {
+
+ // Reset upload limits for large ZIP files
+ \WP_Ultimo\Site_Exporter\Site_Exporter::get_instance()->reset_upload_limits();
+
+ $fields = [
+ 'template_name' => [
+ 'type' => 'text',
+ 'title' => __('Template Name', 'ultimate-multisite'),
+ 'placeholder' => __('My Awesome Template', 'ultimate-multisite'),
+ 'desc' => __('A descriptive name for this template.', 'ultimate-multisite'),
+ ],
+ 'zip_file' => [
+ 'type' => 'text',
+ 'title' => __('ZIP File URL', 'ultimate-multisite'),
+ 'placeholder' => __('https://example.com/export.zip', 'ultimate-multisite'),
+ 'desc' => __('Upload or enter URL to a site export ZIP file.', 'ultimate-multisite'),
+ 'html_attr' => [
+ 'id' => 'wu-template-zip-url',
+ ],
+ ],
+ 'upload_btn' => [
+ 'type' => 'html',
+ 'content' => sprintf(
+ '',
+ __('Upload ZIP File', 'ultimate-multisite')
+ ),
+ 'wrapper_classes' => 'wu-mb-4',
+ ],
+ 'template_url' => [
+ 'type' => 'text',
+ 'title' => __('Template Site URL', 'ultimate-multisite'),
+ 'placeholder' => is_subdomain_install() ? 'template-name.example.com' : 'example.com/template-name',
+ 'desc' => __('The URL for the new template site.', 'ultimate-multisite'),
+ ],
+ 'categories' => [
+ 'type' => 'text',
+ 'title' => __('Categories', 'ultimate-multisite'),
+ 'placeholder' => __('business, portfolio', 'ultimate-multisite'),
+ 'desc' => __('Comma-separated list of categories.', 'ultimate-multisite'),
+ ],
+ 'submit_button' => [
+ 'type' => 'submit',
+ 'title' => __('Create Template', 'ultimate-multisite'),
+ 'value' => 'save',
+ 'classes' => 'button button-primary wu-w-full',
+ 'wrapper_classes' => 'wu-items-end wu-text-right',
+ ],
+ ];
+
+ $form = new \WP_Ultimo\UI\Form(
+ 'upload_template',
+ $fields,
+ [
+ 'views' => 'admin-pages/fields',
+ 'classes' => 'wu-modal-form wu-widget-list wu-striped wu-m-0 wu-mt-0',
+ 'field_wrapper_classes' => 'wu-w-full wu-box-border wu-items-center wu-flex wu-justify-between wu-p-4 wu-m-0 wu-border-t wu-border-l-0 wu-border-r-0 wu-border-b-0 wu-border-gray-300 wu-border-solid',
+ ]
+ );
+
+ $form->render();
+
+ // Add media uploader script
+ ?>
+
+ $template_url,
+ 'new_url' => $template_url,
+ 'delete_file' => false,
+ 'zip_url' => $zip_url,
+ ]
+ );
+
+ if (is_wp_error($result)) {
+ wp_send_json_error($result);
+ }
+
+ // Parse categories
+ $category_list = array_map('trim', explode(',', $categories));
+ $category_list = array_filter($category_list);
+
+ // Note: The site will be created by the import process.
+ // We set a transient to update it to site_template type after import completes.
+ set_site_transient(
+ 'wu_pending_template_setup_' . md5($template_url),
+ [
+ 'name' => $template_name,
+ 'url' => $template_url,
+ 'categories' => $category_list,
+ ],
+ HOUR_IN_SECONDS
+ );
+
+ wp_send_json_success(
+ [
+ 'redirect_url' => wu_network_admin_url(
+ 'wp-ultimo-sites',
+ [
+ 'type' => 'site_template',
+ 'updated' => __('Template import started. The site will be available shortly.', 'ultimate-multisite'),
+ ]
+ ),
+ ]
+ );
+ }
+
+ /**
+ * Gets the Template Repository.
+ *
+ * @since 2.5.0
+ * @return Template_Repository
+ */
+ protected function get_repository(): Template_Repository {
+
+ if (null === $this->repository) {
+ $this->repository = new Template_Repository();
+ }
+
+ return $this->repository;
+ }
+
+ /**
+ * Get a template given a slug.
+ *
+ * @since 2.5.0
+ * @param string $template_slug The template slug.
+ * @return array|null
+ */
+ private function get_template(string $template_slug): ?array {
+
+ $templates = $this->get_templates_list();
+
+ foreach ($templates as $template) {
+ if ($template['slug'] === $template_slug) {
+ return $template;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Displays the more info modal.
+ *
+ * @since 2.5.0
+ * @return void
+ */
+ public function display_more_info(): void {
+
+ $template_slug = wu_request('template');
+
+ $template = $this->get_template($template_slug);
+
+ wu_get_template(
+ 'template-library/details',
+ [
+ 'template' => (object) $template,
+ 'template_slug' => $template_slug,
+ ]
+ );
+
+ do_action('wu_form_scripts', false);
+ }
+
+ /**
+ * Installs a given template.
+ *
+ * @since 2.5.0
+ * @return void
+ */
+ public function install_template(): void {
+
+ if (! current_user_can('manage_network_plugins')) {
+ $error = new \WP_Error('error', __('You do not have enough permissions to perform this task.', 'ultimate-multisite'));
+
+ wp_send_json_error($error);
+ }
+
+ $template_slug = wu_request('template');
+
+ $template = $this->get_template($template_slug);
+
+ if (! $template) {
+ wp_send_json_error(new \WP_Error('not_found', __('Template not found.', 'ultimate-multisite')));
+ }
+
+ $download_url = $template['download_url'] ?? '';
+
+ if (! $download_url) {
+ wp_send_json_error(
+ new \WP_Error(
+ 'no_download',
+ sprintf(
+ /* translators: %s slug of the template. */
+ __('Unable to download template. User does not have permission to install %s', 'ultimate-multisite'),
+ $template_slug
+ )
+ )
+ );
+ }
+
+ // Security check: Ensure URL is from our domain
+ $allowed = strncmp($download_url, MULTISITE_ULTIMATE_UPDATE_URL, strlen(MULTISITE_ULTIMATE_UPDATE_URL)) === 0;
+
+ if (! $allowed) {
+ $error = new \WP_Error('insecure-url', __('You are trying to download a template from an insecure URL', 'ultimate-multisite'));
+ wp_send_json_error($error);
+ }
+
+ // Install the template
+ $installer = $this->get_repository()->get_installer();
+
+ $result = $installer->install(
+ $download_url,
+ [
+ 'slug' => $template['slug'],
+ 'name' => $template['name'],
+ 'version' => $template['template_version'],
+ ]
+ );
+
+ if (is_wp_error($result)) {
+ wp_send_json_error($result);
+ }
+
+ // Clear template cache to reflect installation
+ $this->get_repository()->clear_cache();
+
+ wp_send_json_success(
+ [
+ 'redirect_url' => wu_network_admin_url(
+ 'wp-ultimo-sites',
+ [
+ 'type' => 'site_template',
+ ]
+ ),
+ 'message' => __('Template installed successfully!', 'ultimate-multisite'),
+ ]
+ );
+ }
+
+ /**
+ * Enqueue the necessary scripts.
+ *
+ * @since 2.5.0
+ * @return void
+ */
+ public function register_scripts(): void {
+
+ wp_enqueue_style('theme');
+
+ // Enqueue media uploader for template uploads
+ wp_enqueue_media();
+
+ wp_register_script('wu-template-library', wu_get_asset('template-library.js', 'js'), ['jquery', 'wu-vue', 'underscore'], wu_get_version(), true);
+
+ wp_localize_script(
+ 'wu-template-library',
+ 'wu_template_library',
+ [
+ 'search' => wu_request('s', ''),
+ 'category' => wu_request('tab', 'all'),
+ 'i18n' => [
+ 'all' => __('All Templates', 'ultimate-multisite'),
+ 'loading' => __('Loading templates...', 'ultimate-multisite'),
+ 'no_result' => __('No templates found.', 'ultimate-multisite'),
+ ],
+ ]
+ );
+
+ wp_enqueue_script('wu-template-library');
+ }
+
+ /**
+ * Fetches the list of templates available.
+ *
+ * @since 2.5.0
+ * @return array
+ */
+ protected function get_templates_list(): array {
+
+ $templates = $this->get_repository()->get_templates();
+
+ if (is_wp_error($templates)) {
+ wu_log_add(
+ 'api-calls',
+ sprintf(
+ /* translators: %s error message. */
+ __('Failed to fetch templates from API: %s', 'ultimate-multisite'),
+ $templates->get_error_message()
+ )
+ );
+ return [];
+ }
+
+ return $templates;
+ }
+
+ /**
+ * Gets the list of templates from the remote server.
+ *
+ * @since 2.5.0
+ * @return void
+ */
+ public function serve_templates_list(): void {
+
+ $templates_list = $this->get_templates_list();
+
+ wp_send_json_success($templates_list);
+ }
+
+ /**
+ * Returns the title of the page.
+ *
+ * @since 2.5.0
+ * @return string Title of the page.
+ */
+ public function get_title(): string {
+
+ return __('Template Library', 'ultimate-multisite');
+ }
+
+ /**
+ * Returns the title of menu for this page.
+ *
+ * @since 2.5.0
+ * @return string Menu label of the page.
+ */
+ public function get_menu_title(): string {
+
+ return __('Template Library', 'ultimate-multisite');
+ }
+
+ /**
+ * Returns the title links for this page.
+ *
+ * @since 2.5.0
+ * @return array
+ */
+ public function get_title_links(): array {
+
+ return [
+ [
+ 'label' => __('Upload Template', 'ultimate-multisite'),
+ 'icon' => 'upload',
+ 'classes' => 'wubox',
+ 'url' => wu_get_form_url('upload_template'),
+ ],
+ ];
+ }
+
+ /**
+ * Every child class should implement the output method to display the contents of the page.
+ *
+ * @since 2.5.0
+ * @return void
+ */
+ public function output(): void {
+
+ $addon_repo = \WP_Ultimo::get_instance()->get_addon_repository();
+
+ $redirect_url = wu_network_admin_url('wp-ultimo-template-library');
+ $code = wu_request('code');
+
+ if (wu_request('logout') && wp_verify_nonce(wu_request('_wpnonce'), 'logout')) {
+ $addon_repo->delete_tokens();
+ }
+
+ $more_info_url = wu_get_form_url(
+ 'template_more_info',
+ [
+ 'width' => 768,
+ 'template' => 'TEMPLATE_SLUG',
+ ]
+ );
+
+ $user = $addon_repo->get_user_data();
+
+ if (! $user && $code) {
+ $addon_repo->save_access_token($code, $redirect_url);
+ $user = $addon_repo->get_user_data();
+ }
+
+ wu_get_template(
+ 'template-library/template-library',
+ [
+ 'screen' => get_current_screen(),
+ 'page' => $this,
+ 'classes' => '',
+ 'sections' => $this->get_sections(),
+ 'current_section' => $this->get_current_section(),
+ 'clickable_navigation' => $this->clickable_navigation,
+ 'more_info_url' => $more_info_url,
+ 'oauth_url' => $addon_repo->get_oauth_url(),
+ 'logout_url' => wu_network_admin_url(
+ 'wp-ultimo-template-library',
+ [
+ 'logout' => 'logout',
+ '_wpnonce' => wp_create_nonce('logout'),
+ ]
+ ),
+ 'user' => $user ?? false,
+ ]
+ );
+ }
+
+ /**
+ * Returns the list of settings sections.
+ *
+ * @since 2.5.0
+ * @return array
+ */
+ public function get_sections(): array {
+
+ return [
+ 'all' => [
+ 'title' => __('All Templates', 'ultimate-multisite'),
+ 'icon' => 'dashicons-wu-grid',
+ ],
+ 'business' => [
+ 'title' => __('Business', 'ultimate-multisite'),
+ 'icon' => 'dashicons-wu-briefcase',
+ ],
+ 'portfolio' => [
+ 'title' => __('Portfolio', 'ultimate-multisite'),
+ 'icon' => 'dashicons-wu-image',
+ ],
+ 'blog' => [
+ 'title' => __('Blog', 'ultimate-multisite'),
+ 'icon' => 'dashicons-wu-text-page',
+ ],
+ 'ecommerce' => [
+ 'title' => __('E-commerce', 'ultimate-multisite'),
+ 'icon' => 'dashicons-wu-shop',
+ ],
+ 'agency' => [
+ 'title' => __('Agency', 'ultimate-multisite'),
+ 'icon' => 'dashicons-wu-rocket',
+ ],
+ 'saas' => [
+ 'title' => __('SaaS', 'ultimate-multisite'),
+ 'icon' => 'dashicons-wu-cloud',
+ ],
+ 'community' => [
+ 'title' => __('Community', 'ultimate-multisite'),
+ 'icon' => 'dashicons-wu-users',
+ ],
+ ];
+ }
+
+ /**
+ * Default handler for step submission. Simply redirects to the next step.
+ *
+ * @since 2.5.0
+ * @return void
+ */
+ public function default_handler(): void {
+ // Not used for this page.
+ }
+}
diff --git a/inc/admin/class-network-usage-columns.php b/inc/admin/class-network-usage-columns.php
index 840f8cc7..e71f5153 100644
--- a/inc/admin/class-network-usage-columns.php
+++ b/inc/admin/class-network-usage-columns.php
@@ -172,7 +172,7 @@ public function manage_themes_custom_column(string $column_name, string $theme_k
if ( $theme_data->parent() ) {
echo '+ +
++ +
++ ' . esc_html($blog_details->blogname) . ' (' . esc_html($blog_details->siteurl) . ')' + ); + ?> +
+ + ++ + + +
+| + | + |
|---|---|
| options[0] ?? __('Unknown', 'ultimate-multisite')); ?> | ++ |
| + | + | + |
|---|---|---|
| options[0] ?? '')); ?> | +options[1]['new_url'] ?? ''); ?> | ++ + + + | +
%s
' . esc_html__('Site exported successfully!', 'ultimate-multisite') . '
' . esc_html__('Site export started in background. Check back shortly.', 'ultimate-multisite') . '
' . esc_html__('Export deleted successfully.', 'ultimate-multisite') . '
' . esc_html__('Site import started. The site will be available shortly.', 'ultimate-multisite') . '
' . esc_html($error_text) . '
%s
', + __('No exports available for this site.', 'ultimate-multisite') + ); + } + + $html = '
diff --git a/inc/site-exporter/mu-migration/bin/install-package-tests.sh b/inc/site-exporter/mu-migration/bin/install-package-tests.sh
new file mode 100644
index 00000000..2ff49dd8
--- /dev/null
+++ b/inc/site-exporter/mu-migration/bin/install-package-tests.sh
@@ -0,0 +1,10 @@
+#!/usr/bin/env bash
+
+set -ex
+
+install_db() {
+ mysql -e 'CREATE DATABASE IF NOT EXISTS wp_cli_test;' -uroot
+ mysql -e 'GRANT ALL PRIVILEGES ON wp_cli_test.* TO "wp_cli_test"@"localhost" IDENTIFIED BY "password1"' -uroot
+}
+
+install_db
diff --git a/inc/site-exporter/mu-migration/bin/test.sh b/inc/site-exporter/mu-migration/bin/test.sh
new file mode 100644
index 00000000..65198feb
--- /dev/null
+++ b/inc/site-exporter/mu-migration/bin/test.sh
@@ -0,0 +1,14 @@
+#!/bin/bash
+
+set -ex
+
+# Run the unit tests, if they exist
+if [ -f "phpunit.xml" ] || [ -f "phpunit.xml.dist" ]
+then
+ phpunit
+fi
+
+# Run the functional tests
+BEHAT_TAGS=$(php utils/behat-tags.php)
+
+vendor/bin/behat --format progress $BEHAT_TAGS --strict
diff --git a/inc/site-exporter/mu-migration/composer.json b/inc/site-exporter/mu-migration/composer.json
new file mode 100644
index 00000000..2233fc7d
--- /dev/null
+++ b/inc/site-exporter/mu-migration/composer.json
@@ -0,0 +1,25 @@
+{
+ "name": "10up/mu-migration",
+ "description": "A set of WP-CLI commands to support the migration of single WordPress instances to multisite",
+ "type": "wp-cli-package",
+ "homepage": "https://github.com/10up/MU-Migration",
+ "support": {
+ "issues": "https://github.com/10up/MU-Migration/issues"
+ },
+ "require": {
+ "alchemy/zippy": "0.4.8"
+ },
+ "license": "MIT",
+ "authors": [
+ {
+ "name": "Nícholas André",
+ "email": "nicholas@iotecnologia.com.br"
+ }
+ ],
+ "autoload": {
+ "files": [ "mu-migration.php" ]
+ },
+ "require-dev": {
+ "behat/behat": "~2.5"
+ }
+}
diff --git a/inc/site-exporter/mu-migration/composer.lock b/inc/site-exporter/mu-migration/composer.lock
new file mode 100644
index 00000000..e73b243e
--- /dev/null
+++ b/inc/site-exporter/mu-migration/composer.lock
@@ -0,0 +1,1150 @@
+{
+ "_readme": [
+ "This file locks the dependencies of your project to a known state",
+ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
+ "This file is @generated automatically"
+ ],
+ "content-hash": "755a0b7cfc174b3e983f2153afebb0be",
+ "packages": [
+ {
+ "name": "alchemy/zippy",
+ "version": "0.4.8",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/alchemy-fr/Zippy.git",
+ "reference": "2c231a0956daa0fa1e6057d411504ff98717392e"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/alchemy-fr/Zippy/zipball/2c231a0956daa0fa1e6057d411504ff98717392e",
+ "reference": "2c231a0956daa0fa1e6057d411504ff98717392e",
+ "shasum": ""
+ },
+ "require": {
+ "doctrine/collections": "~1.0",
+ "php": ">=5.5",
+ "symfony/filesystem": "^2.0.5|^3.0",
+ "symfony/polyfill-mbstring": "^1.3",
+ "symfony/process": "^2.1|^3.0"
+ },
+ "require-dev": {
+ "ext-zip": "*",
+ "guzzle/guzzle": "~3.0",
+ "guzzlehttp/guzzle": "^6.0",
+ "phpunit/phpunit": "^4.0|^5.0",
+ "symfony/finder": "^2.0.5|^3.0"
+ },
+ "suggest": {
+ "ext-zip": "To use the ZipExtensionAdapter",
+ "guzzle/guzzle": "To use the GuzzleTeleporter with Guzzle 3",
+ "guzzlehttp/guzzle": "To use the GuzzleTeleporter with Guzzle 6"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "0.4.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Alchemy\\Zippy\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Alchemy",
+ "email": "dev.team@alchemy.fr",
+ "homepage": "http://www.alchemy.fr/"
+ }
+ ],
+ "description": "Zippy, the archive manager companion",
+ "keywords": [
+ "bzip",
+ "compression",
+ "tar",
+ "zip"
+ ],
+ "support": {
+ "issues": "https://github.com/alchemy-fr/Zippy/issues",
+ "source": "https://github.com/alchemy-fr/Zippy/tree/master"
+ },
+ "time": "2017-03-03T08:42:32+00:00"
+ },
+ {
+ "name": "doctrine/collections",
+ "version": "1.8.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/doctrine/collections.git",
+ "reference": "2b44dd4cbca8b5744327de78bafef5945c7e7b5e"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/doctrine/collections/zipball/2b44dd4cbca8b5744327de78bafef5945c7e7b5e",
+ "reference": "2b44dd4cbca8b5744327de78bafef5945c7e7b5e",
+ "shasum": ""
+ },
+ "require": {
+ "doctrine/deprecations": "^0.5.3 || ^1",
+ "php": "^7.1.3 || ^8.0"
+ },
+ "require-dev": {
+ "doctrine/coding-standard": "^9.0 || ^10.0",
+ "phpstan/phpstan": "^1.4.8",
+ "phpunit/phpunit": "^7.5 || ^8.5 || ^9.1.5",
+ "vimeo/psalm": "^4.22"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Doctrine\\Common\\Collections\\": "lib/Doctrine/Common/Collections"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Guilherme Blanco",
+ "email": "guilhermeblanco@gmail.com"
+ },
+ {
+ "name": "Roman Borschel",
+ "email": "roman@code-factory.org"
+ },
+ {
+ "name": "Benjamin Eberlei",
+ "email": "kontakt@beberlei.de"
+ },
+ {
+ "name": "Jonathan Wage",
+ "email": "jonwage@gmail.com"
+ },
+ {
+ "name": "Johannes Schmitt",
+ "email": "schmittjoh@gmail.com"
+ }
+ ],
+ "description": "PHP Doctrine Collections library that adds additional functionality on top of PHP arrays.",
+ "homepage": "https://www.doctrine-project.org/projects/collections.html",
+ "keywords": [
+ "array",
+ "collections",
+ "iterators",
+ "php"
+ ],
+ "support": {
+ "issues": "https://github.com/doctrine/collections/issues",
+ "source": "https://github.com/doctrine/collections/tree/1.8.0"
+ },
+ "time": "2022-09-01T20:12:10+00:00"
+ },
+ {
+ "name": "doctrine/deprecations",
+ "version": "v1.1.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/doctrine/deprecations.git",
+ "reference": "612a3ee5ab0d5dd97b7cf3874a6efe24325efac3"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/doctrine/deprecations/zipball/612a3ee5ab0d5dd97b7cf3874a6efe24325efac3",
+ "reference": "612a3ee5ab0d5dd97b7cf3874a6efe24325efac3",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.1 || ^8.0"
+ },
+ "require-dev": {
+ "doctrine/coding-standard": "^9",
+ "phpstan/phpstan": "1.4.10 || 1.10.15",
+ "phpstan/phpstan-phpunit": "^1.0",
+ "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5",
+ "psalm/plugin-phpunit": "0.18.4",
+ "psr/log": "^1 || ^2 || ^3",
+ "vimeo/psalm": "4.30.0 || 5.12.0"
+ },
+ "suggest": {
+ "psr/log": "Allows logging deprecations via PSR-3 logger implementation"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Doctrine\\Deprecations\\": "lib/Doctrine/Deprecations"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "description": "A small layer on top of trigger_error(E_USER_DEPRECATED) or PSR-3 logging with options to disable all deprecations or selectively for packages.",
+ "homepage": "https://www.doctrine-project.org/",
+ "support": {
+ "issues": "https://github.com/doctrine/deprecations/issues",
+ "source": "https://github.com/doctrine/deprecations/tree/v1.1.1"
+ },
+ "time": "2023-06-03T09:27:29+00:00"
+ },
+ {
+ "name": "symfony/filesystem",
+ "version": "v3.0.9",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/filesystem.git",
+ "reference": "b2da5009d9bacbd91d83486aa1f44c793a8c380d"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/filesystem/zipball/b2da5009d9bacbd91d83486aa1f44c793a8c380d",
+ "reference": "b2da5009d9bacbd91d83486aa1f44c793a8c380d",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.5.9"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "3.0-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\Filesystem\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony Filesystem Component",
+ "homepage": "https://symfony.com",
+ "support": {
+ "source": "https://github.com/symfony/filesystem/tree/3.0"
+ },
+ "time": "2016-07-20T05:43:46+00:00"
+ },
+ {
+ "name": "symfony/polyfill-mbstring",
+ "version": "v1.27.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/polyfill-mbstring.git",
+ "reference": "8ad114f6b39e2c98a8b0e3bd907732c207c2b534"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/8ad114f6b39e2c98a8b0e3bd907732c207c2b534",
+ "reference": "8ad114f6b39e2c98a8b0e3bd907732c207c2b534",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.1"
+ },
+ "provide": {
+ "ext-mbstring": "*"
+ },
+ "suggest": {
+ "ext-mbstring": "For best performance"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "1.27-dev"
+ },
+ "thanks": {
+ "name": "symfony/polyfill",
+ "url": "https://github.com/symfony/polyfill"
+ }
+ },
+ "autoload": {
+ "files": [
+ "bootstrap.php"
+ ],
+ "psr-4": {
+ "Symfony\\Polyfill\\Mbstring\\": ""
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony polyfill for the Mbstring extension",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "compatibility",
+ "mbstring",
+ "polyfill",
+ "portable",
+ "shim"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.27.0"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2022-11-03T14:55:06+00:00"
+ },
+ {
+ "name": "symfony/process",
+ "version": "v3.4.47",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/process.git",
+ "reference": "b8648cf1d5af12a44a51d07ef9bf980921f15fca"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/process/zipball/b8648cf1d5af12a44a51d07ef9bf980921f15fca",
+ "reference": "b8648cf1d5af12a44a51d07ef9bf980921f15fca",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^5.5.9|>=7.0.8"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\Process\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony Process Component",
+ "homepage": "https://symfony.com",
+ "support": {
+ "source": "https://github.com/symfony/process/tree/v3.4.47"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2020-10-24T10:57:07+00:00"
+ }
+ ],
+ "packages-dev": [
+ {
+ "name": "behat/behat",
+ "version": "v2.5.5",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/Behat/Behat.git",
+ "reference": "c1e48826b84669c97a1efa78459aedfdcdcf2120"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/Behat/Behat/zipball/c1e48826b84669c97a1efa78459aedfdcdcf2120",
+ "reference": "c1e48826b84669c97a1efa78459aedfdcdcf2120",
+ "shasum": ""
+ },
+ "require": {
+ "behat/gherkin": "~2.3.0",
+ "php": ">=5.3.1",
+ "symfony/config": "~2.3",
+ "symfony/console": "~2.0",
+ "symfony/dependency-injection": "~2.0",
+ "symfony/event-dispatcher": "~2.0",
+ "symfony/finder": "~2.0",
+ "symfony/translation": "~2.3",
+ "symfony/yaml": "~2.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "~3.7.19"
+ },
+ "suggest": {
+ "behat/mink-extension": "for integration with Mink testing framework",
+ "behat/symfony2-extension": "for integration with Symfony2 web framework",
+ "behat/yii-extension": "for integration with Yii web framework"
+ },
+ "bin": [
+ "bin/behat"
+ ],
+ "type": "library",
+ "autoload": {
+ "psr-0": {
+ "Behat\\Behat": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Konstantin Kudryashov",
+ "email": "ever.zet@gmail.com",
+ "homepage": "http://everzet.com"
+ }
+ ],
+ "description": "Scenario-oriented BDD framework for PHP 5.3",
+ "homepage": "http://behat.org/",
+ "keywords": [
+ "BDD",
+ "Behat",
+ "Symfony2"
+ ],
+ "support": {
+ "issues": "https://github.com/Behat/Behat/issues",
+ "source": "https://github.com/Behat/Behat/tree/v2.5.5"
+ },
+ "time": "2015-06-01T09:37:55+00:00"
+ },
+ {
+ "name": "behat/gherkin",
+ "version": "v2.3.5",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/Behat/Gherkin.git",
+ "reference": "2b33963da5525400573560c173ab5c9c057e1852"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/Behat/Gherkin/zipball/2b33963da5525400573560c173ab5c9c057e1852",
+ "reference": "2b33963da5525400573560c173ab5c9c057e1852",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.3.1",
+ "symfony/finder": "~2.0"
+ },
+ "require-dev": {
+ "symfony/config": "~2.0",
+ "symfony/translation": "~2.0",
+ "symfony/yaml": "~2.0"
+ },
+ "suggest": {
+ "symfony/config": "If you want to use Config component to manage resources",
+ "symfony/translation": "If you want to use Symfony2 translations adapter",
+ "symfony/yaml": "If you want to parse features, represented in YAML files"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-develop": "2.2-dev"
+ }
+ },
+ "autoload": {
+ "psr-0": {
+ "Behat\\Gherkin": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Konstantin Kudryashov",
+ "email": "ever.zet@gmail.com",
+ "homepage": "http://everzet.com"
+ }
+ ],
+ "description": "Gherkin DSL parser for PHP 5.3",
+ "homepage": "http://behat.org/",
+ "keywords": [
+ "BDD",
+ "Behat",
+ "DSL",
+ "Symfony2",
+ "parser"
+ ],
+ "support": {
+ "issues": "https://github.com/Behat/Gherkin/issues",
+ "source": "https://github.com/Behat/Gherkin/tree/2.3"
+ },
+ "time": "2013-10-15T11:22:17+00:00"
+ },
+ {
+ "name": "psr/log",
+ "version": "1.1.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-fig/log.git",
+ "reference": "d49695b909c3b7628b6289db5479a1c204601f11"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-fig/log/zipball/d49695b909c3b7628b6289db5479a1c204601f11",
+ "reference": "d49695b909c3b7628b6289db5479a1c204601f11",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.3.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.1.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Psr\\Log\\": "Psr/Log/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "PHP-FIG",
+ "homepage": "https://www.php-fig.org/"
+ }
+ ],
+ "description": "Common interface for logging libraries",
+ "homepage": "https://github.com/php-fig/log",
+ "keywords": [
+ "log",
+ "psr",
+ "psr-3"
+ ],
+ "support": {
+ "source": "https://github.com/php-fig/log/tree/1.1.4"
+ },
+ "time": "2021-05-03T11:20:27+00:00"
+ },
+ {
+ "name": "symfony/config",
+ "version": "v2.8.52",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/config.git",
+ "reference": "7dd5f5040dc04c118d057fb5886563963eb70011"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/config/zipball/7dd5f5040dc04c118d057fb5886563963eb70011",
+ "reference": "7dd5f5040dc04c118d057fb5886563963eb70011",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.3.9",
+ "symfony/filesystem": "~2.3|~3.0.0",
+ "symfony/polyfill-ctype": "~1.8"
+ },
+ "require-dev": {
+ "symfony/yaml": "~2.7|~3.0.0"
+ },
+ "suggest": {
+ "symfony/yaml": "To use the yaml reference dumper"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.8-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\Config\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony Config Component",
+ "homepage": "https://symfony.com",
+ "support": {
+ "source": "https://github.com/symfony/config/tree/v2.8.50"
+ },
+ "time": "2018-11-26T09:38:12+00:00"
+ },
+ {
+ "name": "symfony/console",
+ "version": "v2.8.52",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/console.git",
+ "reference": "cbcf4b5e233af15cd2bbd50dee1ccc9b7927dc12"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/console/zipball/cbcf4b5e233af15cd2bbd50dee1ccc9b7927dc12",
+ "reference": "cbcf4b5e233af15cd2bbd50dee1ccc9b7927dc12",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.3.9",
+ "symfony/debug": "^2.7.2|~3.0.0",
+ "symfony/polyfill-mbstring": "~1.0"
+ },
+ "require-dev": {
+ "psr/log": "~1.0",
+ "symfony/event-dispatcher": "~2.1|~3.0.0",
+ "symfony/process": "~2.1|~3.0.0"
+ },
+ "suggest": {
+ "psr/log-implementation": "For using the console logger",
+ "symfony/event-dispatcher": "",
+ "symfony/process": ""
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.8-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\Console\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony Console Component",
+ "homepage": "https://symfony.com",
+ "support": {
+ "source": "https://github.com/symfony/console/tree/v2.8.52"
+ },
+ "time": "2018-11-20T15:55:20+00:00"
+ },
+ {
+ "name": "symfony/debug",
+ "version": "v3.0.9",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/debug.git",
+ "reference": "697c527acd9ea1b2d3efac34d9806bf255278b0a"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/debug/zipball/697c527acd9ea1b2d3efac34d9806bf255278b0a",
+ "reference": "697c527acd9ea1b2d3efac34d9806bf255278b0a",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.5.9",
+ "psr/log": "~1.0"
+ },
+ "conflict": {
+ "symfony/http-kernel": ">=2.3,<2.3.24|~2.4.0|>=2.5,<2.5.9|>=2.6,<2.6.2"
+ },
+ "require-dev": {
+ "symfony/class-loader": "~2.8|~3.0",
+ "symfony/http-kernel": "~2.8|~3.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "3.0-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\Debug\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony Debug Component",
+ "homepage": "https://symfony.com",
+ "support": {
+ "source": "https://github.com/symfony/debug/tree/3.0"
+ },
+ "abandoned": "symfony/error-handler",
+ "time": "2016-07-30T07:22:48+00:00"
+ },
+ {
+ "name": "symfony/dependency-injection",
+ "version": "v2.8.52",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/dependency-injection.git",
+ "reference": "c306198fee8f872a8f5f031e6e4f6f83086992d8"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/c306198fee8f872a8f5f031e6e4f6f83086992d8",
+ "reference": "c306198fee8f872a8f5f031e6e4f6f83086992d8",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.3.9"
+ },
+ "conflict": {
+ "symfony/expression-language": "<2.6"
+ },
+ "require-dev": {
+ "symfony/config": "~2.2|~3.0.0",
+ "symfony/expression-language": "~2.6|~3.0.0",
+ "symfony/yaml": "~2.3.42|~2.7.14|~2.8.7|~3.0.7"
+ },
+ "suggest": {
+ "symfony/config": "",
+ "symfony/expression-language": "For using expressions in service container configuration",
+ "symfony/proxy-manager-bridge": "Generate service proxies to lazy load them",
+ "symfony/yaml": ""
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.8-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\DependencyInjection\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony DependencyInjection Component",
+ "homepage": "https://symfony.com",
+ "support": {
+ "source": "https://github.com/symfony/dependency-injection/tree/2.8"
+ },
+ "time": "2019-04-16T11:33:46+00:00"
+ },
+ {
+ "name": "symfony/event-dispatcher",
+ "version": "v2.8.52",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/event-dispatcher.git",
+ "reference": "a77e974a5fecb4398833b0709210e3d5e334ffb0"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/a77e974a5fecb4398833b0709210e3d5e334ffb0",
+ "reference": "a77e974a5fecb4398833b0709210e3d5e334ffb0",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.3.9"
+ },
+ "require-dev": {
+ "psr/log": "~1.0",
+ "symfony/config": "^2.0.5|~3.0.0",
+ "symfony/dependency-injection": "~2.6|~3.0.0",
+ "symfony/expression-language": "~2.6|~3.0.0",
+ "symfony/stopwatch": "~2.3|~3.0.0"
+ },
+ "suggest": {
+ "symfony/dependency-injection": "",
+ "symfony/http-kernel": ""
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.8-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\EventDispatcher\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony EventDispatcher Component",
+ "homepage": "https://symfony.com",
+ "support": {
+ "source": "https://github.com/symfony/event-dispatcher/tree/v2.8.50"
+ },
+ "time": "2018-11-21T14:20:20+00:00"
+ },
+ {
+ "name": "symfony/finder",
+ "version": "v2.8.52",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/finder.git",
+ "reference": "1444eac52273e345d9b95129bf914639305a9ba4"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/finder/zipball/1444eac52273e345d9b95129bf914639305a9ba4",
+ "reference": "1444eac52273e345d9b95129bf914639305a9ba4",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.3.9"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.8-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\Finder\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony Finder Component",
+ "homepage": "https://symfony.com",
+ "support": {
+ "source": "https://github.com/symfony/finder/tree/v2.8.50"
+ },
+ "time": "2018-11-11T11:18:13+00:00"
+ },
+ {
+ "name": "symfony/polyfill-ctype",
+ "version": "v1.27.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/polyfill-ctype.git",
+ "reference": "5bbc823adecdae860bb64756d639ecfec17b050a"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/5bbc823adecdae860bb64756d639ecfec17b050a",
+ "reference": "5bbc823adecdae860bb64756d639ecfec17b050a",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.1"
+ },
+ "provide": {
+ "ext-ctype": "*"
+ },
+ "suggest": {
+ "ext-ctype": "For best performance"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "1.27-dev"
+ },
+ "thanks": {
+ "name": "symfony/polyfill",
+ "url": "https://github.com/symfony/polyfill"
+ }
+ },
+ "autoload": {
+ "files": [
+ "bootstrap.php"
+ ],
+ "psr-4": {
+ "Symfony\\Polyfill\\Ctype\\": ""
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Gert de Pagter",
+ "email": "BackEndTea@gmail.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony polyfill for ctype functions",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "compatibility",
+ "ctype",
+ "polyfill",
+ "portable"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/polyfill-ctype/tree/v1.27.0"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2022-11-03T14:55:06+00:00"
+ },
+ {
+ "name": "symfony/translation",
+ "version": "v2.8.52",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/translation.git",
+ "reference": "fc58c2a19e56c29f5ba2736ec40d0119a0de2089"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/translation/zipball/fc58c2a19e56c29f5ba2736ec40d0119a0de2089",
+ "reference": "fc58c2a19e56c29f5ba2736ec40d0119a0de2089",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.3.9",
+ "symfony/polyfill-mbstring": "~1.0"
+ },
+ "conflict": {
+ "symfony/config": "<2.7"
+ },
+ "require-dev": {
+ "psr/log": "~1.0",
+ "symfony/config": "~2.8",
+ "symfony/intl": "~2.7.25|^2.8.18|~3.2.5",
+ "symfony/yaml": "~2.2|~3.0.0"
+ },
+ "suggest": {
+ "psr/log-implementation": "To use logging capability in translator",
+ "symfony/config": "",
+ "symfony/yaml": ""
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.8-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\Translation\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony Translation Component",
+ "homepage": "https://symfony.com",
+ "support": {
+ "source": "https://github.com/symfony/translation/tree/v2.8.50"
+ },
+ "time": "2018-11-24T21:16:41+00:00"
+ },
+ {
+ "name": "symfony/yaml",
+ "version": "v2.8.52",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/yaml.git",
+ "reference": "02c1859112aa779d9ab394ae4f3381911d84052b"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/yaml/zipball/02c1859112aa779d9ab394ae4f3381911d84052b",
+ "reference": "02c1859112aa779d9ab394ae4f3381911d84052b",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.3.9",
+ "symfony/polyfill-ctype": "~1.8"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.8-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\Yaml\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony Yaml Component",
+ "homepage": "https://symfony.com",
+ "support": {
+ "source": "https://github.com/symfony/yaml/tree/v2.8.52"
+ },
+ "time": "2018-11-11T11:18:13+00:00"
+ }
+ ],
+ "aliases": [],
+ "minimum-stability": "stable",
+ "stability-flags": [],
+ "prefer-stable": false,
+ "prefer-lowest": false,
+ "platform": [],
+ "platform-dev": [],
+ "plugin-api-version": "2.3.0"
+}
diff --git a/inc/site-exporter/mu-migration/features/01-load-wp-cli.feature b/inc/site-exporter/mu-migration/features/01-load-wp-cli.feature
new file mode 100644
index 00000000..035b8673
--- /dev/null
+++ b/inc/site-exporter/mu-migration/features/01-load-wp-cli.feature
@@ -0,0 +1,10 @@
+Feature: Test that WP-CLI loads.
+
+ Scenario: WP-CLI loads for your tests
+ Given a WP install
+
+ When I run `wp eval 'echo "Hello world.";'`
+ Then STDOUT should contain:
+ """
+ Hello world.
+ """
diff --git a/inc/site-exporter/mu-migration/features/02-info.feature b/inc/site-exporter/mu-migration/features/02-info.feature
new file mode 100644
index 00000000..aadd1422
--- /dev/null
+++ b/inc/site-exporter/mu-migration/features/02-info.feature
@@ -0,0 +1,12 @@
+Feature: Test an MU-Migration info command.
+
+ Scenario: MU-Migration info works
+ Given a WP install
+ When I run `wp mu-migration info`
+ Then STDOUT should contain:
+ """
+MU-Migration version: %Yv{MU_MIGRATION_VERSION}%n
+
+Created by Nícholas André at 10up
+Github: https://github.com/10up/MU-Migration
+ """
diff --git a/inc/site-exporter/mu-migration/features/03-export.feature b/inc/site-exporter/mu-migration/features/03-export.feature
new file mode 100644
index 00000000..8be8d980
--- /dev/null
+++ b/inc/site-exporter/mu-migration/features/03-export.feature
@@ -0,0 +1,341 @@
+Feature: Test MU-Migration export commands.
+
+ Scenario: MU-Migration is able to export the users of a single site
+ Given a WP install
+
+ When I run `wp user list --format=count`
+ And save STDOUT as {USERS_COUNT}
+
+ When I run `wp mu-migration export users users.csv`
+ Then the users.csv file should exist
+ Then STDOUT should be:
+ """
+ Success: {USERS_COUNT} users have been exported
+ """
+
+ When I run `cat users.csv`
+ Then STDOUT should be CSV containing:
+ | ID | user_login | user_email | role |
+ | 1 | admin | admin@example.com | administrator |
+
+ When I run `wp eval-file {SRC_DIR}/features/tests/csv_matches_user.php users.csv`
+ Then STDOUT should be:
+ """
+ Success
+ """
+ Scenario: MU-Migration is able to export users of a subsite in multisite
+ Given a WP multisite subdirectory install
+ Given I create multiple sites with dummy content
+
+ When I run `wp user list --format=count --url=example.com/site-2`
+ And save STDOUT as {USERS_COUNT}
+
+ When I run `wp mu-migration export users users-subsite.csv --blog_id=2`
+ Then the users-subsite.csv file should exist
+ Then STDOUT should be:
+ """
+ Success: {USERS_COUNT} users have been exported
+ """
+
+ When I run `cat users-subsite.csv`
+ Then STDOUT should be CSV containing:
+ | ID | user_login | user_email | role |
+ | 1 | admin | admin@example.com | administrator |
+
+ When I run `wp eval-file {SRC_DIR}/features/tests/csv_matches_user.php users-subsite.csv --url=example.com/site-2`
+ Then STDOUT should be:
+ """
+ Success
+ """
+
+ Scenario: MU-Migration is able to export tables of a single site
+ Given a WP install
+
+ When I run `wp db prefix`
+ And save STDOUT as {DB_PREFIX}
+
+ When I run `wp mu-migration export tables tables.sql`
+ Then the tables.sql file should exist
+ Then the tables.sql file should contain:
+ """
+ CREATE TABLE `{DB_PREFIX}posts` |AND|
+ CREATE TABLE `{DB_PREFIX}postmeta` |AND|
+ CREATE TABLE `{DB_PREFIX}terms` |AND|
+ CREATE TABLE `{DB_PREFIX}termmeta` |AND|
+ CREATE TABLE `{DB_PREFIX}options` |AND|
+ CREATE TABLE `{DB_PREFIX}comments` |AND|
+ CREATE TABLE `{DB_PREFIX}commentmeta` |AND|
+ CREATE TABLE `{DB_PREFIX}term_taxonomy` |AND|
+ CREATE TABLE `{DB_PREFIX}term_relationships`
+ """
+ Then the tables.sql file should not contain:
+ """
+ CREATE TABLE `{DB_PREFIX}users` |AND|
+ CREATE TABLE `{DB_PREFIX}usermeta`
+ """
+ Then STDOUT should be:
+ """
+ Success: The export is now complete
+ """
+
+ When I run `wp mu-migration export tables tables1.sql --tables={DB_PREFIX}posts`
+ Then the tables1.sql file should contain:
+ """
+ CREATE TABLE `{DB_PREFIX}posts`
+ """
+ Then the tables1.sql file should not contain:
+ """
+ CREATE TABLE `{DB_PREFIX}postmeta` |AND|
+ CREATE TABLE `{DB_PREFIX}terms` |AND|
+ CREATE TABLE `{DB_PREFIX}termmeta` |AND|
+ CREATE TABLE `{DB_PREFIX}options` |AND|
+ CREATE TABLE `{DB_PREFIX}comments` |AND|
+ CREATE TABLE `{DB_PREFIX}commentmeta` |AND|
+ CREATE TABLE `{DB_PREFIX}term_taxonomy` |AND|
+ CREATE TABLE `{DB_PREFIX}term_relationships`
+ """
+
+ When I run `wp db query "CREATE TABLE {DB_PREFIX}custom_table (ID int, text longtext)"`
+ And I run `wp db query "CREATE TABLE custom_table_no_prefix (ID int, text longtext)"`
+ And I run `wp mu-migration export tables tables2.sql --non-default-tables={DB_PREFIX}custom_table,custom_table_no_prefix`
+ Then the tables2.sql file should contain:
+ """
+ CREATE TABLE `{DB_PREFIX}custom_table` |AND|
+ CREATE TABLE `custom_table_no_prefix` |AND|
+ """
+ Scenario: MU-Migration is able to export tables for subsites in Multisite
+ Given a WP multisite subdirectory install
+ Given I create multiple sites with dummy content
+
+ When I run `wp db prefix --url=example.com/site-3`
+ And save STDOUT as {DB_PREFIX}
+
+ When I run `wp mu-migration export tables tables-subsite.sql --blog_id=3`
+ Then the tables-subsite.sql file should exist
+ Then the tables-subsite.sql file should contain:
+ """
+ CREATE TABLE `{DB_PREFIX}posts` |AND|
+ CREATE TABLE `{DB_PREFIX}postmeta` |AND|
+ CREATE TABLE `{DB_PREFIX}terms` |AND|
+ CREATE TABLE `{DB_PREFIX}termmeta` |AND|
+ CREATE TABLE `{DB_PREFIX}options` |AND|
+ CREATE TABLE `{DB_PREFIX}comments` |AND|
+ CREATE TABLE `{DB_PREFIX}commentmeta` |AND|
+ CREATE TABLE `{DB_PREFIX}term_taxonomy` |AND|
+ CREATE TABLE `{DB_PREFIX}term_relationships` |AND|
+ """
+ Then the tables-subsite.sql file should not contain:
+ """
+ CREATE TABLE `{DB_PREFIX}users` |AND|
+ CREATE TABLE `{DB_PREFIX}usermeta` |AND|
+ CREATE TABLE `{DB_PREFIX}blog_versions` |AND|
+ CREATE TABLE `{DB_PREFIX}blogs` |AND|
+ CREATE TABLE `{DB_PREFIX}site` |AND|
+ CREATE TABLE `{DB_PREFIX}sitemeta` |AND|
+ CREATE TABLE `{DB_PREFIX}registration_log` |AND|
+ CREATE TABLE `{DB_PREFIX}signups`
+ """
+ Then STDOUT should be:
+ """
+ Success: The export is now complete
+ """
+
+ When I run `wp mu-migration export tables tables-subsite1.sql --tables={DB_PREFIX}posts --blog_id=3`
+ Then the tables-subsite1.sql file should contain:
+ """
+ CREATE TABLE `{DB_PREFIX}posts`
+ """
+ Then the tables-subsite1.sql file should not contain:
+ """
+ CREATE TABLE `{DB_PREFIX}postmeta` |AND|
+ CREATE TABLE `{DB_PREFIX}terms` |AND|
+ CREATE TABLE `{DB_PREFIX}termmeta` |AND|
+ CREATE TABLE `{DB_PREFIX}options` |AND|
+ CREATE TABLE `{DB_PREFIX}comments` |AND|
+ CREATE TABLE `{DB_PREFIX}commentmeta` |AND|
+ CREATE TABLE `{DB_PREFIX}term_taxonomy` |AND|
+ CREATE TABLE `{DB_PREFIX}term_relationships`
+ """
+ When I run `wp db query "CREATE TABLE {DB_PREFIX}custom_table (ID int, text longtext)"`
+ And I run `wp db query "CREATE TABLE custom_table_no_prefix (ID int, text longtext)"`
+ And I run `wp mu-migration export tables tables-subsite2.sql --non-default-tables={DB_PREFIX}custom_table,custom_table_no_prefix`
+ Then the tables-subsite2.sql file should contain:
+ """
+ CREATE TABLE `{DB_PREFIX}custom_table` |AND|
+ CREATE TABLE `custom_table_no_prefix` |AND|
+ """
+
+ Scenario: MU-Migration is able to export a single site into a zip package without themes,plugins and uploads
+ Given a WP install
+
+ When I run `wp db prefix`
+ And save STDOUT as {DB_PREFIX}
+ And I run `wp plugin install jetpack --activate`
+
+ When I run `wp mu-migration export all single-site.zip`
+ Then STDOUT should be:
+ """
+ Exporting site meta data...
+ Exporting users...
+ Exporting tables
+ Zipping files....
+ Success: A zip file named single-site.zip has been created
+ """
+
+ When I run `unzip single-site.zip -d temp_folder`
+ And I run `ls temp_folder/ | grep .csv`
+ And save STDOUT as {MU_CSV_FILE}
+ And I run `ls temp_folder/ | grep .json`
+ And save STDOUT as {MU_JSON_FILE}
+ And I run `ls temp_folder/ | grep .sql`
+ And save STDOUT as {MU_SQL_FILE}
+ Then the single-site.zip file should exist
+ Then the temp_folder/{MU_CSV_FILE} file should exist
+ Then the temp_folder/{MU_JSON_FILE} file should exist
+ Then the temp_folder/{MU_SQL_FILE} file should exist
+ Then the temp_folder/{MU_SQL_FILE} file should contain:
+ """
+ CREATE TABLE `{DB_PREFIX}posts` |AND|
+ CREATE TABLE `{DB_PREFIX}postmeta` |AND|
+ CREATE TABLE `{DB_PREFIX}terms` |AND|
+ CREATE TABLE `{DB_PREFIX}termmeta` |AND|
+ CREATE TABLE `{DB_PREFIX}options` |AND|
+ CREATE TABLE `{DB_PREFIX}comments` |AND|
+ CREATE TABLE `{DB_PREFIX}commentmeta` |AND|
+ CREATE TABLE `{DB_PREFIX}term_taxonomy` |AND|
+ CREATE TABLE `{DB_PREFIX}term_relationships`
+ """
+
+ When I run `wp eval-file {SRC_DIR}/features/tests/csv_matches_user.php temp_folder/{MU_CSV_FILE}`
+ Then STDOUT should be:
+ """
+ Success
+ """
+ When I run `cat temp_folder/{MU_JSON_FILE}`
+ Then STDOUT should be JSON containing:
+ """
+ {
+ "url": "http:\/\/example.com",
+ "name": "WP CLI Site",
+ "admin_email":"admin@example.com",
+ "site_language":"en-US",
+ "db_prefix": "{DB_PREFIX}",
+ "blog_id": 1,
+ "blog_plugins": ["jetpack\/jetpack.php"],
+ "network_plugins": []
+ }
+ """
+ Scenario: MU-Migration is able to export a single site into a zip package with themes,plugins and uploads
+ Given a WP install
+
+ When I run `wp db prefix`
+ And save STDOUT as {DB_PREFIX}
+ And I run `wp plugin install jetpack --activate`
+ And I run `wp mu-migration export all single-site.zip --themes --plugins --uploads`
+ Then the single-site.zip file should exist
+
+ When I run `unzip single-site.zip -d temp_folder`
+ And I run `ls temp_folder/ | grep .csv`
+ And save STDOUT as {MU_CSV_FILE}
+ And I run `ls temp_folder/ | grep .json`
+ And save STDOUT as {MU_JSON_FILE}
+ And I run `ls temp_folder/ | grep .sql`
+ And save STDOUT as {MU_SQL_FILE}
+ Then the temp_folder/{MU_CSV_FILE} file should exist
+ Then the temp_folder/{MU_JSON_FILE} file should exist
+ Then the temp_folder/{MU_SQL_FILE} file should exist
+ Then the temp_folder/wp-content directory should exist
+ Then the temp_folder/wp-content/themes directory should exist
+ Then the temp_folder/wp-content/plugins directory should exist
+ Then the temp_folder/wp-content/plugins/jetpack directory should exist
+ Then the temp_folder/wp-content/uploads directory should exist
+
+ Scenario: MU-Migration is able to export a subsite without themes, plugins and uploads
+ Given a WP multisite subdirectory install
+ Given I create multiple sites with dummy content
+
+ When I run `wp db prefix --url=example.com/site-2`
+ And save STDOUT as {DB_PREFIX}
+ And I run `wp plugin install jetpack shortcode-ui --activate --url=example.com/site-2`
+
+ When I run `wp mu-migration export all subsite-2.zip --blog_id=2`
+ Then STDOUT should be:
+ """
+ Exporting site meta data...
+ Exporting users...
+ Exporting tables
+ Zipping files....
+ Success: A zip file named subsite-2.zip has been created
+ """
+
+ When I run `unzip subsite-2.zip -d temp_folder_subsite`
+ And I run `ls temp_folder_subsite/ | grep .csv`
+ And save STDOUT as {MU_CSV_FILE}
+ And I run `ls temp_folder_subsite/ | grep .json`
+ And save STDOUT as {MU_JSON_FILE}
+ And I run `ls temp_folder_subsite/ | grep .sql`
+ And save STDOUT as {MU_SQL_FILE}
+ Then the subsite-2.zip file should exist
+ Then the temp_folder_subsite/{MU_CSV_FILE} file should exist
+ Then the temp_folder_subsite/{MU_JSON_FILE} file should exist
+ Then the temp_folder_subsite/{MU_SQL_FILE} file should exist
+ Then the temp_folder_subsite/{MU_SQL_FILE} file should contain:
+ """
+ CREATE TABLE `{DB_PREFIX}posts` |AND|
+ CREATE TABLE `{DB_PREFIX}postmeta` |AND|
+ CREATE TABLE `{DB_PREFIX}terms` |AND|
+ CREATE TABLE `{DB_PREFIX}termmeta` |AND|
+ CREATE TABLE `{DB_PREFIX}options` |AND|
+ CREATE TABLE `{DB_PREFIX}comments` |AND|
+ CREATE TABLE `{DB_PREFIX}commentmeta` |AND|
+ CREATE TABLE `{DB_PREFIX}term_taxonomy` |AND|
+ CREATE TABLE `{DB_PREFIX}term_relationships`
+ """
+
+ When I run `wp eval-file {SRC_DIR}/features/tests/csv_matches_user.php temp_folder_subsite/{MU_CSV_FILE} --url=example.com/site-2`
+ Then STDOUT should be:
+ """
+ Success
+ """
+ When I run `cat temp_folder_subsite/{MU_JSON_FILE}`
+ Then STDOUT should be JSON containing:
+ """
+ {
+ "url": "http:\/\/example.com/site-2",
+ "name": "Site 2",
+ "admin_email":"admin@example.com",
+ "site_language":"en-US",
+ "db_prefix": "{DB_PREFIX}",
+ "blog_id": 2,
+ "blog_plugins": ["jetpack\/jetpack.php", "shortcode-ui\/shortcode-ui.php"],
+ "network_plugins": []
+ }
+ """
+ Scenario: MU-Migration is able to export a subsite into a zip package with themes,plugins and uploads
+ Given a WP multisite subdirectory install
+ Given I create multiple sites with dummy content
+
+ When I run `wp db prefix`
+ And save STDOUT as {DB_PREFIX}
+ And I run `wp plugin install jetpack --activate --url=example.com/site-2`
+ And I run `wp media import {SRC_DIR}/features/data/images/*.jpg --url=example.com/site-2`
+ And I run `wp mu-migration export all subsite-2.zip --themes --plugins --uploads`
+ Then the subsite-2.zip file should exist
+
+ When I run `unzip subsite-2.zip -d temp_folder_subsite`
+ And I run `ls temp_folder_subsite/ | grep .csv`
+ And save STDOUT as {MU_CSV_FILE}
+ And I run `ls temp_folder_subsite/ | grep .json`
+ And save STDOUT as {MU_JSON_FILE}
+ And I run `ls temp_folder_subsite/ | grep .sql`
+ And save STDOUT as {MU_SQL_FILE}
+ Then the temp_folder_subsite/{MU_CSV_FILE} file should exist
+ Then the temp_folder_subsite/{MU_JSON_FILE} file should exist
+ Then the temp_folder_subsite/{MU_SQL_FILE} file should exist
+ Then the temp_folder_subsite/wp-content directory should exist
+ Then the temp_folder_subsite/wp-content/themes directory should exist
+ Then the temp_folder_subsite/wp-content/plugins directory should exist
+ Then the temp_folder_subsite/wp-content/plugins/jetpack directory should exist
+ Then the temp_folder_subsite/wp-content/uploads directory should exist
+ Then the temp_folder_subsite/wp-content/uploads/sites/2 directory should exist
+
diff --git a/inc/site-exporter/mu-migration/features/04-import.feature b/inc/site-exporter/mu-migration/features/04-import.feature
new file mode 100644
index 00000000..e703a94a
--- /dev/null
+++ b/inc/site-exporter/mu-migration/features/04-import.feature
@@ -0,0 +1,189 @@
+Feature: Test MU-Migration import commands.
+
+ Scenario: MU-Migration is able to import the users from a single site into a subsite
+ Given a WP multisite subdirectory install
+ Given I create multiple sites with dummy content
+ Given a WP install in 'singlesite/'
+
+ When I run `wp user generate --count=100 --path=singlesite`
+ And I run `wp user list --format=count --path=singlesite`
+ And save STDOUT as {SINGLE_SITE_USERS_COUNT}
+ And I run `wp mu-migration export users users.csv --path=singlesite`
+ Then the users.csv file should exist
+ Then STDOUT should be:
+ """
+ Success: {SINGLE_SITE_USERS_COUNT} users have been exported
+ """
+ When I run `wp user list --format=count --url=example.com/site-2`
+ Then STDOUT should be:
+ """
+ 11
+ """
+ When I run `wp mu-migration import users users.csv --blog_id=2 --map_file=users-mapping.json`
+ Then STDOUT should contain:
+ """
+ Parsing users.csv...
+ Success: A map file has been created: users-mapping.json
+ Success: 90 users have been imported and 11 users already existed
+ """
+ Then the users-mapping.json file should exist
+ When I run `wp eval-file {SRC_DIR}/features/tests/csv_matches_user.php users.csv users-mapping.json --url=example.com/site-2`
+ Then STDOUT should be:
+ """
+ Success
+ """
+ Scenario: MU-Migration is able to import the users from a subsite into a single site
+ Given a WP multisite subdirectory install
+ Given I create multiple sites with dummy content
+ Given a WP install in 'singlesite/'
+
+ When I run `wp user list --format=count --blog_id=2`
+ And save STDOUT as {SUBSITE_USERS_COUNT}
+ And I run `wp mu-migration export users users.csv --blog_id=2`
+ Then the users.csv file should exist
+ Then STDOUT should be:
+ """
+ Success: {SUBSITE_USERS_COUNT} users have been exported
+ """
+ When I run `wp user list --format=count --path=singlesite`
+ Then STDOUT should be:
+ """
+ 1
+ """
+ When I run `wp mu-migration import users users.csv --path=singlesite --map_file=users-mapping.json`
+ Then STDOUT should contain:
+ """
+ Parsing users.csv...
+ Success: A map file has been created: users-mapping.json
+ Success: 10 users have been imported and 1 users already existed
+ """
+ Then the users-mapping.json file should exist
+ When I run `wp eval-file {SRC_DIR}/features/tests/csv_matches_user.php users.csv users-mapping.json --path=singlesite`
+ Then STDOUT should be:
+ """
+ Success
+ """
+ Scenario: MU-Migration is able to import the users from a subsite into another subsite
+ Given a WP multisite subdirectory install
+ Given I create multiple sites with dummy content
+
+ When I run `wp user list --format=count --blog_id=2`
+ And save STDOUT as {SUBSITE_USERS_COUNT}
+ And I run `wp mu-migration export users users.csv --blog_id=2`
+ Then the users.csv file should exist
+ Then STDOUT should be:
+ """
+ Success: {SUBSITE_USERS_COUNT} users have been exported
+ """
+ When I run `wp user list --format=count --url=example.com/site-3`
+ Then STDOUT should be:
+ """
+ 11
+ """
+ When I run `wp mu-migration import users users.csv --blog_id=3 --map_file=users-mapping.json`
+ Then STDOUT should contain:
+ """
+ Parsing users.csv...
+ Success: A map file has been created: users-mapping.json
+ Success: 0 users have been imported and 11 users already existed
+ """
+ Then the users-mapping.json file should exist
+ When I run `wp eval-file {SRC_DIR}/features/tests/csv_matches_user.php users.csv users-mapping.json --url=example.com/site-3`
+ Then STDOUT should be:
+ """
+ Success
+ """
+ Scenario: MU-Migration is able to import tables from single site into a subsite
+ Given a WP multisite subdirectory install
+ Given I create multiple sites with dummy content
+ Given a WP install in 'singlesite/'
+
+ When I run `wp post create --post_type=page --post_title='Test Post' --post_status=publish --post_date='2016-12-01 07:00:00' --path=singlesite`
+ And I run `wp mu-migration export tables tables.sql --path=singlesite`
+ And I run `wp db prefix --path=singlesite`
+ And save STDOUT as {DB_PREFIX}
+ And I run `wp db prefix --url=example.com/site-2`
+ And save STDOUT as {SUB_DB_PREFIX}
+ And I run `wp mu-migration import tables tables.sql --blog_id=2 --old_prefix={DB_PREFIX} --new_prefix={SUB_DB_PREFIX} --old_url=http://singlesite.com --new_url=http://example.com/site-2`
+ Then STDOUT should be:
+ """
+ Database imported
+ Running search-replace
+ Search and Replace has been successfully executed
+ Running Search and Replace for uploads paths
+ Uploads paths have been successfully updated: wp-content/uploads -> wp-content/uploads/sites/2
+ """
+
+ When I run `wp option get siteurl --url=http://example.com/site-2`
+ Then STDOUT should be:
+ """
+ http://example.com/site-2
+ """
+ Scenario: MU-Migration is able to import tables from a subsite into another subsite
+ Given a WP multisite subdirectory install
+ Given I create multiple sites with dummy content
+
+ When I run `wp mu-migration export tables tables.sql --blog_id=3`
+ And I run `wp db prefix --url=example.com/site-3`
+ And save STDOUT as {DB_PREFIX}
+ And I run `wp db prefix --url=example.com/site-2`
+ And save STDOUT as {SUB_DB_PREFIX}
+ And I run `wp mu-migration import tables tables.sql --blog_id=2 --original_blog_id=3 --old_prefix={DB_PREFIX} --new_prefix={SUB_DB_PREFIX} --old_url=http://example.com/site-3 --new_url=http://example.com/site-2`
+ Then STDOUT should be:
+ """
+ Database imported
+ Running search-replace
+ Search and Replace has been successfully executed
+ Running Search and Replace for uploads paths
+ Uploads paths have been successfully updated: wp-content/uploads/sites/3 -> wp-content/uploads/sites/2
+ """
+ When I run `wp option get siteurl --url=http://example.com/site-2`
+ Then STDOUT should be:
+ """
+ http://example.com/site-2
+ """
+
+ When I run `wp mu-migration export tables tables.sql --blog_id=3`
+ And I run `wp db prefix --url=example.com/site-3`
+ And save STDOUT as {DB_PREFIX}
+ And I run `wp db prefix --url=example.com`
+ And save STDOUT as {SUB_DB_PREFIX}
+ And I run `wp mu-migration import tables tables.sql --blog_id=1 --original_blog_id=3 --old_prefix={DB_PREFIX} --new_prefix={SUB_DB_PREFIX} --old_url=http://example.com/site-3 --new_url=http://example.com`
+ Then STDOUT should be:
+ """
+ Database imported
+ Running search-replace
+ Search and Replace has been successfully executed
+ Running Search and Replace for uploads paths
+ Uploads paths have been successfully updated: wp-content/uploads/sites/3 -> wp-content/uploads
+ """
+ When I run `wp option get siteurl --url=http://example.com/`
+ Then STDOUT should be:
+ """
+ http://example.com
+ """
+
+ Scenario: MU-Migration is able to import tables from a subsite into a single site
+ Given a WP multisite subdirectory install
+ Given I create multiple sites with dummy content
+ Given a WP install in 'singlesite/'
+
+ When I run `wp mu-migration export tables tables.sql --blog_id=3`
+ And I run `wp db prefix --url=example.com/site-3`
+ And save STDOUT as {DB_PREFIX}
+ And I run `wp db prefix --path=singlesite`
+ And save STDOUT as {SINGLE_DB_PREFIX}
+ And I run `wp mu-migration import tables tables.sql --blog_id=1 --original_blog_id=3 --old_prefix={DB_PREFIX} --new_prefix={SINGLE_DB_PREFIX} --old_url=http://example.com/site-3 --new_url=http://singlesite.com --path=singlesite`
+ Then STDOUT should be:
+ """
+ Database imported
+ Running search-replace
+ Search and Replace has been successfully executed
+ Running Search and Replace for uploads paths
+ Uploads paths have been successfully updated: wp-content/uploads/sites/3 -> wp-content/uploads
+ """
+ When I run `wp option get siteurl --path=singlesite`
+ Then STDOUT should be:
+ """
+ http://singlesite.com
+ """
\ No newline at end of file
diff --git a/inc/site-exporter/mu-migration/features/05-posts.feature b/inc/site-exporter/mu-migration/features/05-posts.feature
new file mode 100644
index 00000000..64ad7ac7
--- /dev/null
+++ b/inc/site-exporter/mu-migration/features/05-posts.feature
@@ -0,0 +1,37 @@
+Feature: Test MU-Migration posts command.
+
+ Scenario: MU-Migration is able to import the users and tables from a single site into a subsite and update the authors
+ Given a WP multisite subdirectory install
+ Given I create multiple sites with dummy content
+ Given a WP install in 'singlesite/'
+
+ When I run `wp user create ann ann@example.com --path=singlesite`
+ And I run `wp post generate --count=10 --post_type=post --post_author=ann --path=singlesite`
+ And I insert arbitrary UID postmeta data for user "ann@example.com" in site "singlesite"
+ And I run `wp mu-migration export users users.csv --path=singlesite`
+ And I run `wp mu-migration export tables tables.sql --path=singlesite`
+ And I run `wp mu-migration import users users.csv --blog_id=2 --map_file=users-mapping.json`
+ Then the users-mapping.json file should exist
+
+ When I run `wp db prefix --path=singlesite`
+ And save STDOUT as {DB_PREFIX}
+ And I run `wp db prefix --url=example.com/site-2`
+ And save STDOUT as {SUB_DB_PREFIX}
+ And I run `wp mu-migration import tables tables.sql --blog_id=2 --old_prefix={DB_PREFIX} --new_prefix={SUB_DB_PREFIX} --old_url=http://singlesite.com --new_url=http://example.com/site-2`
+ And I run `wp mu-migration posts update_author users-mapping.json --blog_id=2 --uid_fields='_a_userid_field'`
+ Then STDOUT should not contain:
+ """
+ records failed to update its post_author:
+ """
+
+ When I run `wp user get $(wp post get 5 --url=example.com/site-2 --field=post_author) --field=login`
+ Then STDOUT should be:
+ """
+ ann
+ """
+
+ When I run `wp user get $(wp post meta get 5 --url=example.com/site-2 _a_userid_field) --field=login`
+ Then STDOUT should be:
+ """
+ ann
+ """
diff --git a/inc/site-exporter/mu-migration/features/06-migration.feature b/inc/site-exporter/mu-migration/features/06-migration.feature
new file mode 100644
index 00000000..d9186676
--- /dev/null
+++ b/inc/site-exporter/mu-migration/features/06-migration.feature
@@ -0,0 +1,126 @@
+Feature: MU-Migration import all command
+ Scenario: MU-Migration is able to export a single site into a zip package with themes,plugins and uploads and import into a Multisite Network
+ Given a WP multisite subdirectory install
+ Given I create multiple sites with dummy content
+ Given a WP install in 'singlesite/'
+
+ When I run `wp theme install pixgraphy --activate --path=singlesite`
+ And I run `wp plugin install jetpack --activate --path=singlesite`
+ And I run `wp plugin install shortcode-ui --path=singlesite`
+ And I run `wp media import {SRC_DIR}/features/data/images/*.jpg --path=singlesite`
+ And I run `wp mu-migration export all single-site.zip --themes --plugins --uploads --path=singlesite`
+ Then the single-site.zip file should exist
+
+ When I run `wp mu-migration import all single-site.zip --new_url=http://singlesite2.com`
+ And I run `wp site list --fields=blog_id,url`
+ Then STDOUT should be a table containing rows:
+ | blog_id | url |
+ | 4 | http://singlesite2.com/ |
+
+ When I run `wp mu-migration import all single-site.zip --blog_id=4`
+ And I run `wp site list --fields=blog_id,url`
+ Then STDOUT should be a table containing rows:
+ | blog_id | url |
+ | 4 | http://singlesite.com/ |
+
+ When I run `wp mu-migration import all single-site.zip --blog_id=4 --new_url=http://singlesite2.com --verbose`
+ Then STDOUT should contain:
+ """
+ Uploads paths have been successfully updated: wp-content/uploads -> wp-content/uploads/sites/4 |AND|
+ Success: All done, your new site is available at http://singlesite2.com. Remember to flush the cache (memcache, redis etc).
+ """
+
+ When I run `wp site list --fields=blog_id,url`
+ Then STDOUT should be a table containing rows:
+ | blog_id | url |
+ | 4 | http://singlesite2.com/ |
+
+ Then the wp-content/themes/pixgraphy directory should exist
+ Then the wp-content/plugins/jetpack directory should exist
+ Then the wp-content/plugins/shortcode-ui directory should not exist
+
+ When I run `wp theme list --status=active --field=name --url=singlesite2.com`
+ Then STDOUT should be:
+ """
+ pixgraphy
+ """
+ When I run `wp plugin list --status-active --field=name --url=singlesite2.com`
+ Then STDOUT should contain:
+ """
+ jetpack
+ """
+ Scenario: MU-Migration is able to export a subsite into a zip package with themes,plugins and uploads and import into a single site
+ Given a WP multisite subdirectory install
+ Given I create multiple sites with dummy content
+ Given a WP install in 'singlesite/'
+
+ When I run `wp theme install pixgraphy --activate --url=example.com/site-2`
+ And I run `wp plugin install jetpack --activate --url=example.com/site-2`
+ And I run `wp plugin install shortcode-ui --activate-network`
+ And I run `wp media import {SRC_DIR}/features/data/images/*.jpg --url=example.com/site-2`
+ And I run `wp mu-migration export all sub-site.zip --themes --plugins --uploads --blog_id=2`
+ Then the sub-site.zip file should exist
+
+ When I run `wp mu-migration import all sub-site.zip --new_url=http://subsite.com --verbose --path=singlesite`
+ Then STDOUT should contain:
+ """
+ Uploads paths have been successfully updated: wp-content/uploads/sites/2 -> wp-content/uploads |AND|
+ Success: All done, your new site is available at http://subsite.com. Remember to flush the cache (memcache, redis etc).
+ """
+ Then the wp-content/themes/pixgraphy directory should exist
+ Then the wp-content/plugins/jetpack directory should exist
+
+ When I run `wp option get siteurl --path=singlesite`
+ Then STDOUT should be:
+ """
+ http://subsite.com
+ """
+
+ When I run `wp theme list --status=active --field=name --path=singlesite`
+ Then STDOUT should be:
+ """
+ pixgraphy
+ """
+ When I run `wp plugin list --status-active --field=name --path=singlesite`
+ Then STDOUT should contain:
+ """
+ jetpack |AND|
+ shortcode-ui
+ """
+ Scenario: MU-Migration is able to export a subsite into a zip package with themes,plugins and uploads and import into another subsite
+ Given a WP multisite subdirectory install
+ Given I create multiple sites with dummy content
+
+ When I run `wp theme install pixgraphy --activate --url=example.com/site-2`
+ And I run `wp plugin install jetpack --activate --url=example.com/site-2`
+ And I run `wp plugin install shortcode-ui --activate-network`
+ And I run `wp media import {SRC_DIR}/features/data/images/*.jpg --url=example.com/site-2`
+ And I run `wp mu-migration export all sub-site.zip --themes --plugins --uploads --blog_id=2`
+ Then the sub-site.zip file should exist
+
+ When I run `wp mu-migration import all sub-site.zip --blog_id=1 --new_url=http://example.com --verbose`
+ Then STDOUT should contain:
+ """
+ Uploads paths have been successfully updated: wp-content/uploads/sites/2 -> wp-content/uploads |AND|
+ Success: All done, your new site is available at http://example.com. Remember to flush the cache (memcache, redis etc).
+ """
+ Then the wp-content/themes/pixgraphy directory should exist
+ Then the wp-content/plugins/jetpack directory should exist
+
+ When I run `wp option get siteurl`
+ Then STDOUT should be:
+ """
+ http://example.com
+ """
+
+ When I run `wp theme list --status=active --field=name`
+ Then STDOUT should be:
+ """
+ pixgraphy
+ """
+ When I run `wp plugin list --status-active --field=name`
+ Then STDOUT should contain:
+ """
+ jetpack |AND|
+ shortcode-ui
+ """
\ No newline at end of file
diff --git a/inc/site-exporter/mu-migration/features/bootstrap/FeatureContext.php b/inc/site-exporter/mu-migration/features/bootstrap/FeatureContext.php
new file mode 100644
index 00000000..cc1f5efe
--- /dev/null
+++ b/inc/site-exporter/mu-migration/features/bootstrap/FeatureContext.php
@@ -0,0 +1,946 @@
+autoload->files) ) {
+ $contents = 'require:' . PHP_EOL;
+ foreach ( $composer->autoload->files as $file ) {
+ $contents .= ' - ' . dirname(dirname(__DIR__)) . '/' . $file . PHP_EOL;
+ }
+ @mkdir(sys_get_temp_dir() . '/wp-cli-package-test/');
+ $project_config = sys_get_temp_dir() . '/wp-cli-package-test/config.yml';
+ file_put_contents($project_config, $contents);
+ putenv('WP_CLI_CONFIG_PATH=' . $project_config);
+ }
+ }
+ // Inside WP-CLI
+} else {
+ require_once __DIR__ . '/../../php/utils.php';
+ require_once __DIR__ . '/../../php/WP_CLI/Process.php';
+ require_once __DIR__ . '/../../php/WP_CLI/ProcessRun.php';
+ if ( file_exists(__DIR__ . '/../../vendor/autoload.php') ) {
+ require_once __DIR__ . '/../../vendor/autoload.php';
+ } elseif ( file_exists(__DIR__ . '/../../../../autoload.php') ) {
+ require_once __DIR__ . '/../../../../autoload.php';
+ }
+}
+
+/**
+ * Features context.
+ */
+class FeatureContext extends BehatContext implements ClosuredContextInterface {
+
+ /**
+ * The current working directory for scenarios that have a "Given a WP install" or "Given an empty directory" step. Variable RUN_DIR. Lives until the end of the scenario.
+ */
+ private static $run_dir;
+
+ /**
+ * Where WordPress core is downloaded to for caching, and which is copied to RUN_DIR during a "Given a WP install" step. Lives until manually deleted.
+ */
+ private static $cache_dir;
+
+ /**
+ * The directory that holds the install cache, and which is copied to RUN_DIR during a "Given a WP install" step. Recreated on each suite run.
+ */
+ private static $install_cache_dir;
+
+ /**
+ * The directory that the WP-CLI cache (WP_CLI_CACHE_DIR, normally "$HOME/.wp-cli/cache") is set to on a "Given an empty cache" step.
+ * Variable SUITE_CACHE_DIR. Lives until the end of the scenario (or until another "Given an empty cache" step within the scenario).
+ */
+ private static $suite_cache_dir;
+
+ /**
+ * Where the current WP-CLI source repository is copied to for Composer-based tests with a "Given a dependency on current wp-cli" step.
+ * Variable COMPOSER_LOCAL_REPOSITORY. Lives until the end of the suite.
+ */
+ private static $composer_local_repository;
+
+ /**
+ * The test database settings. All but `dbname` can be set via environment variables. The database is dropped at the start of each scenario and created on a "Given a WP install" step.
+ */
+ private static $db_settings = [
+ 'dbname' => 'wp_cli_test',
+ 'dbuser' => 'wp_cli_test',
+ 'dbpass' => 'password1',
+ 'dbhost' => '127.0.0.1',
+ ];
+
+ /**
+ * Array of background process ids started by the current scenario. Used to terminate them at the end of the scenario.
+ */
+ private $running_procs = [];
+
+ /**
+ * Array of variables available as {VARIABLE_NAME}. Some are always set: CORE_CONFIG_SETTINGS, SRC_DIR, CACHE_DIR, WP_VERSION-version-latest. Some are step-dependent:
+ * RUN_DIR, SUITE_CACHE_DIR, COMPOSER_LOCAL_REPOSITORY, PHAR_PATH. Scenarios can define their own variables using "Given save" steps. Variables are reset for each scenario.
+ */
+ public $variables = [];
+
+ /**
+ * The current feature file and scenario line number as '