├── .gitignore ├── .travis.yml ├── blog-duplicator.php ├── composer.json ├── features └── load-wp-cli.feature ├── inc └── class-blog-duplicator.php ├── readme.md └── vipgo-helper.php /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor/ 2 | .DS_Store 3 | composer.lock 4 | .vscode/ -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | dist: trusty 3 | 4 | language: php 5 | php: 6 | - 7.1 7 | - 7.2 8 | - 7.3 9 | 10 | notifications: 11 | email: 12 | on_success: never 13 | on_failure: change 14 | 15 | branches: 16 | only: 17 | - master 18 | 19 | cache: 20 | directories: 21 | - $HOME/.composer/cache 22 | 23 | env: 24 | global: 25 | - PATH="$TRAVIS_BUILD_DIR/vendor/bin:$PATH" 26 | - WP_CLI_BIN_DIR="$TRAVIS_BUILD_DIR/vendor/bin" 27 | 28 | before_install: 29 | - | 30 | # Remove Xdebug for a huge performance increase: 31 | if [ -f ~/.phpenv/versions/$(phpenv version-name)/etc/conf.d/xdebug.ini ]; then 32 | phpenv config-rm xdebug.ini 33 | else 34 | echo "xdebug.ini does not exist" 35 | fi 36 | - | 37 | # Raise PHP memory limit to 2048MB 38 | echo 'memory_limit = 2048M' >> ~/.phpenv/versions/$(phpenv version-name)/etc/conf.d/travis.ini 39 | - composer validate 40 | 41 | install: 42 | - composer install 43 | - composer prepare-tests 44 | 45 | script: 46 | - composer phpunit 47 | - composer behat || composer behat-rerun 48 | 49 | jobs: 50 | include: 51 | - stage: sniff 52 | script: 53 | - composer lint 54 | - composer phpcs 55 | env: BUILD=sniff 56 | - stage: test 57 | php: 7.3 58 | env: WP_VERSION=latest 59 | - stage: test 60 | php: 7.2 61 | env: WP_VERSION=latest 62 | - stage: test 63 | php: 5.6 64 | env: WP_VERSION=latest 65 | - stage: test 66 | php: 5.6 67 | env: WP_VERSION=trunk 68 | -------------------------------------------------------------------------------- /blog-duplicator.php: -------------------------------------------------------------------------------- 1 | 25 | * : The subdomain/directory of the new blog. Only lowercase letters (a-z), numbers, and hyphens are allowed. 26 | * 27 | * [--domain] 28 | * : Use if duplicated blog should have a different domain from the origin. 29 | * 30 | * [--skip-copy-files] 31 | * : Skip copying uploaded files 32 | * 33 | * [--ignore-site-path] 34 | * : Ignore network-defined site path 35 | * 36 | * [--extra-tables=] 37 | * : Extra tables to include in duplication. Sans-prefix, comma-separated 38 | * 39 | * [--verbose] 40 | * : Output extra info 41 | * 42 | * [--yes] 43 | * : Confirm 'yes' automatically 44 | * 45 | * ## EXAMPLES 46 | * 47 | * wp duplicate domain-slug 48 | * wp duplicate test-blog-12 --url=multisite.local/test-blog-3 49 | */ 50 | public function __invoke( $args, $assoc_args ) { 51 | 52 | if ( ! is_multisite() ) { 53 | WP_CLI::error( 'This is a multisite command only.' ); 54 | } 55 | 56 | list( $new_slug ) = $args; 57 | $domain = WP_CLI\Utils\get_flag_value( $assoc_args, 'domain', false ); 58 | $skip_copy_files = WP_CLI\Utils\get_flag_value( $assoc_args, 'skip-copy-files', false ); 59 | $ignore_site_path = WP_CLI\Utils\get_flag_value( $assoc_args, 'ignore-site-path', false ); 60 | $manual_extra_tables = wp_parse_list( WP_CLI\Utils\get_flag_value( $assoc_args, 'extra-tables', '' ) ); 61 | $verbose = WP_CLI\Utils\get_flag_value( $assoc_args, 'verbose', false ); 62 | 63 | $new_slug = trim( $new_slug ); 64 | if ( preg_match( '|^([a-zA-Z0-9-])+$|', $new_slug ) ) { 65 | $new_slug = strtolower( $new_slug ); 66 | } else { 67 | WP_CLI::error( 'Missing or invalid site address. Only lowercase letters (a-z), numbers, and hyphens are allowed.' ); 68 | } 69 | 70 | global $wpdb; 71 | 72 | // Get table info for source (origin) blog. 73 | $extra_tables = array(); 74 | 75 | /** 76 | * Filters the list blog tables for a given blog. 77 | * 78 | * This filter allows new tables to be added to the core list. 79 | * 80 | * @param string[] $tables An array of blog tables without the database prefix. 81 | */ 82 | foreach ( apply_filters( 'blog_duplicator_extra_tables', $manual_extra_tables ) as $extra_table ) { 83 | $extra_tables[ $extra_table ] = $wpdb->prefix . $extra_table; 84 | } 85 | 86 | $src_tables = array_merge( $wpdb->tables( 'blog' ), $extra_tables ); 87 | $src_url = home_url(); 88 | $src_roles = get_option( $wpdb->prefix . 'user_roles'); 89 | 90 | global $current_site; 91 | 92 | // Set up new blog information. 93 | if ( is_subdomain_install() ) { 94 | $dest_domain = $domain ?: $new_slug . '.' . preg_replace( '|^www\.|', '', $current_site->domain ); 95 | $dest_path = $ignore_site_path ? '/' : $current_site->path; 96 | } else { 97 | $dest_domain = $domain ?: $current_site->domain; 98 | $dest_path = ($ignore_site_path ? '/' : $current_site->path) . $new_slug . '/'; 99 | } 100 | 101 | // Additional settings to copy from origin blog. 102 | $dest_title = get_bloginfo() . ' Copy'; 103 | $user_id = email_exists( get_option( 'admin_email' ) ); 104 | 105 | WP_CLI::log( 'Preparing to create new blog:' ); 106 | WP_CLI::log( WP_CLI::colorize( " Domain: %G$dest_domain%n" ) ); 107 | WP_CLI::log( WP_CLI::colorize( " Path: %G$dest_path%n" ) ); 108 | WP_CLI::log( WP_CLI::colorize( " Title: %G$dest_title%n" ) ); 109 | WP_CLI::log( WP_CLI::colorize( "Based on: %Y$src_url%n" ) ); 110 | 111 | WP_CLI::confirm( "Proceed with duplication?", $assoc_args ); 112 | // First step, create the blog in the normal way. 113 | $new_blog_id = wpmu_create_blog( $dest_domain, $dest_path, $dest_title, $user_id, array( 'public' => 1 ), $current_site->id ); 114 | 115 | if ( is_wp_error( $new_blog_id ) ) { 116 | WP_CLI::error( $new_blog_id->get_error_message() ); 117 | } 118 | 119 | $this->verbose_line( 'New blog id:', $new_blog_id, $verbose ); 120 | 121 | $src_wp_upload_dir = wp_upload_dir(); 122 | $src_basedir = $src_wp_upload_dir['basedir']; 123 | $src_baseurl = $src_wp_upload_dir['baseurl']; 124 | 125 | // Switch into the new blog to duplicate tables and make other customizations. 126 | switch_to_blog( $new_blog_id ); 127 | 128 | // Make upload destination. 129 | $dest_wp_upload_dir = wp_upload_dir(); 130 | $dest_basedir = $dest_wp_upload_dir['basedir']; 131 | $dest_baseurl = $dest_wp_upload_dir['baseurl']; 132 | wp_mkdir_p( $dest_basedir ); 133 | 134 | // Copy files. 135 | if ( ! $skip_copy_files ) { 136 | $is_shell_exec_enabled = is_callable( 'shell_exec' ) && false === stripos( ini_get( 'disable_functions' ), 'shell_exec' ); 137 | 138 | if ( ! $is_shell_exec_enabled ) { 139 | WP_CLI::warning( 'shell_exec is disabled, skipping file copying!' ); 140 | } else { 141 | $is_rsync_installed = ! empty( shell_exec( 'which rsync' ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.system_calls_shell_exec 142 | 143 | if ( $is_rsync_installed ) { 144 | WP_CLI::log( 'Duplicating uploads...' ); 145 | $this->verbose_line( 'Running command:', "rsync -a {$src_basedir}/ {$dest_basedir} --exclude sites", $verbose ); 146 | shell_exec( // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.system_calls_shell_exec 147 | sprintf( 148 | 'rsync -a %s/ %s --exclude sites', 149 | escapeshellarg( $src_basedir ), 150 | escapeshellarg( $dest_basedir ) 151 | ) 152 | ); 153 | } else { 154 | WP_CLI::warning( 'Cannot find rsync, skipping file copying!' ); 155 | } 156 | } 157 | } else { 158 | WP_CLI::warning( 'SKIPPING Duplicating uploads...' ); 159 | } 160 | 161 | // Here is where table duplication starts. 162 | // phpcs:disable WordPress.DB.DirectDatabaseQuery,WordPress.DB.PreparedSQL.InterpolatedNotPrepared 163 | $url = home_url(); 164 | WP_CLI::log( 'Duplicating tables...' ); 165 | 166 | // This should look familiar. We want an array of tables for the new blog that matches the table array of the source (origin). 167 | $extra_tables = array(); 168 | foreach ( apply_filters( 'blog_duplicator_extra_tables', $manual_extra_tables ) as $extra_table ) { 169 | $extra_tables[ $extra_table ] = $wpdb->prefix . $extra_table; 170 | } 171 | 172 | $blog_tables = array_merge( $wpdb->tables( 'blog' ), $extra_tables ); 173 | foreach ( $blog_tables as $k => $table ) { 174 | $src_table = $src_tables[ $k ]; 175 | 176 | $sql = "DROP TABLE IF EXISTS $table"; 177 | $this->verbose_line( 'Running SQL:', $sql, $verbose ); 178 | $wpdb->query( $sql ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared 179 | unset( $sql ); 180 | 181 | $sql = "CREATE TABLE $table LIKE $src_table"; 182 | $this->verbose_line( 'Running SQL:', $sql, $verbose ); 183 | $wpdb->query( $sql ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared 184 | unset( $sql ); 185 | 186 | // Remove blocked options from option table before import. 187 | if ( $wpdb->options === $table ) { 188 | /** 189 | * Filters the list of options that should not be copied. 190 | * 191 | * @param string[] $options An array of option names. 192 | */ 193 | $blocked_options = apply_filters( 'blog_duplicator_blocked_options', array( 'jetpack_options', 'jetpack_private_options', 'vaultpress' ) ); 194 | 195 | $sql = $wpdb->prepare( "INSERT INTO $table SELECT * FROM $src_table WHERE option_name NOT IN (" . implode( ', ', array_fill( 0, count( $blocked_options ), '%s' ) ) . ')', ...$blocked_options ); 196 | 197 | } else { 198 | $sql = "INSERT INTO $table SELECT * FROM $src_table"; 199 | } 200 | 201 | $this->verbose_line( 'Running SQL:', $sql, $verbose ); 202 | $wpdb->query( $sql ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared 203 | unset( $sql ); 204 | 205 | } 206 | 207 | // Re-set the blogname since the table duplication overwrote our setting in wpmu_create_blog. 208 | update_option( 'blogname', $dest_title ); 209 | update_option( $wpdb->prefix . 'user_roles', $src_roles ); 210 | // phpcs:enable WordPress.DB.DirectDatabaseQuery,WordPress.DB.PreparedSQL.InterpolatedNotPrepared 211 | 212 | // Long match first, replace upload url. 213 | WP_CLI::log( "Run search-replace on tables (1/2)..." ); 214 | $_command = sprintf( "search-replace '$src_baseurl' '$dest_baseurl' --url=$url --%s --all-tables-with-prefix", ( $verbose ? 'report-changed-only' : 'quiet' ) ); 215 | $this->verbose_line( 'Running command:', $_command, $verbose ); 216 | WP_CLI::runcommand( $_command ); 217 | 218 | // Replace root url. 219 | WP_CLI::log( "Run search-replace on tables (2/2)..." ); 220 | $_command = sprintf( "search-replace '$src_url' '$url' --url=$url --%s --all-tables-with-prefix", ( $verbose ? 'report-changed-only' : 'quiet' ) ); 221 | $this->verbose_line( 'Running command:', $_command, $verbose ); 222 | WP_CLI::runcommand( $_command ); 223 | 224 | WP_CLI::runcommand( "cache flush --url=$url" ); 225 | 226 | restore_current_blog(); 227 | 228 | WP_CLI::success( "Blog $new_blog_id created." ); 229 | 230 | } 231 | 232 | /** 233 | * Outputs extra information. 234 | * 235 | * @param string $pre Text prefix. 236 | * @param string $text Main text to output. 237 | * @param boolean $verbose Whether or not to output, defaults to false. 238 | * @return void 239 | */ 240 | private function verbose_line( $pre, $text, $verbose = false ) { 241 | if ( $verbose ) { 242 | WP_CLI::log( 243 | WP_CLI::colorize( 244 | "%C$pre%n $text" 245 | ) 246 | ); 247 | } 248 | } 249 | 250 | } 251 | 252 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | trepmal/blog-duplicator 2 | ======================= 3 | 4 | Blog Duplicator 5 | 6 | [![Build Status](https://api.travis-ci.org/trepmal/blog-duplicator.svg?branch=master&status=unknown)](https://travis-ci.org/github/trepmal/blog-duplicator) 7 | 8 | Quick links: [Using](#using) | [Installing](#installing) | [Contributing](#contributing) | [Support](#support) 9 | 10 | ## Using 11 | 12 | ~~~ 13 | wp duplicate [--skip-copy-files] [--extra-tables=] [--verbose] 14 | ~~~ 15 | 16 | **Important!** 17 | 18 | Only copies Core tables by default. Support for duplicating custom tables 19 | is handled through the `blog_duplicator_extra_tables` filter. e.g. 20 | 21 | function myplugin_blog_duplicator_extra_tables( $tables ) { 22 | $tables[] = 'myplugin'; 23 | return $tables; 24 | } 25 | add_filter( 'blog_duplicator_extra_tables', 'myplugin_blog_duplicator_extra_tables', 10, 1 ); 26 | 27 | **OPTIONS** 28 | 29 | 30 | The subdomain/directory of the new blog 31 | 32 | [--skip-copy-files] 33 | Skip copying uploaded files 34 | 35 | [--extra-tables=] 36 | Extra tables to include in duplication. Sans-prefix, comma-separated 37 | 38 | [--verbose] 39 | Output extra info 40 | 41 | **EXAMPLES** 42 | 43 | wp duplicate domain-slug 44 | wp duplicate test-site-12 --url=multisite.local/test-site-3 45 | 46 | ## Installing 47 | 48 | Installing this package requires WP-CLI v1.3.0 or greater. Update to the latest stable release with `wp cli update`. 49 | 50 | Once you've done so, you can install this package with: 51 | 52 | wp package install git@github.com:trepmal/blog-duplicator.git 53 | 54 | ## Contributing 55 | 56 | We appreciate you taking the initiative to contribute to this project. 57 | 58 | Contributing isn’t limited to just code. We encourage you to contribute in the way that best fits your abilities, by writing tutorials, giving a demo at your local meetup, helping other users with their support questions, or revising our documentation. 59 | 60 | For a more thorough introduction, [check out WP-CLI's guide to contributing](https://make.wordpress.org/cli/handbook/contributing/). This package follows those policy and guidelines. 61 | 62 | ### Reporting a bug 63 | 64 | Think you’ve found a bug? We’d love for you to help us get it fixed. 65 | 66 | Before you create a new issue, you should [search existing issues](https://github.com/trepmal/blog-duplicator/issues?q=label%3Abug%20) to see if there’s an existing resolution to it, or if it’s already been fixed in a newer version. 67 | 68 | Once you’ve done a bit of searching and discovered there isn’t an open or fixed issue for your bug, please [create a new issue](https://github.com/trepmal/blog-duplicator/issues/new). Include as much detail as you can, and clear steps to reproduce if possible. For more guidance, [review our bug report documentation](https://make.wordpress.org/cli/handbook/bug-reports/). 69 | 70 | ### Creating a pull request 71 | 72 | Want to contribute a new feature? Please first [open a new issue](https://github.com/trepmal/blog-duplicator/issues/new) to discuss whether the feature is a good fit for the project. 73 | 74 | Once you've decided to commit the time to seeing your pull request through, [please follow our guidelines for creating a pull request](https://make.wordpress.org/cli/handbook/pull-requests/) to make sure it's a pleasant experience. See "[Setting up](https://make.wordpress.org/cli/handbook/pull-requests/#setting-up)" for details specific to working on this package locally. 75 | 76 | ## Support 77 | 78 | Github issues aren't for general support questions, but there are other venues you can try: https://wp-cli.org/#support 79 | 80 | 81 | *This README.md is generated dynamically from the project's codebase using `wp scaffold package-readme` ([doc](https://github.com/wp-cli/scaffold-package-command#wp-scaffold-package-readme)). To suggest changes, please submit a pull request against the corresponding part of the codebase.* 82 | -------------------------------------------------------------------------------- /vipgo-helper.php: -------------------------------------------------------------------------------- 1 |