├── src ├── autoload.php ├── class-plugin.php ├── class-admin.php ├── class-endpoint.php └── class-builder.php ├── blueprint-builder.php └── readme.txt /src/autoload.php: -------------------------------------------------------------------------------- 1 | register_hooks(); 31 | 32 | if ( is_admin() ) { 33 | $blueprint_builder = new Builder(); 34 | $blueprint_builder_admin = new Admin( $blueprint_builder ); 35 | $blueprint_builder_admin->register_hooks(); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/class-admin.php: -------------------------------------------------------------------------------- 1 | builder = $builder; 20 | } 21 | 22 | /** 23 | * Register hooks. 24 | * 25 | * @return void 26 | */ 27 | public function register_hooks() { 28 | add_action( 'admin_menu', [ $this, 'add_menu' ] ); 29 | } 30 | 31 | /** 32 | * Add the menu. 33 | * 34 | * @return void 35 | */ 36 | public function add_menu() { 37 | add_menu_page( 38 | 'Blueprint Builder', 39 | 'Blueprint Builder', 40 | 'manage_options', 41 | 'blueprint-builder', 42 | [ $this, 'render_page' ], 43 | 'dashicons-welcome-widgets-menus' 44 | ); 45 | } 46 | 47 | /** 48 | * Render the page. 49 | * 50 | * @return void 51 | */ 52 | public function render_page() { 53 | $blueprint = $this->builder->generate(); 54 | echo '

Blueprint Builder

'; 55 | echo '

Here you can create a blueprint of your current environment.

'; 56 | echo '
'; 57 | 58 | $blueprint_url = site_url( 'wp-json/blueprint-builder/v1/json-' . get_option( 'blueprint_builder_key' ) ); 59 | $playground_url = 'https://playground.wordpress.net/?blueprint-url=' . $blueprint_url; 60 | 61 | echo '

If you website is live, you can open the Playground with this blueprint

'; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/class-endpoint.php: -------------------------------------------------------------------------------- 1 | key = get_option( 'blueprint_builder_key' ); 27 | } 28 | 29 | /** 30 | * Register hooks. 31 | */ 32 | public function register_hooks() { 33 | add_action( 'rest_api_init', [ $this, 'register_routes' ] ); 34 | } 35 | 36 | /** 37 | * Register the /json-blueprint/ endpoint. 38 | */ 39 | public function register_routes() { 40 | register_rest_route( 41 | 'blueprint-builder/v1', 42 | '/json-' . $this->key . '/', 43 | [ 44 | 'methods' => 'GET', 45 | 'callback' => [ $this, 'get_blueprint_json' ], 46 | '_pretty_json' => true, // This is a custom parameter, see 'pretty_json' in the 'register_rest_route' function in the 'wp-includes/rest-api.php' file. 47 | 'permission_callback' => '__return_true', // Allows public access. 48 | ] 49 | ); 50 | 51 | register_rest_route( 52 | 'blueprint-builder/v1', 53 | '/wxr-' . $this->key . '.xml', 54 | [ 55 | 'methods' => 'GET', 56 | 'callback' => [ $this, 'get_wxr' ], 57 | 'permission_callback' => '__return_true', // Allows public access. 58 | ] 59 | ); 60 | } 61 | 62 | /** 63 | * Callback to output the blueprint.json content. 64 | * 65 | * @return \WP_REST_Response 66 | */ 67 | public function get_blueprint_json() { 68 | $file_path = WP_CONTENT_DIR . '/blueprint-' . $this->key . '.json'; 69 | 70 | if ( ! file_exists( $file_path ) ) { 71 | $builder = new Builder(); 72 | $builder->generate(); 73 | } 74 | 75 | if ( file_exists( $file_path ) ) { 76 | $response = new \WP_REST_Response( 77 | json_decode( file_get_contents( $file_path ) ), 78 | 200, 79 | [ 80 | 'Content-Type' => 'application/json', 81 | 'Access-Control-Allow-Origin' => '*', 82 | ] 83 | ); 84 | return $response; 85 | } 86 | 87 | return new \WP_Error( 'json_blueprint_not_found', 'Blueprint JSON file not found.', [ 'status' => 404 ] ); 88 | } 89 | 90 | public function get_wxr() { 91 | require_once ABSPATH . 'wp-admin/includes/export.php'; 92 | 93 | $args['content'] = 'all'; 94 | header( 'Access-Control-Allow-Origin: *' ); 95 | export_wp( $args ); 96 | header_remove( 'Content-Description' ); 97 | header_remove( 'Content-Disposition' ); 98 | header( 'Content-Type: application/xml', true ); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/class-builder.php: -------------------------------------------------------------------------------- 1 | 'https://playground.wordpress.net/blueprint-schema.json', 22 | 'preferredVersions' => [ 23 | 'php' => '', 24 | 'wp' => '', 25 | ], 26 | 'features' => [ 27 | 'networking' => true, 28 | ], 29 | 'phpExtensionBundles' => [ 'kitchen-sink' ], 30 | 'landingPage' => '/wp-admin/', 31 | 'steps' => [], 32 | ]; 33 | 34 | /** 35 | * WordPress.org API URL for plugin information. 36 | */ 37 | const WP_ORG_PLUGIN_API_URL = 'https://api.wordpress.org/plugins/info/1.0/'; 38 | 39 | /** 40 | * Generates a blueprint file. 41 | * 42 | * @return string The blueprint, JSON encoded. 43 | */ 44 | public function generate() { 45 | $php_version = explode( '.', phpversion() ); 46 | $this->blueprint['preferredVersions']['php'] = $php_version[0] . '.' . $php_version[1]; 47 | 48 | $wp_version = explode( '.', get_bloginfo( 'version' ) ); 49 | $this->blueprint['preferredVersions']['wp'] = $wp_version[0] . '.' . $wp_version[1]; 50 | if ( ! is_numeric( $wp_version[1] ) ) { 51 | $this->blueprint['preferredVersions']['wp'] = 'nightly'; 52 | } 53 | 54 | $this->add_login_step(); 55 | $this->add_theme_installations_steps(); 56 | $this->add_plugins_installations_steps(); 57 | $this->add_wxr_step(); 58 | $this->add_option_steps(); 59 | 60 | $this->write(); 61 | 62 | return wp_json_encode( $this->blueprint ); 63 | } 64 | 65 | /** 66 | * Writes the blueprint to a file. 67 | * 68 | * @return void 69 | */ 70 | public function write() { 71 | $filename = 'blueprint-' . \get_option( 'blueprint_builder_key' ) . '.json'; 72 | 73 | global $wp_filesystem; 74 | 75 | require_once ( ABSPATH . '/wp-admin/includes/file.php' ); 76 | WP_Filesystem(); 77 | $wp_filesystem->put_contents( trailingslashit( WP_CONTENT_DIR ) . $filename, wp_json_encode( $this->blueprint, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES ) ); 78 | } 79 | 80 | /** 81 | * Adds the login step to the blueprint. 82 | * 83 | * @return void 84 | */ 85 | protected function add_login_step() { 86 | $this->blueprint['steps'][] = [ 87 | 'step' => 'login', 88 | 'username' => 'admin', 89 | 'password' => 'password', 90 | ]; 91 | } 92 | 93 | /** 94 | * Adds the WXR import step to the blueprint. 95 | * 96 | * @return void 97 | */ 98 | protected function add_wxr_step() { 99 | $wxr_url = site_url( 'wp-json/blueprint-builder/v1/wxr-' . get_option( 'blueprint_builder_key' ) . '.xml' ); 100 | 101 | $this->blueprint['steps'][] = [ 102 | 'step' => 'importFile', 103 | 'file' => [ 104 | 'resource' => 'url', 105 | 'url' => $wxr_url, 106 | ], 107 | ]; 108 | } 109 | 110 | /** 111 | * Adds the theme installation steps to the blueprint. 112 | * 113 | * @return void 114 | */ 115 | protected function add_theme_installations_steps() { 116 | $active_theme = $this->get_active_theme(); 117 | 118 | // Workaround for bug in Playground. 119 | // @link https://github.com/WordPress/wordpress-playground/issues/999 120 | if ( $active_theme === 'twentytwentyfour' ) { 121 | return; 122 | } 123 | 124 | // phpcs:ignore Generic.Commenting.Todo.TaskFound 125 | // @todo add support for child & parent themes. 126 | $this->blueprint['steps'][] = [ 127 | 'step' => 'installTheme', 128 | 'themeZipFile' => [ 129 | 'resource' => 'wordpress.org/themes', 130 | 'slug' => $this->get_active_theme(), 131 | ], 132 | 'options' => [ 133 | 'activate' => true, 134 | ], 135 | ]; 136 | } 137 | 138 | /** 139 | * Fetches the active theme. 140 | * 141 | * @return string The active theme. 142 | */ 143 | private function get_active_theme() { 144 | // phpcs:ignore Generic.Commenting.Todo.TaskFound 145 | // @todo Add support for child themes. 146 | // Currently returns the parent theme on purpose until we have a way to download the child theme from the site itself. 147 | return get_template(); 148 | } 149 | 150 | /** 151 | * Adds the plugin installation steps to the blueprint. 152 | * 153 | * @return void 154 | */ 155 | protected function add_plugins_installations_steps() { 156 | foreach ( $this->get_active_plugins() as $plugin ) { 157 | if ( $plugin['wordpress_org'] ) { 158 | $this->blueprint['steps'][] = [ 159 | 'step' => 'installPlugin', 160 | 'pluginZipFile' => [ 161 | 'resource' => 'wordpress.org/plugins', 162 | 'slug' => $plugin['slug'], 163 | ], 164 | 'options' => [ 165 | 'activate' => true, 166 | ], 167 | ]; 168 | } 169 | } 170 | } 171 | 172 | /** 173 | * Fetches the active plugins that are available on WordPress.org. 174 | * 175 | * @return array List of active plugins. 176 | */ 177 | protected function get_active_plugins() { 178 | $plugins = get_option( 'active_plugins' ); 179 | $return_plugins = []; 180 | 181 | foreach ( $plugins as $plugin ) { 182 | $plugin_data = get_plugin_data( WP_PLUGIN_DIR . '/' . $plugin ); 183 | $slug = pathinfo( basename( $plugin ), PATHINFO_FILENAME ); 184 | 185 | /** 186 | * Prevent some special cases: 187 | * - akismet breaks, 188 | * - blueprint-builder is this plugin and not needed, 189 | * - Plausible breaks - https://github.com/plausible/wordpress/issues/174 190 | */ 191 | if ( in_array( $slug, [ 'akismet', 'blueprint-builder', 'plausible-analytics' ] ) ) { 192 | continue; 193 | } 194 | $wp_org_data = wp_remote_post( 195 | self::WP_ORG_PLUGIN_API_URL, 196 | [ 197 | 'body' => [ 198 | 'action' => 'plugin_information', 199 | 'request' => serialize( 200 | (object) [ 201 | 'slug' => $slug, 202 | 'fields' => [ 'sections' => false ], 203 | ] 204 | ), 205 | 'per_page' => 1, 206 | ], 207 | ] 208 | ); 209 | $wordpress_org = false; 210 | if ( wp_remote_retrieve_response_code( $wp_org_data ) === 200 ) { 211 | $wordpress_org = true; 212 | } 213 | $return_plugins[] = [ 214 | 'slug' => $slug, 215 | 'version' => $plugin_data['Version'], 216 | 'wordpress_org' => $wordpress_org, 217 | ]; 218 | } 219 | 220 | return $return_plugins; 221 | } 222 | 223 | /** 224 | * Adds the option steps to the blueprint. 225 | * 226 | * @return void 227 | */ 228 | protected function add_option_steps() { 229 | $options = wp_load_alloptions(); 230 | 231 | // Prevent some special cases. 232 | foreach ( [ 233 | 'active_plugins', 234 | 'auth_key', 235 | 'auth_salt', 236 | 'cron', 237 | 'home', 238 | 'https_detection_errors', 239 | 'initial_db_version', 240 | 'logged_in_key', 241 | 'logged_in_salt', 242 | 'mailserver_url', 243 | 'mailserver_login', 244 | 'mailserver_pass', 245 | 'mailserver_port', 246 | 'new_admin_email', 247 | 'recently_activated', 248 | 'recovery_keys', 249 | 'rewrite_rules', 250 | 'siteurl', 251 | 'site_icon', 252 | 'site_logo', 253 | 'theme_switched', 254 | ] as $key 255 | ) { 256 | unset( $options[ $key ] ); 257 | } 258 | 259 | foreach ( $options as $key => $option ) { 260 | if ( strpos( $key, '_transient' ) === 0 || strpos( $key, '_site_transient' ) === 0 ) { 261 | unset( $options[ $key ] ); 262 | continue; 263 | } 264 | 265 | if ( $option === '' || $option === [] ) { 266 | unset( $options[ $key ] ); 267 | } 268 | } 269 | 270 | $i = 1; 271 | $j = 1; 272 | foreach ( $options as $key => $option ) { 273 | $options_chunks[ $j ][ $key ] = $option; 274 | ++$i; 275 | if ( $i > 10 ) { 276 | ++$j; 277 | $i = 1; 278 | } 279 | } 280 | 281 | foreach ( $options_chunks as $chunk ) { 282 | $this->blueprint['steps'][] = [ 283 | 'step' => 'setSiteOptions', 284 | 'options' => $chunk, 285 | ]; 286 | } 287 | } 288 | } 289 | --------------------------------------------------------------------------------