├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── bin ├── wpbootstrap └── wpcli.php ├── composer.json ├── composer.lock ├── doc └── 01-intro.md ├── phpunit.xml ├── src ├── Bootstrap.php ├── Commands │ ├── BaseCommand.php │ ├── Export.php │ ├── Import.php │ ├── Install.php │ ├── ItemsManagerCommand.php │ ├── Menus.php │ ├── OptionSnap.php │ ├── Posts.php │ ├── Reset.php │ ├── SetEnv.php │ ├── Setup.php │ └── Taxonomies.php ├── Export │ ├── ExportMedia.php │ ├── ExportMenus.php │ ├── ExportOptions.php │ ├── ExportPosts.php │ ├── ExportSidebars.php │ ├── ExportTaxonomies.php │ └── ExtractMedia.php ├── Extensions.php ├── Helpers.php ├── Import │ ├── ImportMenus.php │ ├── ImportOptions.php │ ├── ImportPosts.php │ ├── ImportSidebars.php │ └── ImportTaxonomies.php ├── Providers │ ├── ApplicationParametersProvider.php │ ├── CliUtilsWrapper.php │ ├── CliWrapper.php │ └── DefaultObjectProvider.php └── Resolver.php ├── tests ├── InstallTest.php └── bootstrap.php └── wpcli.php /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | www/ 3 | localsettings.json 4 | appsettings.json 5 | *.sublime-project 6 | *.sublime-workspace 7 | /vendor 8 | 9 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | **0.5.0** 2 | - New feature: All object serialization is now in Yaml instead of serialized php objects. Note! This breaks backwards compatibility and is also the reason for bumping the version to 0.5.0 3 | - New feature: List and add posts, taxonomies and menus to appsettings.yml via new sub commands posts, taxonomies and menus 4 | 5 | **0.4.0** 6 | - Major upgrade. WP Bootstrap now runs as a proper wp-cli sub command 7 | - New feature: localsettings.json is replaced with .env files 8 | - New feature: appsettings.json is replaced with appsettings.yml 9 | - New feature: support for dependencies between plugins/themes to ensure correct install order 10 | 11 | **0.3.9** 12 | - Last version as a "stand alone" binary. 13 | - Skipping already installed themes and plugins (standard) 14 | 15 | **0.3.8** 16 | - New feature: Support for running as a wp-cli sub command 17 | - Enhancement: snapshots command now includes column 'manage' in diff and show 18 | - Enhancement: improved column names snapshots in diff and show 19 | 20 | **0.3.7** 21 | - New feature: Support for dependencies between themes/plugins to determine installation order 22 | - Bug fix: During import, importing a menu and theme_mods would reset theme modifications. 23 | - Enhancement: Logging (DEBUG level) output from external commands (wp-cli, rm, cp, ln etc). 24 | 25 | **0.3.6** 26 | - New feature: Support for extensions 27 | - Better media extraction from content, now finding images in serialized/base64 encoded content 28 | - Improved performance on imports 29 | 30 | **0.3.6** 31 | - New feature: Support for extensions 32 | - Better media extraction from content, now finding images in serialized/base64 encoded content 33 | - Improved performance on imports 34 | 35 | **0.3.5** 36 | 37 | - Bug fix: (major) Fixed issue when importing two posts with same slug but different post types 38 | 39 | **0.3.4** 40 | 41 | - Bug fix: exporting now also includes posts with status = inherit 42 | - Bug fix: importing a post where the parent post is missing doesn't create infinite loop 43 | 44 | **0.3.3** 45 | 46 | - new feature: adding configured symlinks during wp-init 47 | 48 | **0.3.2** 49 | 50 | - new feature: wp-snapshots command to manage options 51 | - Code cleanup, more PSR2 strict 52 | 53 | **0.3.1** 54 | 55 | - Bug fixes for exporting and importing taxonomies of type "postid" 56 | - wp-init generates a wp-cli.yml file if localsettings/wppath has non default value 57 | 58 | **0.3.0** 59 | 60 | - Reference section is moved out from "content" into it's own section in appsettings.json 61 | - Added handling for "postid" taxonomies 62 | - Creating manifest files for taxonomies for better import control 63 | - Fixed some issues with search/replace in options and metadata 64 | - Neutralizing (urls) settings handled via wp-cfm 65 | - Additional refactoring 66 | - Logging all system calls done via PHP exec() 67 | 68 | **0.2.9** 69 | 70 | - Refactored and renamed classes 71 | - Introduced class Container as a (sort of) dependency injection container 72 | - Brought test coverage up to 85% 73 | 74 | **0.2.8** 75 | 76 | - Renamed section "wpbootstrap" to "content" in appsettings.json 77 | - Lots of logging added to the debug level. 78 | - Fixed bugs found from unit testing. 79 | - Brought test coverage back up to 80% 80 | 81 | **0.2.7** 82 | 83 | - When exporting, all taxonomy terms that are referenced by a post will be included. Better taxonomy handling (assignment) when importing the terms 84 | - Improved import of Posts 85 | - Added Monolog as a dependency 86 | - Logging to console and file can be configured via localsettings 87 | 88 | **0.2.6** 89 | 90 | - Improves handling for media that are not images (zip files etc). 91 | 92 | 93 | **0.2.5** 94 | 95 | - Simplified BASEPATH heuristics 96 | - When exporting, missing media files does not generate an error message 97 | 98 | **0.2.4** 99 | 100 | - Added VERSION constant. 101 | - Improvements for being called from within a WordPress plugin (such as Wp-bootstrap-ui) 102 | 103 | **0.2.3** 104 | 105 | - Referenced media handled better, so media that is referenced (used) in posts and widgets are included even if they are not properly attached 106 | - Code style cleanup using php-cs-fixer. 107 | 108 | **0.2.2** 109 | 110 | - Support for ***references***. Possible to add names of options that are references to other posts or taxonomy terms. 111 | - Fixed issues found when Test coverage up to over 80%. 112 | 113 | 114 | **0.2.1** 115 | 116 | - Support for taxonomy terms 117 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 eriktorsner 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # wp-bootstrap 3 | Wp-cli subcommand for managing WordPress installations. Automates installation, configuration and importing/exporting content. 4 | 5 | The central idea with WP Bootstrap is to provide a tool that makes it easier to create a decent WordPress deployment workflow. WP Bootstrap lets you use configuration files and a few command line commands to install WordPress, set it up with the correct plugins and themes, import options as well as pages, posts, menus etc. To top it of, it also maintains ID integrity during import so that the site looks the same when it's imported as it did when it was exported, even if all the underlying post ID's have changed. 6 | 7 | Tutorial for pre-subcommand usage (before 0.4.0): 8 | [Tutorial on wpessentials.io](http://www.wpessentials.io/2015/12/preparing-a-wordpress-site-for-git-using-wp-bootstrap/) 9 | 10 | ## Installation 11 | 12 | To add this package as a local, per-project dependency to your project, simply add a dependency on `eriktorsner/wp-bootstrap` to your project's `composer.json` file. Here is a minimal example of a `composer.json` file that just defines a dependency wp-bootstrap: 13 | 14 | { 15 | "require": { 16 | "eriktorsner/wp-bootstrap": "~0.5.0" 17 | } 18 | } 19 | 20 | or from the command line: 21 | 22 | $ composer require eriktorsner/Wp-bootstrap 23 | 24 | Once WP Bootstrap is installed, wp-cli needs to know about it. Find (or create) your wp-cli.yml config file and add: 25 | 26 | require: 27 | - vendor/autoload.php 28 | 29 | ## Configuration files 30 | 31 | WP Bootstrap uses configuration files to control the installation of WordPress as well as plugins and themes. 32 | 33 | | File | Format | Descriptions | 34 | |:-----------------|:-------|:---------------------------------------------------------------------------------------------------------| 35 | | appsettings.yml | Yaml | Defines plugins, themes and import/export settings | 36 | | .env | Dotenv | Environment variables needed to install WordPress | 37 | | .env-development | Dotenv | Optional. Environment variable for the development environment | 38 | | .env-test | Dotenv | Optional. Environment variable for the test environment | 39 | | wp-cli.yml | Yaml | The standard wp-cli config file. Sub command setenv manages path and writes the environment in this file | 40 | 41 | ## Quick introduction. Installing and setting up WordPress using WP Bootstrap 42 | 43 | **Step 1: Creating .env files** 44 | 45 | As a first step, create a .env file in your project root with settings matching your local database and apache/nginx configuration. Note that since WP Bootstrap maintains settings file in the project root folder, it's recommended to install WordPress in different location like a sub folder. 46 | 47 | # file: .env 48 | wppath=/path/to/target/wordpress 49 | wpurl=www.example.com 50 | dbhost=localhost 51 | dbname=wordpress 52 | dbuser=wordpress 53 | dbpass=secret 54 | wpuser=admin 55 | wppass=anothersecret 56 | 57 | Optionally, create an 'overlay' .env file for a specific environment, i.e 'development': 58 | 59 | # file: .env-development 60 | wppath=/path/to/target/wordpress-dev 61 | wpurl=dev.example.com 62 | dbname=wordpress-dev 63 | 64 | Update the wp-cli.yml file with path and environment info: 65 | 66 | $ wp setenv standard 67 | 68 | Or (if you created a .env-development): 69 | 70 | $ wp setenv development 71 | 72 | **Step 2: Create an appsettings.yml file (optional)** 73 | 74 | Create the appsettings.yml file that defines this WordPress installation 75 | 76 | # file: appsettings.yml 77 | # set a title for the WordPress site (defaults to '[title]') 78 | title: Testing Bootstrap 79 | 80 | # keep default content such as themes, posts, pages etc. 81 | keepDefaultContent: true 82 | 83 | # add a few plugins from the repo 84 | plugins: 85 | standard: 86 | - wp-cfm 87 | - disable-comments:1.3 88 | 89 | content: 90 | posts: 91 | page: '*' 92 | 93 | **Step 3: Install WordPress** 94 | 95 | Install WordPress 96 | 97 | $ wp bootstrap Install 98 | 99 | ...and install plugins etc. 100 | 101 | $ wp bootstrap setup 102 | 103 | Once WordPress is up and running, you will typically run the setup command over and over again as you add config to the appsettings.yml 104 | 105 | **Step 4: Exporting and importing content** 106 | 107 | To export the content defined: 108 | 109 | $ wp bootstrap export 110 | 111 | ... and to import it back again: 112 | 113 | $ wp bootstrap import 114 | 115 | To make a quick test. Use the appsettings.yml defined in step 2 above and execute the export command. The defined content (all pages) will be serialized and stored under the bootstrap subfolder in your project root. Edit (or even delete) the default sample page in WordPress and run the import command. The page should then be completely restored. 116 | 117 | ## Importing and exporting content 118 | 119 | The WP Bootstrap export command will take all content defined in appsettings.yml and serialize it to disk. The target folder is 'bootstrap' in your project root. This folder is supposed to be managed in Git so that it can be easily transferred to a another environment. As you might note, not all content in a WordPress installation is exported, actually it's the opposite, as little as possible but enough to maintain a working site. The idea is that you only export the content that you deem are part of you _application_ meaning for instance the static front page, the menu structure, all the content in various about pages and terms and conditions and anything similar. When the site is deployed to production, the same content will be imported there. 120 | 121 | The benefit of this might not be apparent when you deploy the site the first time. But when the site have been live for a few weeks and it's time to overhaul the menu structure and make a few changes to the About Us page, the benefits will become more apparent. At this time the production site might have lots of new blog posts, even more comments and perhaps even e-commerce orders. You don't want to make a complete database migration from your development environment because that would destroy all this other content. But by using WP Bootstrap you can import and overwrite just exactly those pieces of content that are defined in the appsettings.yml file. No other content on the target site gets touched. 122 | 123 | Besides just exporting and importing content. WP Boostrap does three other things: 124 | 125 | - It keeps track of the internal integrity of the content. A page with ID 10 in your development environment might end up with ID 223 in the production environment. If you have a menu that points to page ID 10 in the exported content, that menu will be modified to point to ID 223 when it's imported into production. 126 | - It keeps track of parent/child relationships. If the exported page with ID 10 is a child page of page 45, page 45 will be included in the exported content as well, otherwise the parent/child relationship would break 127 | - It keeps an eye on media. If an exported page uses a featured image and two more images are used in the post content, WP Bootstrap identifies these images and includes them in the exported data. 128 | 129 | 130 | ### Managing WordPress options 131 | 132 | WP Bootstrap is tightly integrated with the plugin WP-CFM. It's not mandatory to use it and you'll have to specify WP-CFM manually among the plugins in appsettings.yml. When exporting content, WP Bootstrap will use WP-CFM to export options defined in a WP-CFM bundle named 'wpbootstrap'. The resulting json file is copied to the bootstrap/config sub folder and should be managed under git. 133 | 134 | At import, that file is copied back into the expected location under wp-content and pushed back into the WordPress database. 135 | 136 | The actual management of what options to include is managed using the WP-CFM UI in the WordPress admin area. [Read more about WP-CFM here](https://wordpress.org/plugins/wp-cfm/). 137 | 138 | To help you identify what options to include and which ones that are changed, please refer to the section about the optiosnap command below. 139 | 140 | 141 | ## Available commands 142 | 143 | WP Bootstrap adds the following commands to wp-cli: 144 | 145 | ### Subcommand bootstrap 146 | 147 | The below commands are executed using: 148 | 149 | $ wp bootstrap 150 | 151 | | Command | Arguments | Description | 152 | |:--------|:----------|:---------------------------------------------------------------------------------| 153 | | install | | Add Wp-Bootstrap bindings to composer.json | 154 | | setup | | Download and install WordPress core. Creates a default WordPress installation | 155 | | export | | Add themes and plugins and import content | 156 | | import | | Alias for wp-install followed by wp-setup | 157 | | reset | | Updates core, themes or plugins that are installed from the WordPress repository | 158 | 159 | 160 | ### Subcommand setenv 161 | 162 | Setenv is a separate command that updates the wp-cli.yml file with settings from the .env files 163 | 164 | | Command | Arguments | Description | 165 | |:--------|:-------------------|:-------------------------------------------------------------| 166 | | setenv | | Updates the variables 'path' and 'environment' in wp-cli.yml | 167 | 168 | *Note:* setenv uses the output from wp --info to determine name and location of the project specific wp-cli.yml file 169 | 170 | ### Subcommand optionsnap 171 | 172 | Optionsnap is a utility that helps you keep track of which WordPress options that are in use in the WordPress installation. It's essentially dumping the contents of the wp_options table into a file located in sub folder bootstrap/snapshots. It's mainly intended to help developers understand which option values that are modified between two points in time. 173 | 174 | The below commands are executed using: 175 | 176 | $ wp optionsnap 177 | 178 | | Command | Arguments | Description | 179 | |:--------|:---------------------|:---------------------------------------------------------------------------------------------------| 180 | | snap | <--comment> | Creates a new snapshot file with an optional comment | 181 | | list | | Lists all available snapshots | 182 | | show | | Lists alls options values in the snapshot named | 183 | | show | | Shows the option identified by in the snapshot | 184 | | diff | | Shows the diff between the current state of the wp_options table the snapshot identified by | 185 | | diff | | Shows the diff between the two snapshots identified by and | 186 | 187 | 188 | ## Settings 189 | 190 | ### Dotenv files 191 | 192 | Settings that are unique to a specific WordPress installation environment are kept in dotenv files. These settings are only used when installing WordPress for the first time. To make it easier to maintain multiple WordPress installs on the same physical or virtual machine, one dotenv file can overlay the base one. 193 | 194 | When executing the install command, WP Bootstrap first looks in the wp-cli.yml file to read the name of the current environment. In the next step, it reads the variables contained in the base dotenv file (.env). In the last step, it looks for a file named .env- and if that file exists, it is also read and parsed. For values that are found in both the base and the environment specific file, the value from the environment specific one takes precedence. 195 | 196 | | Variable | Description | 197 | |:---------|:-------------------------------------------------------------------------------------------------------------------| 198 | | wppath | The path where WordPress should be installed. Also used by setenv to update wp-cli.yml when switching environments | 199 | | wpurl | The URL for the WordPress installation. Determined by the web server settings | 200 | | dbhost | The host name for the MySql database | 201 | | dbname | The database name for the MySql database | 202 | | dbuser | MySql user name | 203 | | dbpass | MySql user password | 204 | | wpuser | Default WordPress user name (often 'admin') | 205 | | wppass | Password for the above WordPress user | 206 | 207 | 208 | ### Application settings 209 | 210 | The Application settings file, appsettings.yml defines WP Bootstraps behavior when installing plugins and themes and when importing and exporting data to and from WordPress. 211 | 212 | The settings file consists of several sections: 213 | 214 | #### Base parameters: 215 | - **title** Specifies the title/blogname for the new WordPress installation. Used during bootstrap install 216 | - **version** (optional). Specifies the WordPress core version to install. If not specified (recommended) the latest version is installed 217 | - **keepDefaultContent** (optional). If not set or if set to false, all default content and all themes and plugins are removed after initial installation. To keep the default content, set this parameter to true. 218 | 219 | #### Plugins 220 | 221 | ### Section: plugins 222 | 223 | This section consists of up to three sub sections named "standard", "local" and "localcopy". In each section, a list of plugins is defined. Plugins that are available in the WordPress plugin repository or via a URL are defined in the "standard" section. Plugins that exists locally in the same Git repository as the project are defined in the "local" section and are linked into the target WordPress install using a symlink. Some plugins won't behave very well as a symlink and they need to be copied into the correct place, these plugins are defined in the "localcopy" section. The "local" and "localcopy" sections are mostly used for premium plugins that can't be installed via the standard WordPress repository. 224 | 225 | Each of the three plugin sections define a list. In it's simplest form, the list is just a string that identifies the slug of the plugin and an optional version like shown here: 226 | 227 | plugins: 228 | standard: 229 | - akismet 230 | - woocommerce 231 | - google-sitemap-generator:4.0.6 232 | local: 233 | - mycustomplugin 234 | 235 | 236 | In some (rare) cases, a plugin can't be installed unless another plugin or theme is already installed. To handle this, the plugin list can also contain an object with some optional extra parameters. To ensure that a specific plugin isn't installed before another one: 237 | 238 | plugins: 239 | standard: 240 | - akismet 241 | - woocommerce: 242 | requires: 243 | plugins: ['google-sitemap-generator'] 244 | themes: ['mycustomtheme'] 245 | - google-sitemap-generator 246 | local: 247 | - mycustomplugin 248 | 249 | In this example, the WooCommerce plugin will not be installed until after Google Sitemap Generator and the theme Mycustomthme are installed. 250 | 251 | For standard plugins, the slug refers to the unique identifier (last part of the URL) in the WordPress repository. If the slug is a full URL, the plugin will be installed from that URL. But for local and localcopy plugins, the slug refers to the folder name that the plugin is stored in on the local file system. The above definition would assume a project file tree that looks something like this: 252 | 253 | . 254 | └── wp-content 255 | ├── plugins 256 | │   └── mycustomplugin 257 | │   ├── file1.php 258 | │   └── file2.php 259 | └── themes 260 | └── mycustomtheme 261 | ├── functions.php 262 | └── style.css 263 | 264 | 265 | When defining a plugin as an object rather than a simple string. The object can have the following properties: 266 | 267 | - **slug** The name/slug of the plugin (will override the key name used in the list) 268 | - **version** Specifying a version when installing a plugin from the official WordPress repository, 269 | - **requires** Two lists of slugs identifying other themes and plugins that needs to be installed first 270 | - **plugins** An array of slugs (string) that defines required plugins 271 | - **themes** An array of slugs (string) that defines required themes 272 | 273 | 274 | ### Section: themes 275 | Similar to the plugins section but for themes. The only real difference is that the themes section also has the parameter 'active' that identifies the theme that should be activated. 276 | 277 | - **standard** Fetches themes from the official WordPress repository. If a specific version is needed, specify the version using a colon and the version identifier i.e **footheme:1.1** 278 | - **local** A list of themes in your local project folder. The themes are expected to be located in folder projectroot/wp-content/themes/. Local themes are symlinked into place in the wp-content folder of the WordPress installation specified by wppath in localsettings.json 279 | - **localcopy** A list of themes in your local project folder. The themes are expected to be located in folder projectroot/wp-content/themes/. Local themes are copied into place in the wp-content folder of the WordPress installation specified by wppath in localsettings.json. Some poorly written themes and plugins requires to be located in the correct WordPress folder, but consider it as a last option 280 | - **active** A string specifying what theme to activate. 281 | 282 | 283 | ### Section: settings 284 | 285 | A list of options (settings) that will be applied to the WordPress installation using the wp-cli command "option update %s". Currently only supports simple scalar values (strings and integers). Example: 286 | 287 | settings: 288 | admin_email: foobar@example.com 289 | blogname: My first blog 290 | blogdescription: Just anohter blog tagline 291 | 292 | For the most part, it's recommended to manage options using the WP-CFM plugin. WP Bootstrap has build in support for importing and exporting options using WP-CFM. Managing options directly in the appsettings.yml can quickly become overwhelming. 293 | 294 | 295 | ### Section: content 296 | 297 | This sections defines how to handle content during export and import of data using the wp-export or wp-import command. 298 | 299 | **posts** Used during the export process. Contains zero or more keys with an associated array. The key specifies a post_type (page, post etc) and the array contains **post_name** for each post to include. The export process also includes any media (images) that are attached to the specific post or are referred to in the post content or post meta. 300 | 301 | **menus** Used during the export process. Contains zero or more keys with an associated array. The key represents the menu name (as defined in WordPress admin) and the array should contain each *location* that the menu appears in. Note that location identifiers are unique for each theme. 302 | 303 | **taxonomies** Used during the export process. Contains zero or more keys with either a string or array as the value. Use asterisk (\*) if you want to include all terms in the taxonomy. Use an array of term slugs if you want to only include specific terms from that taxonomy. 304 | 305 | 306 | ### Section: references 307 | Used during the import process. This is a structure that describes option values (in the wp_option table) that contains references to a page or a taxonomy term. The reference item can contain a "posts" and a "terms" object describing settings that points to either posts or taxonomy terms. Each of these objects contains one single member "options" referring to the wp_options table (support for other references will be added later). The "options" member contains an array with names of options in the wp_option table. There are three ways to refer to an option: 308 | 309 | - **1.** A simple string, for instance "page_on_front". Meaning that there is an option in the wp_options table named "page_on_front" and that option is a reference to a post ID. 310 | - **2.** An object with a single name-value pair, for instance {"mysetting": "[2]"} or {"mysetting2":"->page_id"} meaning: 311 | - There is an option in the wp_options table named "mysetting" 312 | - That setting is an array or object and the value tells wp-bootstrap how to access the array element or member variable of interest. The value follows PHP syntax, so an array element is accessed via "[]" notation and an object member variable is accessed via the "->" syntax. 313 | - **3.** As above, but instead of a simple string value, the value is an array of strings. 314 | 315 | Reference resolving will only look at the pages/posts/terms included in your import set. The import set might include an option "mypage" in the config/wpbootstrap.json file that points to post ID=10. Also in the import set, there is that page with id=10. When this page is imported in the target WordPress installation, it might get another ID, 22 for instance. By telling wp-bootstrap that the setting "mypage" in the wp_options table refers to a page, wp-bootstrap will update that option to the new value 22 as part of the process. 316 | 317 | ### Section: extensions 318 | 319 | If the basic functionality in Wp-Bootstrap can't handle content in a certain situation, it's often possible to handle it via an extension. An extension is a PHP class that implements WordPress filters and actions to respond to certain events. For instance, if a plugin uses a custom table, an extension can hook into the 'wp-bootstrap_after_export' action to serialize that table to a file when the site is being exported. During import, the extension would hook into the 'wp-bootstrap_after_import' action to read the serialized file back into the database. 320 | 321 | Extensions can be made specifically for a certain plugin or for a specific site project. 322 | 323 | 324 | | Name | Type | Description | 325 | |:---------------------------------------|:-------|:-----------------------------------------------------------------------| 326 | | wp-bootstrap_before_import | Action | Called before all import activities | 327 | | wp-bootstrap_after_import_settings | Action | Called after settings have been imported with WP-CFM | 328 | | wp-bootstrap_after_import_content | Action | Called after content (posts, menus etc) have been imported | 329 | | wp-bootstrap_after_import | Action | Called after all import activities are done | 330 | | wp-bootstrap_before_export | Action | Called before any export activities starts | 331 | | wp-bootstrap_after_export | Action | Called after all exports activities are done | 332 | | wp-bootstrap_option_post_references | Filter | Lets the extension add names of option values that refer to a post id. | 333 | | wp-wp-bootstrap_option_term_references | Filter | Lets the extension add names of option values that refer to a term id | 334 | 335 | To use an extension, WP Bootstrap must be able to autoload the class. The easiest way to achieve this is to add a PSR4 namespace to the composer.json file of your project: 336 | 337 | { 338 | "require": { 339 | "eriktorsner/wp-bootstrap": "~0.4.0" 340 | }, 341 | "autoload": { 342 | "psr-4": { 343 | "MyNamespace\\": "src" 344 | } 345 | } 346 | 347 | Then place your extension in the sub folder "src" (relative to the project root). Any extension classes will be instantiated at the beginning of the process and a method "init" will be called. In this method the extension can add filters and actions that will be executed in various stages in the installation/import process: 348 | 349 | term_id' } 450 | - { mysettings2: '[2]' } 451 | - { mysettings3: ['->term_id', '->other_term_id']} 452 | 453 | # references to term id's 454 | terms: 455 | # references in the options table 456 | options: 457 | - some_term 458 | - { other_term: '->term_id'} 459 | 460 | extensions: 461 | - MyNamespace\MyClass 462 | 463 | 464 | ## Parent child references and automatic includes 465 | 466 | Wp-bootstrap tries it's hardest to preserve references between exported WordPress objects. If you export a page that is the child of another page, the parent page will be included in the exported data regardless if that page was included in the settings. Similar, if you export a menu that points to a page or taxonomy term was not specified, that page and taxonomy term will also be included in the exported data. 467 | 468 | ### Import matching 469 | When importing into a WordPress installation, wp-bootstrap will use the **slug** to match pages, menus and taxonomy terms. So if the dataset to be imported contains a page with the **slug** 'foobar', that page will be (a) created if it didn't previously exist or (b) updated if it did. The same logic applies to posts (pages, attachments, posts etc), menu items and taxonomy terms. 470 | 471 | **Note:** Taxonomies are defined in code rather than in the database. So the exact taxonomies that exist in a WordPress installation are defined at load time. The built in taxonomies are always there, but some taxonomies are defined in a theme or plugin. In order for your taxonomy terms to be imported during the wp-import process, the theme or plugin that defined the taxonomy needs to exist. 472 | 473 | 474 | ## Testing 475 | 476 | Since wp-bootstrap relies a lot on WordPress, there's a separate Github repository for testing using Vagrant. The test repo is available at [https://github.com/eriktorsner/wp-bootstrap-test](https://github.com/eriktorsner/wp-bootstrap-test). 477 | 478 | ## Contributing 479 | 480 | Contributions are welcome. Apart from code, the project is in need of better documentation, more test cases, testing with popular themes and plugins and so on. Any type of help is appreciated. 481 | 482 | ## Change log 483 | [Separate change log](CHANGELOG.md) 484 | -------------------------------------------------------------------------------- /bin/wpbootstrap: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | getExtensions()->init(); 14 | 15 | switch ($argv[1]) { 16 | case 'wp-bootstrap': 17 | $bootstrap = $container->getBootstrap(); 18 | $container->validateSettings(); 19 | $bootstrap->bootstrap(); 20 | break; 21 | case 'wp-install': 22 | $container->validateSettings(); 23 | $bootstrap = $container->getBootstrap(); 24 | $bootstrap->install(); 25 | break; 26 | case 'wp-setup': 27 | $container->validateSettings(); 28 | $bootstrap = $container->getBootstrap(); 29 | $bootstrap->setup(); 30 | break; 31 | case 'wp-update': 32 | $container->validateSettings(); 33 | $bootstrap = $container->getBootstrap(); 34 | $bootstrap->update(); 35 | break; 36 | case 'wp-export': 37 | $container->validateSettings(); 38 | $export = $container->getExport(); 39 | $export->export(); 40 | break; 41 | case 'wp-import': 42 | $container->validateSettings(); 43 | $import = $container->getImport(); 44 | $import->import(); 45 | break; 46 | case 'wp-reset': 47 | $container->validateSettings(); 48 | $bootstrap = $container->getBootstrap(); 49 | $bootstrap->reset(); 50 | break; 51 | case 'wp-init': 52 | $initBootstrap = $container->getInitbootstrap(); 53 | $initBootstrap->init(); 54 | break; 55 | case 'wp-init-composer': 56 | $initBootstrap = $container->getInitbootstrap(); 57 | $initBootstrap->initComposer(); 58 | break; 59 | case 'wp-init-wpcli': 60 | $initBootstrap = $container->getInitbootstrap(); 61 | $initBootstrap->initWpCli(true); 62 | break; 63 | case 'wp-snapshots': 64 | $container->validateSettings(); 65 | $manageState = $container->getSnapshots(); 66 | $manageState->manage(); 67 | break; 68 | default: 69 | die("Command {$argv[1]} not recognized\n"); 70 | } 71 | -------------------------------------------------------------------------------- /bin/wpcli.php: -------------------------------------------------------------------------------- 1 | config; 12 | $assocArgs = $runner->assoc_args; 13 | 14 | // check if we got environment from cmd line args before 15 | // reading the localsettings for the first time 16 | if (isset($assocArgs['env'])) { 17 | define('WPBOOT_ENVIRONMENT', $assocArgs['env']); 18 | } 19 | $localSettings = new \Wpbootstrap\Settings('local'); 20 | 21 | if (rtrim($localSettings->wppath, '/') != rtrim($config['path'], '/') && $localSettings->wppath != '[wppath]') { 22 | wpbstrp_rewritePath($localSettings->wppath); 23 | $cmd = join(' ', $argv); 24 | $output = []; 25 | exec($cmd, $output); 26 | echo join("\n", $output) . "\n"; 27 | WP_CLI::line("Path in wp-cli.yml is now set to {$localSettings->wppath}"); 28 | die(); 29 | } 30 | } 31 | 32 | WP_CLI::add_command('bootstrap', 'Wpbootstrap\WpCli'); 33 | 34 | function wpbstrp_rewritePath($newPath) { 35 | $lines = file(WPBOOT_BASEPATH . '/wp-cli.yml'); 36 | $buffer = ''; 37 | foreach ($lines as $line) { 38 | if (substr($line, 0, 5) != 'path:') { 39 | $buffer .= $line; 40 | } 41 | } 42 | $buffer .= "path: $newPath"; 43 | file_put_contents(WPBOOT_BASEPATH . '/wp-cli.yml', $buffer); 44 | } 45 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "eriktorsner/wp-bootstrap", 3 | "description": "Utils for bootstrapping a WordPress installation", 4 | "homepage": "https://github.com/eriktorsner/wp-bootstrap/", 5 | "type": "wp-cli-package", 6 | "keywords": [ 7 | "wordpress" 8 | ], 9 | "license": "MIT", 10 | "authors": [ 11 | { 12 | "name": "Erik Torsner", 13 | "email": "erik@torgesta.com", 14 | "role": "lead" 15 | } 16 | ], 17 | "support": { 18 | "issues": "https://github.com/eriktorsner/wp-bootstrap/issues" 19 | }, 20 | "require": { 21 | "php": ">=5.3.9", 22 | "symfony/yaml": "^2.8", 23 | "vlucas/phpdotenv": "^2.2", 24 | "pimple/pimple": "~3.0" 25 | }, 26 | "require-dev": { 27 | "10up/wp_mock": "dev-master" 28 | }, 29 | "autoload": { 30 | "psr-4": { 31 | "Wpbootstrap\\": "src" 32 | }, 33 | "files": ["wpcli.php"] 34 | }, 35 | "bin": ["bin/wpbootstrap"] 36 | } 37 | -------------------------------------------------------------------------------- /composer.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_readme": [ 3 | "This file locks the dependencies of your project to a known state", 4 | "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", 5 | "This file is @generated automatically" 6 | ], 7 | "hash": "c3f09018dc4ce7f602d52d10fea5eb13", 8 | "content-hash": "86cc15ace5ceaed0cac1cc06809f4e7c", 9 | "packages": [ 10 | { 11 | "name": "pimple/pimple", 12 | "version": "v3.0.2", 13 | "source": { 14 | "type": "git", 15 | "url": "https://github.com/silexphp/Pimple.git", 16 | "reference": "a30f7d6e57565a2e1a316e1baf2a483f788b258a" 17 | }, 18 | "dist": { 19 | "type": "zip", 20 | "url": "https://api.github.com/repos/silexphp/Pimple/zipball/a30f7d6e57565a2e1a316e1baf2a483f788b258a", 21 | "reference": "a30f7d6e57565a2e1a316e1baf2a483f788b258a", 22 | "shasum": "" 23 | }, 24 | "require": { 25 | "php": ">=5.3.0" 26 | }, 27 | "type": "library", 28 | "extra": { 29 | "branch-alias": { 30 | "dev-master": "3.0.x-dev" 31 | } 32 | }, 33 | "autoload": { 34 | "psr-0": { 35 | "Pimple": "src/" 36 | } 37 | }, 38 | "notification-url": "https://packagist.org/downloads/", 39 | "license": [ 40 | "MIT" 41 | ], 42 | "authors": [ 43 | { 44 | "name": "Fabien Potencier", 45 | "email": "fabien@symfony.com" 46 | } 47 | ], 48 | "description": "Pimple, a simple Dependency Injection Container", 49 | "homepage": "http://pimple.sensiolabs.org", 50 | "keywords": [ 51 | "container", 52 | "dependency injection" 53 | ], 54 | "time": "2015-09-11 15:10:35" 55 | }, 56 | { 57 | "name": "symfony/yaml", 58 | "version": "v2.8.4", 59 | "source": { 60 | "type": "git", 61 | "url": "https://github.com/symfony/yaml.git", 62 | "reference": "584e52cb8f788a887553ba82db6caacb1d6260bb" 63 | }, 64 | "dist": { 65 | "type": "zip", 66 | "url": "https://api.github.com/repos/symfony/yaml/zipball/584e52cb8f788a887553ba82db6caacb1d6260bb", 67 | "reference": "584e52cb8f788a887553ba82db6caacb1d6260bb", 68 | "shasum": "" 69 | }, 70 | "require": { 71 | "php": ">=5.3.9" 72 | }, 73 | "type": "library", 74 | "extra": { 75 | "branch-alias": { 76 | "dev-master": "2.8-dev" 77 | } 78 | }, 79 | "autoload": { 80 | "psr-4": { 81 | "Symfony\\Component\\Yaml\\": "" 82 | }, 83 | "exclude-from-classmap": [ 84 | "/Tests/" 85 | ] 86 | }, 87 | "notification-url": "https://packagist.org/downloads/", 88 | "license": [ 89 | "MIT" 90 | ], 91 | "authors": [ 92 | { 93 | "name": "Fabien Potencier", 94 | "email": "fabien@symfony.com" 95 | }, 96 | { 97 | "name": "Symfony Community", 98 | "homepage": "https://symfony.com/contributors" 99 | } 100 | ], 101 | "description": "Symfony Yaml Component", 102 | "homepage": "https://symfony.com", 103 | "time": "2016-03-04 07:54:35" 104 | }, 105 | { 106 | "name": "vlucas/phpdotenv", 107 | "version": "v2.2.0", 108 | "source": { 109 | "type": "git", 110 | "url": "https://github.com/vlucas/phpdotenv.git", 111 | "reference": "9caf304153dc2288e4970caec6f1f3b3bc205412" 112 | }, 113 | "dist": { 114 | "type": "zip", 115 | "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/9caf304153dc2288e4970caec6f1f3b3bc205412", 116 | "reference": "9caf304153dc2288e4970caec6f1f3b3bc205412", 117 | "shasum": "" 118 | }, 119 | "require": { 120 | "php": ">=5.3.9" 121 | }, 122 | "require-dev": { 123 | "phpunit/phpunit": "^4.8|^5.0" 124 | }, 125 | "type": "library", 126 | "extra": { 127 | "branch-alias": { 128 | "dev-master": "2.2-dev" 129 | } 130 | }, 131 | "autoload": { 132 | "psr-4": { 133 | "Dotenv\\": "src/" 134 | } 135 | }, 136 | "notification-url": "https://packagist.org/downloads/", 137 | "license": [ 138 | "BSD" 139 | ], 140 | "authors": [ 141 | { 142 | "name": "Vance Lucas", 143 | "email": "vance@vancelucas.com", 144 | "homepage": "http://www.vancelucas.com" 145 | } 146 | ], 147 | "description": "Loads environment variables from `.env` to `getenv()`, `$_ENV` and `$_SERVER` automagically.", 148 | "homepage": "http://github.com/vlucas/phpdotenv", 149 | "keywords": [ 150 | "dotenv", 151 | "env", 152 | "environment" 153 | ], 154 | "time": "2015-12-29 15:10:30" 155 | } 156 | ], 157 | "packages-dev": [ 158 | { 159 | "name": "10up/wp_mock", 160 | "version": "dev-master", 161 | "source": { 162 | "type": "git", 163 | "url": "https://github.com/10up/wp_mock.git", 164 | "reference": "3e032aac29063cf03eec13946597338781bc7082" 165 | }, 166 | "dist": { 167 | "type": "zip", 168 | "url": "https://api.github.com/repos/10up/wp_mock/zipball/3e032aac29063cf03eec13946597338781bc7082", 169 | "reference": "3e032aac29063cf03eec13946597338781bc7082", 170 | "shasum": "" 171 | }, 172 | "require": { 173 | "antecedent/patchwork": "~1.2", 174 | "mockery/mockery": "~0.8", 175 | "php": ">=5.3.2" 176 | }, 177 | "require-dev": { 178 | "phpunit/phpunit": "~3.7" 179 | }, 180 | "type": "library", 181 | "extra": { 182 | "branch-alias": { 183 | "dev-dev": "1.0.x-dev" 184 | } 185 | }, 186 | "autoload": { 187 | "psr-0": { 188 | "WP_Mock\\": "./" 189 | }, 190 | "classmap": [ 191 | "WP_Mock.php" 192 | ] 193 | }, 194 | "notification-url": "https://packagist.org/downloads/", 195 | "license": [ 196 | "GPL-2.0+" 197 | ], 198 | "description": "A mocking library to take the pain out of unit testing for WordPress", 199 | "time": "2015-08-19 23:01:51" 200 | }, 201 | { 202 | "name": "antecedent/patchwork", 203 | "version": "1.4.2", 204 | "source": { 205 | "type": "git", 206 | "url": "https://github.com/antecedent/patchwork.git", 207 | "reference": "f691893db7111eb858f0e4b7b8d4b9c4143848c8" 208 | }, 209 | "dist": { 210 | "type": "zip", 211 | "url": "https://api.github.com/repos/antecedent/patchwork/zipball/f691893db7111eb858f0e4b7b8d4b9c4143848c8", 212 | "reference": "f691893db7111eb858f0e4b7b8d4b9c4143848c8", 213 | "shasum": "" 214 | }, 215 | "require": { 216 | "php": ">=5.4.0" 217 | }, 218 | "type": "library", 219 | "notification-url": "https://packagist.org/downloads/", 220 | "license": [ 221 | "MIT" 222 | ], 223 | "authors": [ 224 | { 225 | "name": "Ignas Rudaitis", 226 | "email": "ignas.rudaitis@gmail.com" 227 | } 228 | ], 229 | "description": "Method redefinition (monkey-patching) functionality for PHP.", 230 | "keywords": [ 231 | "aop", 232 | "aspect", 233 | "interception", 234 | "monkeypatching", 235 | "redefinition", 236 | "runkit", 237 | "testing" 238 | ], 239 | "time": "2016-02-22 05:07:12" 240 | }, 241 | { 242 | "name": "hamcrest/hamcrest-php", 243 | "version": "v1.2.2", 244 | "source": { 245 | "type": "git", 246 | "url": "https://github.com/hamcrest/hamcrest-php.git", 247 | "reference": "b37020aa976fa52d3de9aa904aa2522dc518f79c" 248 | }, 249 | "dist": { 250 | "type": "zip", 251 | "url": "https://api.github.com/repos/hamcrest/hamcrest-php/zipball/b37020aa976fa52d3de9aa904aa2522dc518f79c", 252 | "reference": "b37020aa976fa52d3de9aa904aa2522dc518f79c", 253 | "shasum": "" 254 | }, 255 | "require": { 256 | "php": ">=5.3.2" 257 | }, 258 | "replace": { 259 | "cordoval/hamcrest-php": "*", 260 | "davedevelopment/hamcrest-php": "*", 261 | "kodova/hamcrest-php": "*" 262 | }, 263 | "require-dev": { 264 | "phpunit/php-file-iterator": "1.3.3", 265 | "satooshi/php-coveralls": "dev-master" 266 | }, 267 | "type": "library", 268 | "autoload": { 269 | "classmap": [ 270 | "hamcrest" 271 | ], 272 | "files": [ 273 | "hamcrest/Hamcrest.php" 274 | ] 275 | }, 276 | "notification-url": "https://packagist.org/downloads/", 277 | "license": [ 278 | "BSD" 279 | ], 280 | "description": "This is the PHP port of Hamcrest Matchers", 281 | "keywords": [ 282 | "test" 283 | ], 284 | "time": "2015-05-11 14:41:42" 285 | }, 286 | { 287 | "name": "mockery/mockery", 288 | "version": "0.9.4", 289 | "source": { 290 | "type": "git", 291 | "url": "https://github.com/padraic/mockery.git", 292 | "reference": "70bba85e4aabc9449626651f48b9018ede04f86b" 293 | }, 294 | "dist": { 295 | "type": "zip", 296 | "url": "https://api.github.com/repos/padraic/mockery/zipball/70bba85e4aabc9449626651f48b9018ede04f86b", 297 | "reference": "70bba85e4aabc9449626651f48b9018ede04f86b", 298 | "shasum": "" 299 | }, 300 | "require": { 301 | "hamcrest/hamcrest-php": "~1.1", 302 | "lib-pcre": ">=7.0", 303 | "php": ">=5.3.2" 304 | }, 305 | "require-dev": { 306 | "phpunit/phpunit": "~4.0" 307 | }, 308 | "type": "library", 309 | "extra": { 310 | "branch-alias": { 311 | "dev-master": "0.9.x-dev" 312 | } 313 | }, 314 | "autoload": { 315 | "psr-0": { 316 | "Mockery": "library/" 317 | } 318 | }, 319 | "notification-url": "https://packagist.org/downloads/", 320 | "license": [ 321 | "BSD-3-Clause" 322 | ], 323 | "authors": [ 324 | { 325 | "name": "Pádraic Brady", 326 | "email": "padraic.brady@gmail.com", 327 | "homepage": "http://blog.astrumfutura.com" 328 | }, 329 | { 330 | "name": "Dave Marshall", 331 | "email": "dave.marshall@atstsolutions.co.uk", 332 | "homepage": "http://davedevelopment.co.uk" 333 | } 334 | ], 335 | "description": "Mockery is a simple yet flexible PHP mock object framework for use in unit testing with PHPUnit, PHPSpec or any other testing framework. Its core goal is to offer a test double framework with a succinct API capable of clearly defining all possible object operations and interactions using a human readable Domain Specific Language (DSL). Designed as a drop in alternative to PHPUnit's phpunit-mock-objects library, Mockery is easy to integrate with PHPUnit and can operate alongside phpunit-mock-objects without the World ending.", 336 | "homepage": "http://github.com/padraic/mockery", 337 | "keywords": [ 338 | "BDD", 339 | "TDD", 340 | "library", 341 | "mock", 342 | "mock objects", 343 | "mockery", 344 | "stub", 345 | "test", 346 | "test double", 347 | "testing" 348 | ], 349 | "time": "2015-04-02 19:54:00" 350 | } 351 | ], 352 | "aliases": [], 353 | "minimum-stability": "stable", 354 | "stability-flags": { 355 | "10up/wp_mock": 20 356 | }, 357 | "prefer-stable": false, 358 | "prefer-lowest": false, 359 | "platform": { 360 | "php": ">=5.3.9" 361 | }, 362 | "platform-dev": [] 363 | } 364 | -------------------------------------------------------------------------------- /doc/01-intro.md: -------------------------------------------------------------------------------- 1 | # Using wp-bootstrap 2 | 3 | - [Installation](#installation) 4 | - [Core Concepts](#core-concepts) 5 | - [Intended workflow](#intended-workflow) 6 | 7 | 8 | ## Installation 9 | 10 | wp-bootstrap is available on Packagist ([eriktorsner/wp-bootstrap](https://packagist.org/packages/eriktorsner/wp-bootstrap)) 11 | and as such installable via [Composer](http://getcomposer.org/). 12 | 13 | ```bash 14 | composer require eriktorsner/wp-bootstrap 15 | ``` 16 | 17 | ## Core Concepts 18 | 19 | Wp-bootstrap is a util for bootstrapping a WordPress installations. It's intended use is to automate installation, configuration and content bootstrapping in a safe and scriptable way. 20 | 21 | First, wp-bootstrap enables you to script an entire WordPress installation with themes and plugins by writing two configuration files. (1) localsettings.json where you provide details about the server environment and (2) appsettings.json where you provide information about what plugins and themes to install. This makes it possible to quickly create identical installation in development, test, staging or other environments. 22 | 23 | The second, perhaps more important part, is that wp-bootstrap provides methods to export and import content and settings. 24 | 25 | Exported data is stored in text files suitable for both source code control and distribution. When the export is created, wp-bootstrap aims to include everything that is needed to recreate the content on another WordPress installation. When exporting a menu item that is pointing to a taxonomy term, that taxonomy term will be included in the export. When exporting a page that uses a thumbnail, that item in the media library will be included etc. 26 | 27 | When data is imported to the target environment, wp-bootstrap imports related media, creates (or updates) existing taxonomy terms etc. so that every item looks and works the same. 28 | 29 | In addition to normal content, wp-bootstrap also exports and imports settings from the wp_options table. Settings are also stored in a file. Wp-bootstrap also resolves references embedded in the wp_options table so that any individual option that points to a page or a post (like "page_on_front") will be adjusted after import, making sure that the correct database id is used. 30 | 31 | Wp-bootstrap depends on [wp-cli](http://wp-cli.org/) and a plugin named [WP-CFM](https://wordpress.org/plugins/wp-cfm/) to do a lot of the heavy lifting under the hood. The ideas and rationale that inspired this project was originally presented on [my blog](http://wpessentials.io/blog) and in the book [WordPress DevOps](https://leanpub.com/wordpressdevops) available on [Leanpub](https://leanpub.com/wordpressdevops). Wp-bootstrap also assumes that you are using Composer even if it's not strictly needed. Installation of WP-CFM is easiest to do via wp-bootstrap itself but installation of wp-cli needs to be done separately. 32 | 33 | This project scratches a very specific WordPress itch: being able to develop locally, managing the WordPress site in Git and being able to push changes (releases) to a production environment without worrying about overwriting content or having to manually migrate any setting or content. 34 | 35 | #Intended workflow 36 | 37 | #### On the development server (hint: use Vagrant): 38 | 39 | - Start a new project by requiring wp-bootstrap in composer.json 40 | - Run vendor/bin/wpbootstrap wp-init-composer to get easier access to the wp-bootstrap commands 41 | - Create a localsettings.json and appsettings.json 42 | - Make sure you exclude localsettings.json from source code control 43 | - Initiate the development installation with commands `composer wp-install` and `composer wp-setup` 44 | - As settings are updated, use the WP-CFM interface in WordPress Admin to include the relevant settings into the application configuration 45 | - As plugins and themes are needed, add them to appsettings.json and rerun the wp-setup command to get them installed into your local environment 46 | - As posts and menus are added, include them in appsettings.json. 47 | - When it's time to deploy to a staging or production environment, run `composer wp-export` command to get all content serialized to disk. Add them to your Git repo 48 | 49 | #### On the staging or production server: 50 | 51 | - Create the local database 52 | - Check out the project from Git 53 | - Create up your localsettings.json file with the relevant passwords and paths. 54 | - Run composer update 55 | - Run vendor/bin/wpbootstrap wp-init-composer to get easier access to the wp-bootstrap commands 56 | - Run `composer wp-install`, `composer wp-setup` and `composer wp-import` 57 | 58 | Once the target environment has been setup, new changes from the development environment can be pushed by checking out the new changes using Git and rerunning `wp-setup` and `wp-import`. 59 | 60 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | ./tests/ 12 | 13 | 14 | 15 | 16 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | src 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/Bootstrap.php: -------------------------------------------------------------------------------- 1 | register(new Providers\DefaultObjectProvider()); 27 | self::$application->register(new Providers\ApplicationParametersProvider()); 28 | } 29 | 30 | return self::$application; 31 | } 32 | 33 | /** 34 | * @param \Pimple\Container $application 35 | */ 36 | public static function setApplication($application) 37 | { 38 | self::$application = $application; 39 | } 40 | 41 | /** 42 | * Install a WordPress site based on appsettings.json and localsettings.json. 43 | * 44 | * @param $args 45 | * @param $assocArgs 46 | * 47 | * @when before_wp_load 48 | */ 49 | public function install($args, $assocArgs) 50 | { 51 | $app = self::getApplication(); 52 | $installer = $app['install']; 53 | $installer->run($args, $assocArgs); 54 | 55 | $this->writeDefinesInWPConfig(); 56 | } 57 | 58 | /** 59 | * Completely removes the WordPress installation defined in localsettings.json 60 | * 61 | * @param $args 62 | * @param $assocArgs 63 | * 64 | */ 65 | public function reset($args, $assocArgs) 66 | { 67 | $app = self::getApplication(); 68 | $reset = $app['reset']; 69 | $reset->run($args, $assocArgs); 70 | } 71 | 72 | /** 73 | * Install themes and plugins and apply options from appsettings.yml 74 | * 75 | * @param $args 76 | * @param $assocArgs 77 | * 78 | */ 79 | public function setup($args, $assocArgs) 80 | { 81 | $app = self::getApplication(); 82 | $obj = $app['setup']; 83 | $obj->run($args, $assocArgs); 84 | 85 | $this->writeDefinesInWPConfig(); 86 | } 87 | 88 | /** 89 | * Import serialized settings and content from folder bootstrap/ 90 | * 91 | * @param $args 92 | * @param $assocArgs 93 | * 94 | */ 95 | public function import($args, $assocArgs) 96 | { 97 | $app = self::getApplication(); 98 | $obj = $app['import']; 99 | $obj->run($args, $assocArgs); 100 | } 101 | 102 | /** 103 | * Export serialized settings and content to folder ./bootstrap 104 | * 105 | * @param $args 106 | * @param $assocArgs 107 | * 108 | */ 109 | public function export($args, $assocArgs) 110 | { 111 | $app = self::getApplication(); 112 | $obj = $app['export']; 113 | $obj->run($args, $assocArgs); 114 | } 115 | 116 | private function writeDefinesInWPConfig() 117 | { 118 | $app = self::getApplication(); 119 | $helpers = $app['helpers']; 120 | $helpers->ensureDefineInFile( 121 | $app['path'] . '/wp-config.php', 122 | 'WPBOOT_BASEPATH', 123 | WPBOOT_BASEPATH 124 | ); 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/Commands/BaseCommand.php: -------------------------------------------------------------------------------- 1 | required($parameter); 22 | } 23 | } catch (\Exception $e) { 24 | $cli->warning($e->getMessage()); 25 | return false; 26 | } 27 | 28 | return true; 29 | 30 | } 31 | } -------------------------------------------------------------------------------- /src/Commands/Export.php: -------------------------------------------------------------------------------- 1 | init(); 20 | 21 | do_action('wp-bootstrap_before_export'); 22 | 23 | $cli->log('Exporting options'); 24 | $exportOptions = $app['exportoptions']; 25 | $exportOptions->export(); 26 | 27 | $cli->log('Exporting content'); 28 | $this->clearExportFolders(); 29 | 30 | $exportMenus = $app['exportmenus']; 31 | $exportMenus->export(); 32 | 33 | $exportSidebars = $app['exportsidebars']; 34 | $exportSidebars->export(); 35 | 36 | $exportPosts = $app['exportposts']; 37 | $exportPosts->export(); 38 | 39 | $exportTaxonomies = $app['exporttaxonomies']; 40 | $exportTaxonomies->export(); 41 | 42 | $exportMedia = $app['exportmedia']; 43 | $exportMedia->export(); 44 | 45 | do_action('wp-bootstrap_after_export'); 46 | 47 | $this->createManifest(); 48 | 49 | } 50 | 51 | private function clearExportFolders() 52 | { 53 | $app = \Wpbootstrap\Bootstrap::getApplication(); 54 | $helpers = $app['helpers']; 55 | $cli = $app['cli']; 56 | $base = WPBOOT_BASEPATH.'/bootstrap'; 57 | 58 | $cli->debug("Cleaning export folders under $base"); 59 | $helpers->recursiveRemoveDirectory($base.'/menus'); 60 | $helpers->recursiveRemoveDirectory($base.'/posts'); 61 | $helpers->recursiveRemoveDirectory($base.'/media'); 62 | $helpers->recursiveRemoveDirectory($base.'/taxonomies'); 63 | $helpers->recursiveRemoveDirectory($base.'/sidebars'); 64 | } 65 | 66 | /** 67 | * Create and saves manifest file with details about the export 68 | */ 69 | private function createManifest() 70 | { 71 | $app = Bootstrap::getApplication(); 72 | $settings = $app['settings']; 73 | $helpers = $app['helpers']; 74 | 75 | $manifest = new \stdClass(); 76 | $dumper = new Dumper(); 77 | $manifest->created = date('Y-m-d H:i:s'); 78 | $manifest->boostrapVersion = Bootstrap::VERSION; 79 | $manifest->appSettings = $dumper->dump($settings, 2); 80 | 81 | $manifestFile = WPBOOT_BASEPATH.'/bootstrap/manifest.json'; 82 | if (file_exists(dirname($manifestFile))) { 83 | file_put_contents($manifestFile, $helpers->prettyPrint(json_encode($manifest))); 84 | } 85 | } 86 | } -------------------------------------------------------------------------------- /src/Commands/Import.php: -------------------------------------------------------------------------------- 1 | init(); 75 | 76 | // Run the import 77 | do_action('wp-bootstrap_before_import'); 78 | 79 | $cli->log('Importing settings'); 80 | $importOptions = $app['importoptions']; 81 | $importOptions->import(); 82 | do_action('wp-bootstrap_after_import_settings'); 83 | 84 | $cli->log('Importing content'); 85 | 86 | $cli->debug('Importing posts'); 87 | $posts = $app['importposts']; 88 | $posts->import(); 89 | $this->posts = &$posts->posts; 90 | 91 | $cli->debug('Importing taxonomies'); 92 | $taxonomies = $app['importtaxonomies']; 93 | $taxonomies->import(); 94 | $this->taxonomies = &$taxonomies->taxonomies; 95 | 96 | $cli->debug('Importing menus'); 97 | $menus = $app['importmenus']; 98 | $menus->import(); 99 | 100 | $cli->debug('Importing sidebars'); 101 | $sidebars = $app['importsidebars']; 102 | $sidebars->import(); 103 | 104 | $taxonomies->assignObjects(); 105 | do_action('wp-bootstrap_after_import_content'); 106 | 107 | // references 108 | $cli->log('Resolving references'); 109 | $this->resolvePostMetaReferences(); 110 | $this->resolveOptionReferences(); 111 | 112 | do_action('wp-bootstrap_after_import'); 113 | 114 | } 115 | 116 | /** 117 | * Finds a post or term based on it's original id. If found, returns the new (after import) id 118 | * 119 | * @param int $originalId 120 | * @param string $type 121 | * @return int 122 | */ 123 | public function findTargetObjectId($originalId, $type) 124 | { 125 | switch ($type) { 126 | case 'post': 127 | foreach ($this->posts as $post) { 128 | if ($post->post['ID'] == $originalId) { 129 | return $post->id; 130 | } 131 | } 132 | break; 133 | case 'term': 134 | foreach ($this->taxonomies as $taxonomy) { 135 | foreach ($taxonomy->terms as $term) { 136 | if ($term->term['term_id'] == $originalId) { 137 | return $term->id; 138 | } 139 | } 140 | } 141 | } 142 | 143 | return 0; 144 | } 145 | 146 | /** 147 | * Runs after all import is done. Resolves post_meta fields that 148 | * are references to posts or terms 149 | */ 150 | private function resolvePostMetaReferences() 151 | { 152 | $app = Bootstrap::getApplication(); 153 | $resolver = $app['resolver']; 154 | $settings = $app['settings']; 155 | 156 | if (isset($settings['references']['posts']['postmeta'])) { 157 | $references = $settings['references']['posts']['postmeta']; 158 | if (is_array($references)) { 159 | foreach ($references as $value) { 160 | if (is_string($value)) { 161 | $this->postPostMetaReferenceNames[] = $value; 162 | } elseif (is_object($value)) { 163 | $arr = (array) $value; 164 | $key = key($arr); 165 | $this->postPostMetaReferenceNames[$key] = $arr[$key]; 166 | } elseif (is_array($value)) { 167 | $key = key($value); 168 | $this->postPostMetaReferenceNames[$key] = $value[$key]; 169 | } 170 | } 171 | } 172 | } 173 | $resolver->resolvePostMetaReferences($this->postPostMetaReferenceNames, 'post'); 174 | 175 | if (isset($settings['references']['terms']['postmeta'])) { 176 | $references = $settings['references']['terms']['postmeta']; 177 | if (is_array($references)) { 178 | foreach ($references as $value) { 179 | if (is_string($value)) { 180 | $this->termPostMetaReferenceNames[] = $value; 181 | } elseif (is_object($value)) { 182 | $arr = (array) $value; 183 | $key = key($arr); 184 | $this->termPostMetaReferenceNames[$key] = $arr[$key]; 185 | } elseif (is_array($value)) { 186 | $key = key($value); 187 | $this->termPostMetaReferenceNames[$key] = $value[$key]; 188 | } 189 | } 190 | } 191 | } 192 | $resolver->resolvePostMetaReferences($this->termPostMetaReferenceNames, 'term'); 193 | } 194 | /** 195 | * Runs after all import is done. Resolves options fields that 196 | * are references to posts or terms 197 | */ 198 | private function resolveOptionReferences() 199 | { 200 | $app = Bootstrap::getApplication(); 201 | $resolver = $app['resolver']; 202 | $settings = $app['settings']; 203 | 204 | $options = []; 205 | if (isset($settings['references']['posts']['options'])) { 206 | $options = $settings['references']['posts']['options']; 207 | } 208 | 209 | // Ask any extensions to add option references via filter 210 | $options = apply_filters('wp-bootstrap_option_post_references', $options); 211 | 212 | if (is_array($options)) { 213 | foreach ($options as $value) { 214 | if (is_string($value)) { 215 | $this->optionPostReferenceNames[] = $value; 216 | } elseif (is_object($value)) { 217 | $arr = (array) $value; 218 | $key = key($arr); 219 | $this->optionPostReferenceNames[$key] = $arr[$key]; 220 | } elseif (is_array($value)) { 221 | $key = key($value); 222 | $this->optionPostReferenceNames[$key] = $value[$key]; 223 | } 224 | } 225 | } 226 | $resolver->resolveOptionReferences($this->optionPostReferenceNames, 'post'); 227 | 228 | $options = []; 229 | if (isset($settings['references']['terms']['options'])) { 230 | $options = $settings['references']['terms']['options']; 231 | } 232 | // Ask any extensions to add option references via filter 233 | $options = apply_filters('wp-bootstrap_option_term_references', $options); 234 | 235 | if (is_array($options)) { 236 | foreach ($options as $value) { 237 | if (is_string($value)) { 238 | $this->optionTermReferenceNames[] = $value; 239 | } elseif (is_object($value)) { 240 | $arr = (array) $value; 241 | $key = key($arr); 242 | $this->optionTermReferenceNames[$key] = $arr[$key]; 243 | } elseif (is_array($value)) { 244 | $key = key($value); 245 | $this->optionTermReferenceNames[$key] = $value[$key]; 246 | } 247 | } 248 | } 249 | $resolver->resolveOptionReferences($this->optionTermReferenceNames, 'term'); 250 | } 251 | } -------------------------------------------------------------------------------- /src/Commands/Install.php: -------------------------------------------------------------------------------- 1 | log('Running Bootstrap::install'); 20 | 21 | $this->installCore(); 22 | } 23 | 24 | /** 25 | * Download, configure and install WordPress core 26 | */ 27 | private function installCore() 28 | { 29 | $app = \Wpbootstrap\Bootstrap::getApplication(); 30 | $cli = $app['cli']; 31 | 32 | $ret = $this->requireEnv(array('wppath', 'wpurl', 'wpuser', 'wppass', 'dbhost', 'dbname', 'dbuser', 'dbpass')); 33 | if (!$ret) { 34 | return; 35 | } 36 | 37 | if (file_exists($app['path'] . '/wp-config.php')) { 38 | $cli->error('The \'wp-config.php\' file already exists.'); 39 | } 40 | 41 | // download core 42 | $assocArgs = array( 43 | 'path' => $_ENV['wppath'], 44 | ); 45 | $cli->run_command(array('core', 'download'), $assocArgs); 46 | 47 | // config core 48 | $assocArgs = array( 49 | 'dbhost' => $_ENV['dbhost'], 50 | 'dbname' => $_ENV['dbname'], 51 | 'dbuser' => $_ENV['dbuser'], 52 | 'dbpass' => $_ENV['dbpass'], 53 | ); 54 | $cli->run_command(array('core', 'config'), $assocArgs); 55 | 56 | // install core 57 | $assocArgs = array( 58 | 'url' => $_ENV['wpurl'], 59 | 'title' => '[title]', 60 | 'admin_user' => $_ENV['wpuser'], 61 | 'admin_password' => $_ENV['wppass'], 62 | ); 63 | 64 | if (isset($app['settings']['title'])) { 65 | $assocArgs['title'] = $app['settings']['title']; 66 | } 67 | 68 | $assocArgs['skip-email'] = 1; 69 | $assocArgs['admin_email'] = 'admin@local.dev'; 70 | if (isset($_ENV['wpemail'])) { 71 | $assocArgs['admin_email'] = $_ENV['wpemail']; 72 | } 73 | 74 | $ret = $cli->launch_self('core install', array(), $assocArgs, false, true); 75 | if ($ret->return_code !=0 && strlen($ret->stderr) > 0) { 76 | $cli->error(substr($ret->stderr, 7)); 77 | } 78 | $this->deleteDefaultContent(); 79 | 80 | $cli->success('Installation succeeded'); 81 | } 82 | 83 | /** 84 | * Unless specified otherwise, remove default content from a fresh 85 | * WordPress installation 86 | */ 87 | private function deleteDefaultContent() 88 | { 89 | $app = \Wpbootstrap\Bootstrap::getApplication(); 90 | $cli = $app['cli']; 91 | 92 | $cli->log('Deleting default posts, comments, themes and plugins'); 93 | $app = \Wpbootstrap\Bootstrap::getApplication(); 94 | 95 | if (!isset($app['settings']['keepDefaultContent']) || $app['settings']['keepDefaultContent'] == false) { 96 | $cmd = sprintf( 97 | 'db query "%s %s %s %s"', 98 | 'delete from wp_posts;', 99 | 'delete from wp_postmeta;', 100 | 'delete from wp_comments;', 101 | 'delete from wp_commentmeta;' 102 | ); 103 | $ret = $cli->launch_self($cmd, array(), array(), false, true); 104 | 105 | $cmd = sprintf("rm -rf %s/wp-content/plugins/*", $app['path']); 106 | $ret = $cli->launch($cmd); 107 | 108 | $cmd = sprintf("rm -rf %s/wp-content/themes/*", $app['path']); 109 | $ret = $cli->launch($cmd); 110 | } 111 | } 112 | } -------------------------------------------------------------------------------- /src/Commands/ItemsManagerCommand.php: -------------------------------------------------------------------------------- 1 | addToSettings($path, $settings); 24 | $app['settings'] = $settings; 25 | } 26 | 27 | /** 28 | * @param string $path 29 | */ 30 | protected function addToSettings($path, &$settings) 31 | { 32 | $app = Bootstrap::getApplication(); 33 | $cli = $app['cli']; 34 | 35 | $head = array_shift($path); 36 | if (count($path) == 0) { 37 | if ($head == '*') { 38 | if (is_array($settings)) { 39 | $cli->warning('Overwrote array with wildcard expression'); 40 | } 41 | $settings = '*'; 42 | } else { 43 | if (is_array($settings)) { 44 | if (!in_array($head, $settings)) { 45 | $settings[] = $head; 46 | } else { 47 | $cli->warning("$head is already managed."); 48 | } 49 | } else { 50 | $settings = array($head); 51 | $cli->warning('Overwrote wildcard expression with array'); 52 | } 53 | } 54 | } else { 55 | if (!isset($settings[$head])) { 56 | $settings[$head] = array(); 57 | } 58 | $this->addToSettings($path, $settings[$head]); 59 | } 60 | } 61 | 62 | protected function getJsonList($cmd, $args = array(), $assocArgs = array()) 63 | { 64 | $app = Bootstrap::getApplication(); 65 | $cli = $app['cli']; 66 | 67 | $ret = $cli->launch_self($cmd, $args, $assocArgs, false, true); 68 | 69 | if ($ret->return_code == 0) { 70 | return json_decode($ret->stdout); 71 | } else { 72 | if (strlen($ret->stderr) > 0) { 73 | $errMessage = str_replace('Error: ', '', $ret->stderr); 74 | $errMessage = str_replace("\n", '', $errMessage); 75 | $cli->error($errMessage); 76 | return false; 77 | } 78 | } 79 | 80 | return array(); 81 | } 82 | 83 | /** 84 | * @param $name 85 | * @param $default 86 | * 87 | * @return mixed 88 | */ 89 | protected function getAssocArg($name, $default) 90 | { 91 | $ret = $default; 92 | if (isset($this->assocArgs[$name])) { 93 | $ret = $this->assocArgs[$name]; 94 | } 95 | 96 | return $ret; 97 | } 98 | 99 | /** 100 | * Write the current settings in the Pimple 101 | * container back to appsettings.yml 102 | */ 103 | protected function writeAppsettings() 104 | { 105 | $app = Bootstrap::getApplication(); 106 | $settings = $app['settings']; 107 | 108 | $file = WPBOOT_BASEPATH . '/appsettings.yml'; 109 | $dumper = new Dumper(); 110 | file_put_contents($file, $dumper->dump($settings, 4)); 111 | } 112 | 113 | protected function output($output) 114 | { 115 | $app = Bootstrap::getApplication(); 116 | $cliutils = $app['cliutils']; 117 | 118 | if (count($output) > 0) { 119 | $cliutils->format_items( 120 | $this->getAssocArg('format', 'table'), 121 | $output, 122 | array_keys($output[0]) 123 | ); 124 | } 125 | } 126 | 127 | } -------------------------------------------------------------------------------- /src/Commands/Menus.php: -------------------------------------------------------------------------------- 1 | args = $args; 26 | $this->assocArgs = $assocArgs; 27 | 28 | $app = Bootstrap::getApplication(); 29 | 30 | $menus = $this->getJsonList('menu list --format=json', $args, $assocArgs); 31 | $managedMenus = array(); 32 | if (isset($app['settings']['content']['menus'])) { 33 | $managedMenus = $app['settings']['content']['menus']; 34 | } 35 | $output = array(); 36 | foreach ($menus as $menu) { 37 | $fldManaged = 'No'; 38 | if (in_array($menu->slug, $managedMenus)) { 39 | $fldManaged = 'Yes'; 40 | } 41 | 42 | $row = array(); 43 | foreach ($menu as $fieldName => $fieldValue) { 44 | $row[$fieldName] = $fieldValue; 45 | } 46 | $row['Managed'] = $fldManaged; 47 | $output[] = $row; 48 | } 49 | 50 | $this->output($output); 51 | } 52 | 53 | /** 54 | * Add a menu to be managed by WP Bootstrap 55 | * 56 | * ... 57 | * :One or more menus, identified by their (slug) or term id, to be added 58 | * 59 | * @param $args 60 | * @param $assocArgs 61 | */ 62 | public function add($args, $assocArgs) 63 | { 64 | $app = Bootstrap::getApplication(); 65 | $cli = $app['cli']; 66 | 67 | $menus = $this->getJsonList('menu list --format=json', array(), $assocArgs); 68 | 69 | foreach ($args as $menuIdentifier) { 70 | $menu = $this->getMenuSlug($menuIdentifier, $menus); 71 | if ($menu) { 72 | $this->updateSettings(array('content','menus', $menu->slug)); 73 | $this->writeAppsettings(); 74 | } else { 75 | $cli->warning("Menu $menuIdentifier not found\n"); 76 | } 77 | } 78 | } 79 | 80 | /** 81 | * @param $menuIdentifier 82 | * @param $menus 83 | * @return bool 84 | */ 85 | private function getMenuSlug($menuIdentifier, $menus) 86 | { 87 | foreach ($menus as $menu) { 88 | if (is_numeric($menuIdentifier)) { 89 | $hit = $menuIdentifier == $menu->term_id ? true : false; 90 | } else { 91 | $hit = $menuIdentifier == $menu->slug ? true : false; 92 | } 93 | 94 | if ($hit) { 95 | return $menu; 96 | } 97 | } 98 | 99 | return false; 100 | } 101 | } -------------------------------------------------------------------------------- /src/Commands/OptionSnap.php: -------------------------------------------------------------------------------- 1 | baseFolder = WPBOOT_BASEPATH . '/bootstrap/snapshots'; 34 | } 35 | 36 | /** 37 | * Grab a snapshot of the current option table and store to disk 38 | * 39 | * ## OPTIONS 40 | * 41 | * 42 | * : A name for the new snapshot 43 | * 44 | * [--comment=] 45 | * : A comment. I.e "before installing plugin Foobar" 46 | * 47 | * @param $args 48 | * @param $assocArgs 49 | * 50 | */ 51 | public function snap($args, $assocArgs) 52 | { 53 | $app = Bootstrap::getApplication(); 54 | $cli = $app['cli']; 55 | list($name) = $args; 56 | $comment = isset($assocArgs['comment'])?$assocArgs['comment']:''; 57 | 58 | $file = "{$this->baseFolder}/$name.snapshot"; 59 | if (file_exists($file)) { 60 | $cli->error("Snapshot $name already exists"); 61 | return; 62 | } 63 | 64 | $snapshot = new \stdClass(); 65 | $snapshot->name = $name; 66 | $snapshot->created = date('Y-m-d H:i:s'); 67 | $snapshot->environment = $app['environment']; 68 | $snapshot->host = php_uname('n'); 69 | $snapshot->options = $this->getOptionsSnapshot(); 70 | $snapshot->comment = $comment; 71 | 72 | if (!file_exists($this->baseFolder)) { 73 | @mkdir($this->baseFolder, 0777, true); 74 | } 75 | file_put_contents($file, serialize($snapshot)); 76 | } 77 | 78 | /** 79 | * List all existing snapshots 80 | * 81 | * @param $args 82 | * @param $assocArgs 83 | * 84 | * @subcommand list 85 | */ 86 | public function listSnapshots($args, $assocArgs) 87 | { 88 | $app = Bootstrap::getApplication(); 89 | $helpers = $app['helpers']; 90 | $cliutils = $app['cliutils']; 91 | 92 | $snapshots = $helpers->getFiles($this->baseFolder); 93 | $output = array(); 94 | foreach ($snapshots as $snapshotFile) { 95 | $snapshot = unserialize(file_get_contents($this->baseFolder.'/'.$snapshotFile)); 96 | $output[] = array( 97 | 'name' => $snapshot->name, 98 | 'created' => $snapshot->created, 99 | 'environment' => $snapshot->environment, 100 | 'host' => $snapshot->host, 101 | 'comment' => $snapshot->comment, 102 | ); 103 | } 104 | if (count($output) > 0) { 105 | $cliutils->format_items('table', $output, array_keys($output[0])); 106 | } 107 | } 108 | 109 | /** 110 | * Shows all modified options between the current WordPress install or between two snapshots 111 | * 112 | * 113 | * ## OPTIONS 114 | * 115 | * ... 116 | * : Name of the snapshot to compare current WordPress options against, 117 | * If a second is passed in, the diff diff will be between and 118 | * 119 | * @param $args 120 | * @param $assocArgs 121 | * 122 | */ 123 | public function diff($args, $assocArgs) 124 | { 125 | $app = Bootstrap::getApplication(); 126 | $cli = $app['cli']; 127 | $cliutils = $app['cliutils']; 128 | 129 | $oldState = false; 130 | $newState = false; 131 | if (count($args) == 1) { 132 | $oldState = $this->readSnapshot($args[0]); 133 | if (!$oldState) { 134 | $cli->error("There's no snapshot file for {$args[0]}. Aborting"); 135 | return; 136 | } 137 | $newState = new \stdClass(); 138 | $newState->name = '[current state]'; 139 | $newState->created = 'just now'; 140 | $newState->options = $this->getOptionsSnapshot(); 141 | } 142 | if (count($args) > 1) { 143 | $oldState = $this->readSnapshot($args[0]); 144 | if (!$oldState) { 145 | $cli->error("There's no snapshot file for {$args[0]}. Aborting"); 146 | return; 147 | } 148 | $newState = $this->readSnapshot($args[1]); 149 | if (!$newState) { 150 | $cli->error("There's no snapshot file for {$args[1]}. Aborting"); 151 | return; 152 | } 153 | } 154 | 155 | $diff = $this->internalDiff($oldState, $newState); 156 | $cli->line("Comparing snapshot {$oldState->name}, created {$oldState->created} with "); 157 | $cli->line("snapshot {$newState->name}, created {$newState->created}"); 158 | 159 | if (count($diff) > 0) { 160 | $cliutils->format_items('table', $diff, array_keys($diff[0])); 161 | } else { 162 | $cli->line('No new, removed or changed options.'); 163 | } 164 | } 165 | 166 | 167 | /** 168 | * Show all options and values contained in a snapshot, or an individual option 169 | * 170 | * ## OPTIONS 171 | * 172 | * ... 173 | * : Name of the snapshot to show 174 | * If a second is passed in, shows the option named 175 | * Objects and arrays will be shown json encoded 176 | * 177 | * @param $args 178 | * @param $assocArgs 179 | * 180 | */ 181 | public function show($args, $assocArgs) 182 | { 183 | $app = Bootstrap::getApplication(); 184 | $cli = $app['cli']; 185 | $cliutils = $app['cliutils']; 186 | $helpers = $app['helpers']; 187 | 188 | $oldState = $this->readSnapshot($args[0]); 189 | if (!$oldState) { 190 | $cli->error("There's no snapshot file for {$args[0]}. Aborting"); 191 | return; 192 | } 193 | 194 | $wpCfmSettings = $helpers->getWPCFMSettings(); 195 | 196 | if (count($args) == 1) { 197 | $options = array(); 198 | foreach ($oldState->options as $name => $value) { 199 | if (in_array($name, $this->excludedOptions)) { 200 | continue; 201 | } 202 | $options[] = array( 203 | 'name' => $name, 204 | 'value' => $this->valueToString($value), 205 | 'managed' => isset($wpCfmSettings->$name) ? 'Yes' : 'No', 206 | ); 207 | } 208 | if (count($options) > 0) { 209 | $cliutils->format_items('table', $options, array_keys($options[0])); 210 | } 211 | } else { 212 | $name = $args[1]; 213 | $value = $oldState->options[$name]; 214 | if (is_object($value) || is_array($value)) { 215 | $cli->line($helpers->prettyPrint(json_encode($value))); 216 | } else { 217 | $cli->line($value); 218 | } 219 | 220 | } 221 | } 222 | 223 | /** 224 | * Finds all new, modified and removed options between two snapshots 225 | * 226 | * @param \stdClass $oldState 227 | * @param \stdClass $newState 228 | * @return array 229 | */ 230 | private function internalDiff($oldState, $newState) 231 | { 232 | $added = array(); 233 | $modified = array(); 234 | $removed = array(); 235 | $oldName = $oldState->name; 236 | $newName = $newState->name; 237 | 238 | $app = Bootstrap::getApplication(); 239 | $helpers = $app['helpers']; 240 | $wpCfmSettings = $helpers->getWPCFMSettings(); 241 | 242 | foreach ($oldState->options as $name => $value) { 243 | if (in_array($name, $this->excludedOptions)) { 244 | continue; 245 | } 246 | if (isset($newState->options[$name])) { 247 | if (md5(serialize($value)) != md5(serialize($newState->options[$name]))) { 248 | $modified[] = array( 249 | 'state' => 'MOD', 250 | 'name' => $name, 251 | $oldName => $this->valueToString($value), 252 | $newName => $this->valueToString($newState->options[$name]), 253 | 'managed' => isset($wpCfmSettings->$name) ? 'Yes' : 'No', 254 | ); 255 | } 256 | } else { 257 | $removed[] = array( 258 | 'state' => 'DEL', 259 | 'name' => $name, 260 | $oldName => $this->valueToString($value), 261 | $newName => null, 262 | 'managed' => isset($wpCfmSettings->$name) ? 'Yes' : 'No', 263 | ); 264 | } 265 | } 266 | foreach ($newState->options as $name => $value) { 267 | if (in_array($name, $this->excludedOptions)) { 268 | continue; 269 | } 270 | if (!isset($oldState->options[$name])) { 271 | $added[] = array( 272 | 'state' => 'NEW', 273 | 'name' => $name, 274 | $oldName => null, 275 | $newName => $this->valueToString($value), 276 | 'managed' => isset($wpCfmSettings->$name) ? 'Yes' : 'No', 277 | ); 278 | } 279 | } 280 | 281 | return array_merge($added, $modified, $removed); 282 | } 283 | 284 | /** 285 | * Formats any value in a terminal output friendly way 286 | * 287 | * @param mixed $value 288 | * @return mixed|string 289 | */ 290 | private function valueToString($value) 291 | { 292 | if (gettype($value) == 'object' || is_array($value)) { 293 | $ret = print_r($value, true); 294 | } else { 295 | $ret = (string) $value; 296 | } 297 | if (strlen($ret) > self::MAX_STRLEN) { 298 | $ret = substr($ret, 0, self::MAX_STRLEN - 3).'...'; 299 | } 300 | $ret = str_replace("\n", '', $ret); 301 | $ret = str_replace(' ', '', $ret); 302 | 303 | return $ret; 304 | } 305 | 306 | /** 307 | * Reads a snapshot from file 308 | * 309 | * @param string $name 310 | * @return mixed|null 311 | */ 312 | private function readSnapshot($name) 313 | { 314 | $snapshotFile = $name.'.snapshot'; 315 | if (!file_exists($this->baseFolder.'/'.$snapshotFile)) { 316 | return null; 317 | } 318 | 319 | return unserialize(file_get_contents($this->baseFolder.'/'.$snapshotFile)); 320 | } 321 | 322 | /** 323 | * Get all current options from WordPress 324 | * 325 | * @return array 326 | */ 327 | private function getOptionsSnapshot() 328 | { 329 | global $wpdb; 330 | wp_cache_delete('alloptions', 'options'); 331 | $allOptions = $wpdb->get_col( 332 | "SELECT option_name from $wpdb->options 333 | WHERE option_name NOT like '\_%';" 334 | ); 335 | $options = array(); 336 | foreach ($allOptions as $optionName) { 337 | if (!in_array($optionName, $this->excludedOptions)) { 338 | $options[$optionName] = get_option($optionName); 339 | } 340 | } 341 | 342 | return $options; 343 | } 344 | } 345 | -------------------------------------------------------------------------------- /src/Commands/Posts.php: -------------------------------------------------------------------------------- 1 | ] 18 | * : Limit output to post-type = 19 | * 20 | * @subcommand list 21 | * 22 | * @param $args 23 | * @param $assocArgs 24 | * 25 | */ 26 | public function listItems($args, $assocArgs) 27 | { 28 | $this->args = $args; 29 | $this->assocArgs = $assocArgs; 30 | 31 | $app = Bootstrap::getApplication(); 32 | $fields = 'ID,post_title,post_name,post_date,post_status,post_type,post_name'; 33 | 34 | $postTypes = $this->getAssocArg('post_type', 'all'); 35 | 36 | if ($postTypes == 'all') { 37 | $postTypes = get_post_types(); 38 | unset($postTypes['revision']); 39 | unset($postTypes['nav_menu_item']); 40 | $assocArgs['post_type'] = join(',', $postTypes); 41 | } 42 | $assocArgs['fields'] = $fields; 43 | 44 | $posts = $this->getJsonList('post list --format=json', $args, $assocArgs); 45 | $managedPosts = array(); 46 | if (isset($app['settings']['content']['posts'])) { 47 | $managedPosts = $app['settings']['content']['posts']; 48 | } 49 | 50 | $output = array(); 51 | foreach ($posts as $post) { 52 | $fldManaged = 'No'; 53 | if (isset($managedPosts[$post->post_type])) { 54 | if (is_array($managedPosts[$post->post_type])) { 55 | $fldManaged = in_array($post->post_name, $managedPosts[$post->post_type])?'Yes':'No'; 56 | } else { 57 | if ($managedPosts[$post->post_type] == '*') { 58 | $fldManaged = 'Yes'; 59 | } 60 | } 61 | } 62 | 63 | $row = array(); 64 | foreach ($post as $fieldName => $fieldValue) { 65 | $row[$fieldName] = $fieldValue; 66 | } 67 | $row['Managed'] = $fldManaged; 68 | $output[] = $row; 69 | } 70 | 71 | $this->output($output); 72 | } 73 | 74 | /** 75 | * Add a post to be managed by WP Bootstrap 76 | * 77 | * ... 78 | * :One or more post_name (slug) or post id's to be added 79 | * 80 | * [--post_type=] 81 | * :When adding a posts by post name, limit the post search 82 | * to single post-type. 83 | * 84 | * @param $args 85 | * @param $assocArgs 86 | */ 87 | public function add($args, $assocArgs) 88 | { 89 | $app = Bootstrap::getApplication(); 90 | $cli = $app['cli']; 91 | 92 | foreach ($args as $postIdentifier) { 93 | if (is_numeric($postIdentifier)) { 94 | $post = get_post($postIdentifier); 95 | } else { 96 | $post = false; 97 | $args = array('name' => $postIdentifier, 'post_type' => 'any'); 98 | $posts = get_posts($args); 99 | if (is_array($posts)) { 100 | $post = reset($posts); 101 | } 102 | } 103 | 104 | if ($post) { 105 | $this->updateSettings( 106 | array('content','posts', $post->post_type, $post->post_name) 107 | ); 108 | $this->writeAppsettings(); 109 | } else { 110 | $cli->warning("Post $postIdentifier not found\n"); 111 | } 112 | } 113 | } 114 | } -------------------------------------------------------------------------------- /src/Commands/Reset.php: -------------------------------------------------------------------------------- 1 | log('Running Bootstrap::reset'); 15 | 16 | $resp = "y\n"; 17 | if (!isset($assocArgs['yes'])) { 18 | $cli->line("*************************************************************************************"); 19 | $cli->line("**"); 20 | $cli->line("** WARNING! WARNING! WARNING! WARNING! WARNING! WARNING! WARNING! **"); 21 | $cli->line("**"); 22 | $cli->line("*************************************************************************************"); 23 | $cli->line("The WordPress installation located in {$app['path']} will be removed"); 24 | $resp = $cli->confirm("Are you sure? Hit Y to go ahead, anything else to cancel"); 25 | } 26 | 27 | if (strtolower($resp) != "y\n") { 28 | $cli->line("Aborted"); 29 | return; 30 | } 31 | 32 | // Reset the DB 33 | $cli->run_command(array('db', 'reset'), array('yes' => 1)); 34 | 35 | // Remove all files 36 | $cmd = 'rm -rf '. $app['path'] .'/*'; 37 | $cli->launch($cmd); 38 | } 39 | } -------------------------------------------------------------------------------- /src/Commands/SetEnv.php: -------------------------------------------------------------------------------- 1 | 23 | * : The name of the environment to set. Typically matched by a .env- file in the project root 24 | * 25 | * @param $args 26 | * @param $assocArgs 27 | * 28 | * @when before_wp_load 29 | */ 30 | public function __invoke($args, $assocArgs) 31 | { 32 | $environment = $args[0]; 33 | 34 | if (file_exists(WPBOOT_BASEPATH . "/.env")) { 35 | $dotEnv = new Dotenv(WPBOOT_BASEPATH); 36 | $dotEnv->load(); 37 | } 38 | 39 | $file = '.env-' . $environment; 40 | if (file_exists(WPBOOT_BASEPATH . "/$file")) { 41 | $dotEnv = new Dotenv(WPBOOT_BASEPATH, $file); 42 | $dotEnv->overload(); 43 | } 44 | 45 | try { 46 | $dotEnv = new Dotenv(__DIR__); 47 | $dotEnv->required('wppath'); 48 | } catch (\Exception $e) { 49 | echo $e->getMessage() . "\n"; 50 | return; 51 | } 52 | 53 | $runner = WP_CLI::get_runner(); 54 | $ymlPath = $runner->project_config_path; 55 | 56 | $yaml = new Yaml(); 57 | $config = $yaml->parse(file_get_contents($ymlPath)); 58 | $config['path'] = $_ENV['wppath']; 59 | $config['environment'] = $environment; 60 | 61 | $dumper = new Dumper(); 62 | file_put_contents($ymlPath, $dumper->dump($config, 2)); 63 | } 64 | } -------------------------------------------------------------------------------- /src/Commands/Setup.php: -------------------------------------------------------------------------------- 1 | app = Bootstrap::getApplication(); 45 | $this->cli = $this->app['cli']; 46 | $this->cli->log('Running Bootstrap::setup'); 47 | 48 | $this->checkAlreadyInstalled(); 49 | 50 | $this->parseInstallables('plugin', 'standard'); 51 | $this->parseInstallables('plugin', 'local'); 52 | $this->parseInstallables('plugin', 'localcopy'); 53 | $this->parseInstallables('theme', 'standard'); 54 | $this->parseInstallables('theme', 'local'); 55 | $this->parseInstallables('theme', 'localcopy'); 56 | $this->resolveInstallOrder(); 57 | 58 | $this->installAll(); 59 | 60 | $this->applySettings(); 61 | $this->wpContentSymlinks(); 62 | } 63 | 64 | /** 65 | * Read a section of the appSettings and collect info 66 | * about all identified installables 67 | * 68 | * @param string $type 69 | * @param string $path 70 | */ 71 | private function parseInstallables($type, $path) 72 | { 73 | $settings = $this->app['settings']; 74 | $base = $type . 's'; 75 | 76 | if (isset($settings[$base][$path]) && is_array($settings[$base][$path])) { 77 | foreach ($settings[$base][$path] as $installable) { 78 | if (is_string($installable)) { 79 | $this->addInstallableString($type, $path, $installable); 80 | } else { 81 | $this->addInstallableArray($type, $path, $installable); 82 | } 83 | } 84 | } 85 | } 86 | 87 | /** 88 | * Add an installable plugin or theme based on an array 89 | * definition 90 | * 91 | * @param string $type 92 | * @param string $path 93 | * @param array $arr 94 | */ 95 | private function addInstallableArray($type, $path, $arr) 96 | { 97 | $definition = reset($arr); 98 | $slug = key($arr); 99 | 100 | $obj = new \stdClass(); 101 | $obj->type = $type; 102 | $obj->path = $path; 103 | $obj->slug = isset($definition['slug']) ? $definition['slug']:$slug; 104 | $obj->version = isset($definition['version']) ? $definition['version']:null; 105 | $obj->requires = new \stdClass(); 106 | $obj->requires->plugins = []; 107 | $obj->requires->themes = []; 108 | 109 | if (isset($definition['requires']['plugins'])) { 110 | $obj->requires->plugins = $definition['requires']['plugins']; 111 | } 112 | 113 | if (isset($definition['requires']['themes'])) { 114 | $obj->requires->themes = $definition['requires']['themes']; 115 | } 116 | 117 | $id = $type . ':' . $obj->slug; 118 | $this->installables[$id] = $obj; 119 | } 120 | 121 | /** 122 | * Add an installable plugin or theme based on a string 123 | * definition 124 | * 125 | * @param string $type 126 | * @param string $path 127 | * @param string $definition 128 | 129 | */ 130 | private function addInstallableString($type, $path, $definition) 131 | { 132 | $helpers = $this->app['helpers']; 133 | 134 | $obj = new \stdClass(); 135 | $obj->type = $type; 136 | $obj->path = $path; 137 | $obj->version = null; 138 | $obj->requires = new \stdClass(); 139 | $obj->requires->plugins = []; 140 | $obj->requires->themes = []; 141 | 142 | if ($helpers->isUrl($definition)) { 143 | $obj->slug = $definition; 144 | $obj->url = $definition; 145 | } else { 146 | $parts = explode(':', $definition); 147 | if (count($parts) == 1) { 148 | $obj->slug = $definition; 149 | } else { 150 | $obj->slug = $parts[0]; 151 | $obj->version = $parts[1]; 152 | } 153 | } 154 | 155 | $id = $type . ':' . $obj->slug; 156 | $this->installables[$id] = $obj; 157 | } 158 | 159 | /** 160 | * Make sure all installable objects are treated in order 161 | */ 162 | private function resolveInstallOrder() 163 | { 164 | $unordered = $this->installables; 165 | $ordered = []; 166 | 167 | $added = 99; 168 | while (count($ordered) < count($unordered) && $added > 0) { 169 | $added = 0; 170 | foreach ($unordered as $key => $obj) { 171 | if (isset($ordered[$key])) { 172 | continue; 173 | } 174 | 175 | $ok = true; 176 | foreach ($obj->requires->plugins as $plugin) { 177 | $id = 'plugin:' . $plugin; 178 | if (!isset($unordered[$id]) && !in_array($id, $this->installedPlugins)) { 179 | $this->cli->error( 180 | "Unresolvable plugin dependency. $id is not installed or defined in settings" 181 | ); 182 | } 183 | if (!isset($ordered[$id])) { 184 | $ok = false; 185 | } 186 | } 187 | foreach ($obj->requires->themes as $theme) { 188 | $id = 'theme:' . $theme; 189 | if (!isset($unordered[$id]) && !in_array($id, $this->installedThemes)) { 190 | $this->cli->error( 191 | "Unresolvable plugin dependency. $id is not installed or defined in settings" 192 | ); 193 | } 194 | if (!isset($ordered[$id])) { 195 | $ok = false; 196 | } 197 | } 198 | if ($ok) { 199 | $ordered[$key] = $obj; 200 | $added++; 201 | } elseif (!isset($unordered[$key])) { 202 | return false; 203 | } 204 | } 205 | } 206 | 207 | if (count($ordered) < count($unordered)) { 208 | $this->cli->error("Unresolvable dependencies, do you have a circular reference?"); 209 | } 210 | 211 | $this->installables = $ordered; 212 | return true; 213 | } 214 | 215 | private function installAll() 216 | { 217 | foreach ($this->installables as $key => $installable) { 218 | switch ($installable->path) { 219 | case 'standard': 220 | $this->installStandard($installable); 221 | break; 222 | case 'local': 223 | $this->installLocal($installable, true); 224 | break; 225 | case 'localcopy': 226 | $this->installLocal($installable, false); 227 | break; 228 | } 229 | } 230 | 231 | if (isset($this->app['settings']['themes']['active'])) { 232 | $theme = $this->app['settings']['themes']['active']; 233 | $this->cli->run_command(array('theme', 'activate', $theme)); 234 | } 235 | } 236 | 237 | 238 | /** 239 | * @param \stdClass $installable 240 | */ 241 | private function installStandard($installable) 242 | { 243 | if ($installable->type == 'plugin' && in_array($installable->slug, $this->installedPlugins)) { 244 | $this->cli->debug("Skipping plugin {$installable->slug}. Already installed"); 245 | return; 246 | } 247 | if ($installable->type == 'theme' && in_array($installable->slug, $this->installedThemes)) { 248 | $this->cli->debug("Skipping theme {$installable->slug}. Already installed"); 249 | return; 250 | } 251 | 252 | $cmd = $installable->type . ' install'; 253 | $args = array($installable->slug); 254 | $assocArgs = array(); 255 | if ($installable->type == 'plugin') { 256 | $assocArgs['activate'] = 1; 257 | } 258 | if ($installable->version) { 259 | $assocArgs['version'] = $installable->version; 260 | } 261 | $this->cli->launch_self($cmd, $args, $assocArgs); 262 | } 263 | 264 | /** 265 | * 266 | * @param \stdClass $installable 267 | * @param bool $symlink 268 | */ 269 | private function installLocal($installable, $symlink = true) 270 | { 271 | $path = 'plugins'; 272 | if ($installable->type == 'theme') { 273 | $path = 'themes'; 274 | } 275 | 276 | $cmd = sprintf('rm -rf %s/wp-content/%s/%s', $this->app['path'], $path, $installable->slug); 277 | $this->cli->launch($cmd); 278 | 279 | $cmdTemplate = 'ln -s %s/wp-content/%s/%s %s/wp-content/%s/%s'; 280 | if (!$symlink) { 281 | $cmdTemplate = 'cp -a %s/wp-content/%s/%s %s/wp-content/%s/%s'; 282 | } 283 | 284 | $cmd = sprintf( 285 | $cmdTemplate, 286 | WPBOOT_BASEPATH, 287 | $path, 288 | $installable->slug, 289 | $this->app['path'], 290 | $path, 291 | $installable->slug 292 | ); 293 | $this->cli->launch($cmd); 294 | 295 | if ($installable->type == 'plugin') { 296 | $args = array($installable->slug); 297 | $cmd = 'plugin activate'; 298 | $this->cli->launch_self($cmd, $args, array()); 299 | } 300 | } 301 | 302 | /** 303 | * Apply settings defined in appsettings 304 | */ 305 | private function applySettings() 306 | { 307 | $settings = $this->app['settings']; 308 | if (isset($settings['settings'])) { 309 | foreach ($settings['settings'] as $key => $value) { 310 | $this->cli->run_command(array( 311 | 'option', 312 | 'update', 313 | $key, 314 | $value, 315 | )); 316 | } 317 | } 318 | } 319 | 320 | private function wpContentSymlinks() 321 | { 322 | $settings = $this->app['settings']; 323 | if (isset($settings['symlinks'])) { 324 | foreach ($settings['symlinks'] as $symlink) { 325 | if (!file_exists(WPBOOT_BASEPATH . '/wp-content/' . $symlink)) { 326 | continue; 327 | } 328 | $cmd = sprintf( 329 | 'rm -rf %s/wp-content/%s', 330 | $this->app['path'], 331 | $symlink 332 | ); 333 | $this->cli->run_command($cmd); 334 | 335 | $cmd = sprintf( 336 | 'ln -s %s/wp-content/%s %s/wp-content/%s', 337 | WPBOOT_BASEPATH, 338 | $symlink, 339 | $this->app['path'], 340 | $symlink 341 | ); 342 | $this->cli->run_command($cmd); 343 | } 344 | } 345 | } 346 | 347 | /** 348 | * Check the WordPress installation for existing themes and plugins 349 | */ 350 | private function checkAlreadyInstalled() 351 | { 352 | $this->installedPlugins = []; 353 | $items = $this->getJsonList('plugin list', array(), array('format' => 'json')); 354 | array_walk($items, function ($item) { 355 | $this->installedPlugins[] = $item->name; 356 | }); 357 | 358 | $this->installedThemes= []; 359 | $items = $this->getJsonList('theme list', array(), array('format' => 'json')); 360 | array_walk($items, function ($item) { 361 | $this->installedThemes[] = $item->name; 362 | }); 363 | } 364 | 365 | private function getJsonList($cmd, $args = array(), $assocArgs = array()) 366 | { 367 | $ret = $this->cli->launch_self($cmd, $args, $assocArgs, false, true); 368 | 369 | if ($ret->return_code == 0) { 370 | return json_decode($ret->stdout); 371 | } 372 | 373 | return array(); 374 | } 375 | } -------------------------------------------------------------------------------- /src/Commands/Taxonomies.php: -------------------------------------------------------------------------------- 1 | ... 18 | * :List terms of one or more taxonomies 19 | * 20 | * [--format=] 21 | * :Accepted values: table, csv, json, count, ids, yaml. Default: table 22 | * 23 | * @subcommand list 24 | * 25 | * @param $args 26 | * @param $assocArgs 27 | * 28 | */ 29 | public function listItems($args, $assocArgs) 30 | { 31 | $this->args = $args; 32 | $this->assocArgs = $assocArgs; 33 | 34 | $app = Bootstrap::getApplication(); 35 | $terms = $this->getTerms(); 36 | 37 | $managedTerms = array(); 38 | if (isset($app['settings']['content']['taxonomies'])) { 39 | $managedTerms = $app['settings']['content']['taxonomies']; 40 | } 41 | 42 | $output = array(); 43 | foreach ($terms as $term) { 44 | $fldManaged = 'No'; 45 | if (isset($managedTerms[$term->taxonomy])) { 46 | if (is_array($managedTerms[$term->taxonomy])) { 47 | $fldManaged = in_array($term->slug, $managedTerms[$term->taxonomy])?'Yes':'No'; 48 | } else { 49 | if ($managedTerms[$term->taxonomy] == '*') { 50 | $fldManaged = 'Yes'; 51 | } 52 | } 53 | } 54 | 55 | $row = array(); 56 | foreach ($term as $fieldName => $fieldValue) { 57 | $row[$fieldName] = $fieldValue; 58 | } 59 | $row['Managed'] = $fldManaged; 60 | $output[] = $row; 61 | } 62 | 63 | $this->output($output); 64 | 65 | } 66 | 67 | /** 68 | * Add a post to be managed by WP Bootstrap 69 | * 70 | * 71 | * :The name of the taxonomy 72 | * 73 | * ... 74 | * :One or more term slugs or term id's to be added 75 | * 76 | * [--post_type=] 77 | * :When adding a posts by post name, limit the post search 78 | * to single post-type. 79 | * 80 | * @param $args 81 | * @param $assocArgs 82 | */ 83 | public function add($args, $assocArgs) 84 | { 85 | $this->args = $args; 86 | $this->assocArgs = $assocArgs; 87 | 88 | $app = Bootstrap::getApplication(); 89 | $cli = $app['cli']; 90 | $taxonomy = array_shift($args); 91 | 92 | foreach ($args as $termIdentifier) { 93 | $slug = false; 94 | 95 | if ($termIdentifier != '*') { 96 | if (is_numeric($termIdentifier)) { 97 | $term = get_term_by('id', $termIdentifier, $taxonomy); 98 | } else { 99 | $term = get_term_by('slug', $termIdentifier, $taxonomy); 100 | } 101 | if ($term) { 102 | $slug = $term->slug; 103 | } 104 | } else { 105 | $slug = $termIdentifier; 106 | } 107 | 108 | if ($slug) { 109 | $this->updateSettings( 110 | array('content','taxonomies', $taxonomy, $slug) 111 | ); 112 | } else { 113 | $cli->warning("Term $termIdentifier not found"); 114 | return; 115 | } 116 | } 117 | $this->writeAppsettings(); 118 | } 119 | 120 | private function getTerms() 121 | { 122 | global $wp_version; 123 | if (version_compare($wp_version, '4.5', '>=')) { 124 | $terms = get_terms(array( 125 | 'taxonomy' => $this->args, 126 | 'hide_empty' => false, 127 | )); 128 | return $terms; 129 | } else { 130 | $terms = get_terms($this->args, array( 131 | 'hide_empty' => false, 132 | )); 133 | 134 | return $terms; 135 | } 136 | } 137 | } -------------------------------------------------------------------------------- /src/Export/ExportMedia.php: -------------------------------------------------------------------------------- 1 | mediaIds = array_unique($this->mediaIds); 25 | $uploadDir = wp_upload_dir(); 26 | $dumper = new Dumper(); 27 | 28 | foreach ($this->mediaIds as $itemId) { 29 | $item = get_post($itemId, ARRAY_A); 30 | if ($item) { 31 | $meta = get_post_meta($itemId); 32 | $item['post_meta'] = $meta; 33 | 34 | $file = $meta['_wp_attached_file'][0]; 35 | 36 | $attachmentMeta = wp_get_attachment_metadata($itemId, true); 37 | $item['attachment_meta'] = $attachmentMeta; 38 | 39 | $dir = WPBOOT_BASEPATH.'/bootstrap/media/'.$item['post_name']; 40 | @mkdir($dir, 0777, true); 41 | file_put_contents($dir.'/meta', $dumper->dump($item, 4)); 42 | $src = $uploadDir['basedir'].'/'.$file; 43 | $trg = $dir.'/'.basename($file); 44 | if (file_exists($src)) { 45 | copy($src, $trg); 46 | } 47 | } 48 | } 49 | } 50 | 51 | /** 52 | * Add mediaId to the internal array 53 | * @param int|array $mediaId 54 | */ 55 | public function addMedia($mediaId) 56 | { 57 | $app = Bootstrap::getApplication(); 58 | $cli = $app['cli']; 59 | 60 | if (is_array($mediaId)) { 61 | $cli->debug('Including thumbnail media', $mediaId); 62 | $this->mediaIds = array_merge($this->mediaIds, $mediaId); 63 | } else { 64 | $cli->debug('Including thumbnail media', array($mediaId)); 65 | $this->mediaIds[] = $mediaId; 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/Export/ExportMenus.php: -------------------------------------------------------------------------------- 1 | readMenus(); 31 | $dumper = new Dumper(); 32 | remove_all_filters('wp_get_nav_menu_items'); 33 | 34 | $helpers = $app['helpers']; 35 | $exportTaxonomies = $app['exporttaxonomies']; 36 | $exportPosts = $app['exportposts']; 37 | $baseUrl = get_option('siteurl'); 38 | 39 | foreach ($settings['content']['menus'] as $menu) { 40 | $dir = WPBOOT_BASEPATH.'/bootstrap/menus/'.$menu; 41 | array_map('unlink', glob("$dir/*")); 42 | @mkdir($dir, 0777, true); 43 | 44 | $menuMeta = array(); 45 | $menuMeta['locations'] = array(); 46 | if (isset($this->navMenus[$menu])) { 47 | $menuMeta['locations'] = $this->navMenus[$menu]->locations; 48 | } 49 | 50 | $file = WPBOOT_BASEPATH."/bootstrap/menus/{$menu}_manifest"; 51 | file_put_contents($file, $dumper->dump($menuMeta, 4)); 52 | 53 | $menuItems = wp_get_nav_menu_items($menu); 54 | 55 | foreach ($menuItems as $menuItem) { 56 | $obj = get_post($menuItem->ID, ARRAY_A); 57 | $obj['post_meta'] = get_post_meta($obj['ID']); 58 | 59 | switch ($obj['post_meta']['_menu_item_type'][0]) { 60 | case 'post_type': 61 | $postType = $obj['post_meta']['_menu_item_object'][0]; 62 | $postId = $obj['post_meta']['_menu_item_object_id'][0]; 63 | $objPost = get_post($postId); 64 | $exportPosts->addPost($postType, $objPost->post_name); 65 | break; 66 | case 'taxonomy': 67 | $id = $obj['post_meta']['_menu_item_object_id'][0]; 68 | $taxonomy = $obj['post_meta']['_menu_item_object'][0]; 69 | $objTerm = get_term($id, $taxonomy); 70 | if (!is_wp_error($objTerm)) { 71 | $exportTaxonomies->addTerm($taxonomy, $objTerm->slug); 72 | } 73 | break; 74 | } 75 | $helpers->fieldSearchReplace($obj, $baseUrl, Bootstrap::NEUTRALURL); 76 | 77 | $file = $dir.'/'.$menuItem->post_name; 78 | file_put_contents($file, $dumper->dump($obj, 4)); 79 | } 80 | } 81 | } 82 | 83 | /** 84 | * Read all current nav menus into 85 | * a class member for later convenience 86 | */ 87 | private function readMenus() 88 | { 89 | $navMenus = wp_get_nav_menus(); 90 | $menuLocations = get_nav_menu_locations(); 91 | $this->navMenus = array(); 92 | foreach ($navMenus as $menu) { 93 | $menu->locations = array(); 94 | foreach ($menuLocations as $location => $termId) { 95 | if ($termId == $menu->term_id) { 96 | $menu->locations[] = $location; 97 | } 98 | } 99 | $this->navMenus[$menu->slug] = $menu; 100 | 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/Export/ExportOptions.php: -------------------------------------------------------------------------------- 1 | ensureBundleExists(); 24 | WPCFM()->readwrite->push_bundle('wpbootstrap'); 25 | } else { 26 | $cli->warning('Plugin WP-CFM does not seem to be installed. No options exported.'); 27 | $cli->warning('Add the WP-CFM plugin directly to this install using:'); 28 | $cli->warning('$ wp plugin install wp-cfm --activate'); 29 | $cli->warning(''); 30 | $cli->warning('...or to your appsettings using:'); 31 | $cli->warning('$ wp bootstrap add plugin wp-cfm'); 32 | $cli->warning('$ wp bootstrap setup'); 33 | return; 34 | } 35 | 36 | $src = $app['path'] . '/wp-content/config/wpbootstrap.json'; 37 | $trg = WPBOOT_BASEPATH . '/bootstrap/config/wpbootstrap.json'; 38 | if (file_exists($src)) { 39 | @mkdir(dirname($trg), 0777, true); 40 | copy($src, $trg); 41 | $cli->debug("Copied $src to $trg"); 42 | 43 | // read settings 44 | $settings = json_decode(file_get_contents($trg)); 45 | 46 | // sanity check 47 | $label = '.label'; 48 | if (is_null($settings->$label)) { 49 | $settings->$label = 'wpbootstrap'; 50 | } 51 | 52 | // look for media refrences in the included settings 53 | $extractMedia = $app['extractmedia']; 54 | $exportMedia = $app['exportmedia']; 55 | 56 | foreach ($settings as $name => $value) { 57 | $ret = $extractMedia->getReferencedMedia($value); 58 | if (count($ret) > 0) { 59 | $exportMedia->addMedia($ret); 60 | } 61 | } 62 | 63 | // neutralize 64 | $helpers = $app['helpers']; 65 | $baseUrl = get_option('siteurl'); 66 | $helpers->fieldSearchReplace($settings, $baseUrl, Bootstrap::NEUTRALURL); 67 | 68 | // save 69 | file_put_contents($trg, $helpers->prettyPrint(json_encode($settings))); 70 | } 71 | } 72 | 73 | /** 74 | * Checks if a wpbootstrap bundle exists in WP-CFM settings and creates one if needed 75 | */ 76 | private function ensureBundleExists() 77 | { 78 | $wpcfm = json_decode(get_option('wpcfm_settings', '{}')); 79 | if (!isset($wpcfm->bundles)) { 80 | $wpcfm->bundles = array(); 81 | } 82 | $found = false; 83 | foreach ($wpcfm->bundles as $bundle) { 84 | if ($bundle->name == 'wpbootstrap') { 85 | $found = true; 86 | } 87 | } 88 | if (!$found) { 89 | $bundle = new \stdClass(); 90 | $bundle->name = 'wpbootstrap'; 91 | $bundle->label = 'wpbootstrap'; 92 | $bundle->config = null; 93 | $wpcfm->bundles[] = $bundle; 94 | update_option('wpcfm_settings', json_encode($wpcfm)); 95 | } 96 | } 97 | } -------------------------------------------------------------------------------- /src/Export/ExportPosts.php: -------------------------------------------------------------------------------- 1 | posts = new \stdClass(); 32 | } 33 | 34 | /** 35 | * Export posts 36 | */ 37 | public function export() 38 | { 39 | $app = Bootstrap::getApplication(); 40 | $settings = $app['settings']; 41 | if (!isset($settings['content']['posts'])) { 42 | return; 43 | } 44 | 45 | $cli = $app['cli']; 46 | 47 | foreach ($settings['content']['posts'] as $postType => $posts) { 48 | if ($posts == '*') { 49 | $args = array( 50 | 'post_type' => $postType, 51 | 'posts_per_page' => -1, 52 | 'post_status' => array('publish', 'inherit') 53 | ); 54 | $allPosts = get_posts($args); 55 | foreach ($allPosts as $post) { 56 | $this->addPost($postType, $post->post_name); 57 | } 58 | } else { 59 | foreach ($posts as $post) { 60 | $this->addPost($postType, $post); 61 | } 62 | } 63 | } 64 | 65 | $cli->debug('Initiated ExportPosts'); 66 | $this->doExport(); 67 | } 68 | 69 | /** 70 | * Export the posts 71 | */ 72 | public function doExport() 73 | { 74 | global $wpdb; 75 | $app = Bootstrap::getApplication(); 76 | $cli = $app['cli']; 77 | $extractMedia = $app['extractmedia']; 78 | $exportMedia = $app['exportmedia']; 79 | $exportTaxonomies = $app['exporttaxonomies']; 80 | $helpers = $app['helpers']; 81 | $baseUrl = get_option('siteurl'); 82 | $dumper = new Dumper(); 83 | 84 | $count = 1; 85 | while ($count > 0) { 86 | $count = 0; 87 | foreach ($this->posts as $postType => &$posts) { 88 | foreach ($posts as &$post) { 89 | if ($post->done == true) { 90 | continue; 91 | } 92 | $postId = $wpdb->get_var($wpdb->prepare( 93 | "SELECT ID FROM $wpdb->posts WHERE post_type = %s AND post_name = %s", 94 | $postType, 95 | $post->slug 96 | )); 97 | $arrPost = get_post($postId, ARRAY_A); 98 | if (!$arrPost) { 99 | $cli->debug("Post $postId not found"); 100 | continue; 101 | } 102 | 103 | $cli->debug("Exporting post {$post->slug} ($postId)"); 104 | $meta = get_post_meta($arrPost['ID']); 105 | $arrPost['taxonomies'] = array(); 106 | 107 | // extract media ids 108 | // 1. from attached media: 109 | $media = get_attached_media('', $postId); 110 | foreach ($media as $item) { 111 | $exportMedia->addMedia($item->ID); 112 | } 113 | 114 | // 2. from featured image: 115 | if (has_post_thumbnail($postId)) { 116 | $mediaId = get_post_thumbnail_id($postId); 117 | $exportMedia->addMedia($mediaId); 118 | } 119 | 120 | // 3. Is this post actually an attachment? 121 | if ($arrPost['post_type'] == 'attachment') { 122 | $exportMedia->addMedia($arrPost['ID']); 123 | } 124 | 125 | // 4. referenced in the content 126 | $ret = $extractMedia->getReferencedMedia($arrPost); 127 | if (count($ret) > 0) { 128 | $exportMedia->addMedia($ret); 129 | } 130 | 131 | // 5. referenced in meta data 132 | $ret = $extractMedia->getReferencedMedia($meta); 133 | if (count($ret) > 0) { 134 | $exportMedia->addMedia($ret); 135 | } 136 | 137 | // terms 138 | foreach (get_taxonomies() as $taxonomy) { 139 | if (in_array($taxonomy, $this->excludedTaxonomies)) { 140 | continue; 141 | } 142 | $terms = wp_get_object_terms($postId, $taxonomy); 143 | if (count($terms)) { 144 | $cli->debug('Adding '.count($terms)." terms from $taxonomy"); 145 | } 146 | foreach ($terms as $objTerm) { 147 | // add it to the exported terms 148 | $exportTaxonomies->addTerm($taxonomy, $objTerm->slug); 149 | 150 | if (!isset($arrPost['taxonomies'][$taxonomy])) { 151 | $arrPost['taxonomies'][$taxonomy] = array(); 152 | } 153 | $arrPost['taxonomies'][$taxonomy][] = $objTerm->slug; 154 | } 155 | } 156 | 157 | $arrPost['post_meta'] = $meta; 158 | $helpers->fieldSearchReplace($arrPost, $baseUrl, Bootstrap::NEUTRALURL); 159 | 160 | $file = WPBOOT_BASEPATH."/bootstrap/posts/{$arrPost['post_type']}/{$arrPost['post_name']}"; 161 | $cli->debug("Storing $file"); 162 | @mkdir(dirname($file), 0777, true); 163 | file_put_contents($file, $dumper->dump($arrPost, 4)); 164 | $post->done = true; 165 | ++$count; 166 | } 167 | } 168 | } 169 | } 170 | 171 | /** 172 | * Add the post identified by post_type/slug to the internal array 173 | * 174 | * @param string $postType 175 | * @param string $slug 176 | */ 177 | public function addPost($postType, $slug) 178 | { 179 | if (!isset($this->posts->$postType)) { 180 | $this->posts->$postType = array(); 181 | } 182 | foreach ($this->posts->$postType as $post) { 183 | if ($post->slug == $slug) { 184 | return; 185 | } 186 | } 187 | 188 | $newPost = new \stdClass(); 189 | $newPost->slug = $slug; 190 | $newPost->done = false; 191 | array_push($this->posts->$postType, $newPost); 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /src/Export/ExportSidebars.php: -------------------------------------------------------------------------------- 1 | $widgetRef) { 42 | $parts = explode('-', $widgetRef); 43 | $ord = end($parts); 44 | $name = substr($widgetRef, 0, -1 * strlen('-'.$ord)); 45 | $widgetTypeSettings = get_option('widget_'.$name); 46 | $widgetSettings = $widgetTypeSettings[$ord]; 47 | 48 | $ret = $extractMedia->getReferencedMedia($widgetSettings); 49 | $exportMedia->addMedia($ret); 50 | 51 | $file = $dir.'/'.$widgetRef; 52 | $helpers->fieldSearchReplace($widgetSettings, $baseUrl, Bootstrap::NEUTRALURL); 53 | file_put_contents($file, $dumper->dump($widgetSettings, 4)); 54 | } 55 | 56 | $file = WPBOOT_BASEPATH."/bootstrap/sidebars/{$sidebar}_manifest"; 57 | file_put_contents($file, $dumper->dump($sidebarDef, 4)); 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Export/ExportTaxonomies.php: -------------------------------------------------------------------------------- 1 | taxonomies = new \stdClass(); 25 | } 26 | 27 | /** 28 | * Export Taxonomies 29 | */ 30 | public function export() 31 | { 32 | $app = Bootstrap::getApplication(); 33 | $settings = $app['settings']; 34 | $cli = $app['cli']; 35 | 36 | if (isset($settings['content']['taxonomies'])) { 37 | foreach ($settings['content']['taxonomies'] as $taxonomyName => $terms) { 38 | if (!isset($this->taxonomies->$taxonomyName)) { 39 | $this->taxonomies->$taxonomyName = new \stdClass(); 40 | } 41 | $this->taxonomies->$taxonomyName->termsDescriptor = $terms; 42 | $this->taxonomies->$taxonomyName->type = 'standard'; 43 | $this->taxonomies->$taxonomyName->terms = array(); 44 | if (is_object($terms)) { 45 | if (isset($terms->terms)) { 46 | $this->taxonomies->$taxonomyName->termsDescriptor = $terms->terms; 47 | } else { 48 | $cli->warning("No terms property defined on $taxonomyName, using *"); 49 | $this->taxonomies->$taxonomyName->termsDescriptor = '*'; 50 | } 51 | $this->taxonomies->$taxonomyName->type = $terms->type; 52 | } 53 | if ($this->taxonomies->$taxonomyName->termsDescriptor == '*') { 54 | $allTerms = get_terms($taxonomyName, array('hide_empty' => false)); 55 | foreach ($allTerms as $term) { 56 | $this->addTerm($taxonomyName, $term->slug); 57 | } 58 | } else { 59 | foreach ($terms as $term) { 60 | $this->addTerm($taxonomyName, $term); 61 | } 62 | } 63 | } 64 | } 65 | 66 | $this->doExport(); 67 | } 68 | 69 | /** 70 | * Export taxonomies 71 | */ 72 | private function doExport() 73 | { 74 | $app = Bootstrap::getApplication(); 75 | $cli = $app['cli']; 76 | $helpers = $app['helpers']; 77 | $dumper = new Dumper(); 78 | 79 | $count = 1; 80 | while ($count > 0) { 81 | $count = 0; 82 | 83 | foreach ($this->taxonomies as $taxonomyName => $taxonomy) { 84 | foreach ($taxonomy->terms as &$term) { 85 | if ($term->done == true) { 86 | continue; 87 | } 88 | 89 | $objTerm = get_term_by('slug', $term->slug, $taxonomyName, ARRAY_A); 90 | $file = WPBOOT_BASEPATH."/bootstrap/taxonomies/$taxonomyName/{$term->slug}"; 91 | @mkdir(dirname($file), 0777, true); 92 | file_put_contents($file, $dumper->dump($objTerm, 4)); 93 | 94 | if ($objTerm['parent']) { 95 | $parentTerm = get_term_by('id', $objTerm['parent'], $taxonomyName, ARRAY_A); 96 | $this->addTerm($taxonomyName, $parentTerm['slug']); 97 | } 98 | $term->done = true; 99 | ++$count; 100 | } 101 | } 102 | } 103 | 104 | foreach ($this->taxonomies as $taxonomyName => $taxonomy) { 105 | $manifestFile = WPBOOT_BASEPATH."/bootstrap/taxonomies/{$taxonomyName}_manifest"; 106 | $manifest = array(); 107 | $manifest['name'] = $taxonomyName; 108 | $manifest['type'] = $taxonomy->type; 109 | $manifest['termsDescriptor'] = $taxonomy->termsDescriptor; 110 | $cli->debug("Creating $taxonomyName manifest ".$manifestFile); 111 | file_put_contents($manifestFile, $dumper->dump($manifest, 4)); 112 | } 113 | } 114 | 115 | /** 116 | * Add the term identified by taxonomyName/slug to the internal array 117 | * 118 | * @param string $taxonomyName 119 | * @param string $slug 120 | */ 121 | public function addTerm($taxonomyName, $slug) 122 | { 123 | $app = Bootstrap::getApplication(); 124 | $cli = $app['cli']; 125 | 126 | $cli->debug("Adding term $slug to Taxonomy $taxonomyName"); 127 | if (!isset($this->taxonomies->$taxonomyName)) { 128 | $this->taxonomies->$taxonomyName = new \stdClass(); 129 | $this->taxonomies->$taxonomyName->type = 'standard'; 130 | $this->taxonomies->$taxonomyName->termsDescriptor = 'indirect'; 131 | $this->taxonomies->$taxonomyName->terms = array(); 132 | } 133 | foreach ($this->taxonomies->$taxonomyName->terms as $term) { 134 | if ($term->slug == $slug) { 135 | return; 136 | } 137 | } 138 | $newTerm = new \stdClass(); 139 | $newTerm->slug = $slug; 140 | $newTerm->done = false; 141 | array_push($this->taxonomies->$taxonomyName->terms, $newTerm); 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /src/Export/ExtractMedia.php: -------------------------------------------------------------------------------- 1 | uploadDir = wp_upload_dir(); 19 | } 20 | 21 | /** 22 | * Extracts id's of all attachments that this post/term/widget is referring to via 23 | * an url in the content 24 | * 25 | * @param \stdClass $obj 26 | * @return array 27 | */ 28 | public function getReferencedMedia($obj) 29 | { 30 | $links = $this->findMediaFromObj($obj); 31 | 32 | $ret = array(); 33 | foreach ($links as $link) { 34 | if (!$this->isImageUrl($link)) { 35 | continue; 36 | } 37 | 38 | $id = $this->getImageId($link); 39 | if (!$id) { 40 | $id = $this->getImageId($this->removeLastSizeIndicator($link)); 41 | } 42 | 43 | if ($id > 0) { 44 | $ret[] = $id; 45 | } 46 | } 47 | return $ret; 48 | } 49 | 50 | /** 51 | * Identifies links to images/attachments on WordPress installation 52 | * 53 | * @param \stdClass $obj 54 | * @return array 55 | */ 56 | private function findMediaFromObj($obj) 57 | { 58 | $ret = array(); 59 | $app = Bootstrap::getApplication(); 60 | $helpers = $app['helpers']; 61 | 62 | $b64 = $helpers->isBase64($obj); 63 | if ($b64) { 64 | $obj = base64_decode($obj); 65 | } 66 | $ser = $helpers->isSerialized($obj); 67 | if ($ser) { 68 | $obj = unserialize($obj); 69 | } 70 | 71 | if (is_object($obj) || is_array($obj)) { 72 | foreach ($obj as $key => &$member) { 73 | $arr = $this->findMediaFromObj($member); 74 | $ret = array_merge($ret, $arr); 75 | } 76 | } else { 77 | if (is_string($obj)) { 78 | $base = rtrim($this->uploadDir['baseurl'], '/') . '/'; 79 | $pattern = '~('.preg_quote($base, '~').'[^.]+.\w\w\w\w?)~i'; 80 | preg_match_all($pattern, $obj, $matches); 81 | if (isset($matches[0]) && is_array($matches[0])) { 82 | $ret = array_merge($ret, $matches[0]); 83 | } 84 | } 85 | } 86 | 87 | return array_unique($ret); 88 | } 89 | 90 | /** 91 | * Removes the last (and only last) media size indicator at the end of an URL. 92 | * 93 | * @param string $link 94 | * @return string 95 | */ 96 | public function removeLastSizeIndicator($link) 97 | { 98 | $ret = $link; 99 | $pattern = '~-\d{1,}x\d{1,}~'; 100 | $hits = preg_match_all($pattern, $link, $matches, PREG_OFFSET_CAPTURE); 101 | if ($hits > 0) { 102 | $last = end($matches[0]); 103 | $ret = substr($link, 0, $last[1]).substr($link, $last[1] + strlen($last[0])); 104 | } 105 | 106 | return $ret; 107 | } 108 | 109 | /** 110 | * Returns the attachment/media id of an image by looking up the GUID 111 | * 112 | * @param string $link 113 | * @return mixed 114 | */ 115 | public function getImageId($link) 116 | { 117 | global $wpdb; 118 | $attachment = $wpdb->get_col( 119 | $wpdb->prepare( 120 | "SELECT ID FROM $wpdb->posts WHERE guid='%s';", 121 | $link 122 | ) 123 | ); 124 | 125 | if ($attachment[0]) { 126 | return $attachment[0]; 127 | } 128 | 129 | $attachment = $wpdb->get_col($wpdb->prepare( 130 | "SELECT post_id FROM $wpdb->postmeta WHERE meta_key='_wp_attached_file' AND meta_value='%s';", 131 | str_replace($this->uploadDir['baseurl'] . '/', '', $link) 132 | ) 133 | ); 134 | 135 | return $attachment[0]; 136 | 137 | } 138 | 139 | /** 140 | * Determines if a link is in fact a link to an image 141 | * 142 | * @param string $link 143 | * @return bool 144 | */ 145 | public function isImageUrl($link) 146 | { 147 | $ret = false; 148 | $imgExtensions = array('gif', 'jpg', 'jpeg', 'png', 'tiff', 'tif'); 149 | $urlParts = parse_url($link); 150 | $urlExt = strtolower(pathinfo($urlParts['path'], PATHINFO_EXTENSION)); 151 | if (in_array($urlExt, $imgExtensions)) { 152 | $ret = true; 153 | } 154 | 155 | return $ret; 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /src/Extensions.php: -------------------------------------------------------------------------------- 1 | extensions[] = new $class; 24 | } else { 25 | $cli->error("Error: the class: $class not found"); 26 | } 27 | } 28 | } 29 | 30 | $this->callFunction('init'); 31 | } 32 | 33 | private function callFunction($functionName) 34 | { 35 | $app = Bootstrap::getApplication(); 36 | $cli = $app['cli']; 37 | 38 | foreach ($this->extensions as $extension) { 39 | if (method_exists($extension, $functionName)) { 40 | $cli->debug("Calling $functionName on extension ". get_class($extension)); 41 | call_user_func(array($extension, $functionName)); 42 | } 43 | } 44 | } 45 | } -------------------------------------------------------------------------------- /src/Helpers.php: -------------------------------------------------------------------------------- 1 | recursiveRemoveDirectory($file); 133 | } else { 134 | unlink($file); 135 | } 136 | } 137 | if (file_exists($directory)) { 138 | rmdir($directory); 139 | } 140 | } 141 | 142 | /** 143 | * Returns unique array of objects, based on the object property $key 144 | * 145 | * @param array $array 146 | * @param string $key 147 | * @return array 148 | */ 149 | public function uniqueObjectArray($array, $key) 150 | { 151 | $temp_array = array(); 152 | $i = 0; 153 | $key_array = array(); 154 | 155 | foreach ($array as $val) { 156 | if (!in_array($val->$key, $key_array)) { 157 | $key_array[$i] = $val->$key; 158 | $temp_array[$i] = $val; 159 | } 160 | ++$i; 161 | } 162 | 163 | return $temp_array; 164 | } 165 | 166 | /** 167 | * Search replace in a string, object or array 168 | * 169 | * @param mixed $fld_value 170 | * @param string $search 171 | * @param string $replace 172 | * @return bool 173 | */ 174 | public function fieldSearchReplace(&$fld_value, $search, $replace) 175 | { 176 | $value = $fld_value; 177 | $ret = false; 178 | 179 | $this->recurseSearchReplace($value, $search, $replace); 180 | 181 | if ($fld_value != $value) { 182 | $fld_value = $value; 183 | $ret = true; 184 | } 185 | 186 | return $ret; 187 | } 188 | 189 | /** 190 | * Helper function. Recursive search and replace in a string, object or array 191 | * 192 | * @param mixed $obj 193 | * @param string $search 194 | * @param string $replace 195 | */ 196 | private function recurseSearchReplace(&$obj, $search, $replace) 197 | { 198 | if (is_object($obj) || is_array($obj)) { 199 | foreach ($obj as &$member) { 200 | $this->recurseSearchReplace($member, $search, $replace); 201 | } 202 | } else { 203 | if (is_numeric($obj)) { 204 | return; 205 | } 206 | if (is_bool($obj)) { 207 | return; 208 | } 209 | if (is_null($obj)) { 210 | return; 211 | } 212 | 213 | $b64 = $this->isBase64($obj); 214 | if ($b64) { 215 | $obj = base64_decode($obj); 216 | } 217 | $ser = $this->isSerialized($obj); 218 | if ($ser) { 219 | $obj = unserialize($obj); 220 | } 221 | 222 | if (is_object($obj) || is_array($obj)) { 223 | foreach ($obj as &$member) { 224 | $this->recurseSearchReplace($member, $search, $replace); 225 | } 226 | } else { 227 | $obj = str_replace($search, $replace, $obj); 228 | } 229 | 230 | if ($ser) { 231 | $obj = serialize($obj); 232 | } 233 | if ($b64) { 234 | $obj = base64_encode($obj); 235 | } 236 | } 237 | } 238 | 239 | /** 240 | * Scans through a PHP file and ensures that a specific 241 | * define statement exists and has the correct value 242 | * 243 | * @param string $file The file to scan 244 | * @param string $define The define to look for 245 | * @param string $value The target value of the define 246 | */ 247 | public function ensureDefineInFile($file, $define, $value) 248 | { 249 | if (!file_exists($file)) { 250 | return; 251 | } 252 | $lines = file($file); 253 | $safeDefine = preg_quote($define); 254 | $pattern = "/\s*define\s*\(\s*'($safeDefine)'\s*,\s*'([^']+)'\)\s*;/"; 255 | $targetLine = " define('$define', '$value');\n"; 256 | $hit = false; 257 | $modified = false; 258 | foreach ($lines as &$line) { 259 | if (preg_match($pattern, $line, $matches)) { 260 | $hit = true; 261 | if ($matches[2] != $value) { 262 | $modified = true; 263 | $line = $targetLine; 264 | } 265 | } 266 | } 267 | 268 | if (!$hit) { 269 | $modified = true; 270 | $newLine = "\n// ** Added by WP Bootstrap (don't edit)** //\n"; 271 | $newLine .= "if (!defined('$define')) {\n"; 272 | $newLine .= $targetLine; 273 | $newLine .= "}\n"; 274 | 275 | array_splice($lines, 1, 0, $newLine); 276 | } 277 | 278 | if ($modified) { 279 | file_put_contents($file, join('', $lines)); 280 | } 281 | } 282 | 283 | /** 284 | * @param mixed$data 285 | * @return bool 286 | */ 287 | public function isSerialized($data) 288 | { 289 | if (!is_string($data)) { 290 | return false; 291 | } 292 | $test = @unserialize(($data)); 293 | 294 | return ($test !== false || $test === 'b:0;') ? true : false; 295 | } 296 | 297 | /** 298 | * @return array|mixed|object 299 | */ 300 | public function getWPCFMSettings() 301 | { 302 | $ret = []; 303 | $src = WPBOOT_BASEPATH.'/bootstrap/config/wpbootstrap.json'; 304 | if (file_exists($src)) { 305 | $config = json_decode(file_get_contents($src)); 306 | if ($config) { 307 | $ret = $config; 308 | } 309 | } 310 | 311 | return $ret; 312 | } 313 | 314 | /** 315 | * @param mixed $data 316 | * @return bool 317 | */ 318 | public function isBase64($data) 319 | { 320 | if (!is_string($data)) { 321 | return false; 322 | } 323 | 324 | $decoded = base64_decode($data, true); 325 | if (@base64_encode($decoded) === $data) { 326 | $printable = $this->ctypePrintUnicode($decoded); //ctype_print($decoded); 327 | if ($printable) { 328 | return true; 329 | } else { 330 | return false; 331 | } 332 | } else { 333 | return false; 334 | } 335 | } 336 | 337 | /** 338 | * @param string $input 339 | * @return int 340 | */ 341 | private function ctypePrintUnicode($input) 342 | { 343 | $pattern = "~^[\pL\pN\s\"\~". preg_quote("!#$%&'()*+,-./:;<=>?@[\]^_`{|}´") ."]+$~u"; 344 | return preg_match($pattern, $input); 345 | } 346 | 347 | } 348 | -------------------------------------------------------------------------------- /src/Import/ImportMenus.php: -------------------------------------------------------------------------------- 1 | parse(file_get_contents(WPBOOT_BASEPATH."/bootstrap/menus/{$menu}_manifest")); 51 | 52 | $newMenu = new \stdClass(); 53 | $newMenu->slug = $menu; 54 | $newMenu->locations = $menuMeta['locations']; 55 | $newMenu->items = array(); 56 | foreach ($helpers->getFiles($dir) as $file) { 57 | $menuItem = new \stdClass(); 58 | $menuItem->done = false; 59 | $menuItem->id = 0; 60 | $menuItem->parentId = 0; 61 | $menuItem->slug = $file; 62 | //$menuItem->menu = unserialize(file_get_contents($dir.'/'.$file)); 63 | $menuItem->menu = $yaml->parse(file_get_contents("$dir/$file")); 64 | $newMenu->items[] = $menuItem; 65 | } 66 | usort($newMenu->items, function ($a, $b) { 67 | return (int) $a->menu['menu_order'] - (int) $b->menu['menu_order']; 68 | }); 69 | $this->menus[] = $newMenu; 70 | } 71 | 72 | $helpers->fieldSearchReplace($this->menus, Bootstrap::NEUTRALURL, $baseUrl); 73 | $this->process(); 74 | } 75 | 76 | /** 77 | * The main import process 78 | */ 79 | private function process() 80 | { 81 | remove_all_filters('wp_get_nav_menu_items'); 82 | $locations = array(); 83 | 84 | foreach ($this->menus as $menu) { 85 | $this->processMenu($menu); 86 | foreach ($menu->locations as $location) { 87 | $locations[$location] = $menu->id; 88 | } 89 | } 90 | set_theme_mod('nav_menu_locations', $locations); 91 | } 92 | 93 | /** 94 | * Process individual menu 95 | * 96 | * @param $menu 97 | */ 98 | private function processMenu(&$menu) 99 | { 100 | $app = Bootstrap::getApplication(); 101 | $import = $app['import']; 102 | 103 | $objMenu = wp_get_nav_menu_object($menu->slug); 104 | if (!$objMenu) { 105 | wp_create_nav_menu($menu->slug); 106 | $objMenu = wp_get_nav_menu_object($menu->slug); 107 | } 108 | $menuId = $objMenu->term_id; 109 | $menu->id = $menuId; 110 | 111 | $existingMenuItems = wp_get_nav_menu_items($menu->slug); 112 | foreach ($existingMenuItems as $existingMenuItem) { 113 | wp_delete_post($existingMenuItem->ID, true); 114 | } 115 | 116 | foreach ($menu->items as &$objMenuItem) { 117 | $menuItemType = $objMenuItem->menu['post_meta']['_menu_item_type'][0]; 118 | $newTarget = 0; 119 | switch ($menuItemType) { 120 | case 'post_type': 121 | $newTarget = $import->findTargetObjectId( 122 | $objMenuItem->menu['post_meta']['_menu_item_object_id'][0], 123 | 'post' 124 | ); 125 | break; 126 | case 'taxonomy': 127 | $newTarget = $import->findTargetObjectId( 128 | $objMenuItem->menu['post_meta']['_menu_item_object_id'][0], 129 | 'term' 130 | ); 131 | break; 132 | } 133 | 134 | $parentItem = $this->findMenuItem($objMenuItem->menu['post_meta']['_menu_item_menu_item_parent'][0]); 135 | 136 | $args = array( 137 | 'menu-item-title' => $objMenuItem->menu['post_title'], 138 | 'menu-item-position' => $objMenuItem->menu['menu_order'], 139 | 'menu-item-description' => $objMenuItem->menu['post_content'], 140 | 'menu-item-attr-title' => $objMenuItem->menu['post_title'], 141 | 'menu-item-status' => $objMenuItem->menu['post_status'], 142 | 'menu-item-type' => $menuItemType, 143 | 'menu-item-object' => $objMenuItem->menu['post_meta']['_menu_item_object'][0], 144 | 'menu-item-object-id' => $newTarget, 145 | 'menu-item-url' => $objMenuItem->menu['post_meta']['_menu_item_url'][0], 146 | 'menu-item-parent-id' => $parentItem, 147 | ); 148 | $ret = wp_update_nav_menu_item($menuId, 0, $args); 149 | $objMenuItem->id = $ret; 150 | 151 | foreach ($objMenuItem->menu['post_meta'] as $key => $meta) { 152 | if (in_array($key, $this->skipped_meta_fields) || substr($key, 0, 1) == '_') { 153 | continue; 154 | } 155 | $val = $meta[0]; 156 | update_post_meta($ret, $key, $val); 157 | } 158 | } 159 | } 160 | 161 | 162 | 163 | /** 164 | * Finds a menu item based on it's original id. If found, returns the new (after import) id 165 | * 166 | * @param int $target 167 | * @return int 168 | */ 169 | private function findMenuItem($target) 170 | { 171 | foreach ($this->menus as $menu) { 172 | foreach ($menu->items as $item) { 173 | if ($item->menu['ID'] == $target) { 174 | return $item->id; 175 | } 176 | } 177 | } 178 | 179 | return 0; 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /src/Import/ImportOptions.php: -------------------------------------------------------------------------------- 1 | fieldSearchReplace($settings, Bootstrap::NEUTRALURL, $baseUrl); 32 | file_put_contents($trg, $helpers->prettyPrint(json_encode($settings))); 33 | 34 | if (function_exists('WPCFM')) { 35 | WPCFM()->readwrite->pull_bundle('wpbootstrap'); 36 | } else { 37 | $cli = $app['cli']; 38 | $cli->warning('Plugin WP-CFM does not seem to be installed. No options imported.'); 39 | $cli->warning('Add the WP-CFM plugin directly to this install using:'); 40 | $cli->warning('$ wp plugin install wp-cfm --activate'); 41 | $cli->warning(''); 42 | $cli->warning('...or to your appsettings using:'); 43 | $cli->warning('$ wp bootstrap add plugin wp-cfm'); 44 | $cli->warning('$ wp bootstrap setup'); 45 | return; 46 | } 47 | 48 | // Flush options to make sure no other code overwrites based 49 | // on old settings stored in cache 50 | wp_cache_flush(); 51 | } 52 | } 53 | } -------------------------------------------------------------------------------- /src/Import/ImportPosts.php: -------------------------------------------------------------------------------- 1 | getFiles($dir) as $postType) { 41 | foreach ($helpers->getFiles($dir . '/' . $postType) as $slug) { 42 | $newPost = new \stdClass(); 43 | $newPost->done = false; 44 | $newPost->id = 0; 45 | $newPost->parentId = 0; 46 | $newPost->slug = $slug; 47 | $newPost->type = $postType; 48 | $newPost->tries = 0; 49 | 50 | $file = WPBOOT_BASEPATH . "/bootstrap/posts/$postType/$slug"; 51 | $newPost->post = $yaml->parse(file_get_contents($file)); 52 | 53 | $this->posts[] = $newPost; 54 | } 55 | } 56 | 57 | if (count($this->posts) > 0) { 58 | $this->internalImport(); 59 | } 60 | } 61 | 62 | private function internalImport() 63 | { 64 | $app = Bootstrap::getApplication(); 65 | $helpers = $app['helpers']; 66 | $baseUrl = get_option('siteurl'); 67 | 68 | $helpers->fieldSearchReplace($this->posts, Bootstrap::NEUTRALURL, $baseUrl); 69 | remove_all_actions('transition_post_status'); 70 | 71 | $done = false; 72 | while (!$done) { 73 | $deferred = 0; 74 | foreach ($this->posts as &$post) { 75 | $post->tries++; 76 | if (!$post->done) { 77 | $parentId = $this->parentId($post->post['post_parent'], $this->posts); 78 | if ($parentId || $post->post['post_parent'] == 0 || $post->tries > 9) { 79 | $this->updatePost($post, $parentId); 80 | $post->done = true; 81 | } else { 82 | $deferred++; 83 | } 84 | } 85 | } 86 | if ($deferred == 0) { 87 | $done = true; 88 | } 89 | } 90 | 91 | $this->importMedia(); 92 | } 93 | 94 | /** 95 | * Update individual post 96 | * 97 | * @param $post 98 | * @param $parentId 99 | */ 100 | private function updatePost(&$post, $parentId) 101 | { 102 | global $wpdb; 103 | $app = Bootstrap::getApplication(); 104 | $helpers = $app['helpers']; 105 | 106 | $sql = $wpdb->prepare( 107 | "SELECT ID FROM $wpdb->posts WHERE post_name = %s AND post_type = %s", 108 | $post->slug, 109 | $post->type 110 | ); 111 | 112 | $postId = $wpdb->get_var($sql); 113 | $args = array( 114 | 'post_type' => $post->post['post_type'], 115 | 'post_mime_type' => $post->post['post_mime_type'], 116 | 'post_parent' => $parentId, 117 | 'post_title' => $post->post['post_title'], 118 | 'post_content' => $post->post['post_content'], 119 | 'post_status' => $post->post['post_status'], 120 | 'post_name' => $post->post['post_name'], 121 | 'post_excerpt' => $post->post['post_excerpt'], 122 | 'ping_status' => $post->post['ping_status'], 123 | 'pinged' => $post->post['pinged'], 124 | 'comment_status' => $post->post['comment_status'], 125 | 'post_date' => $post->post['post_date'], 126 | 'post_date_gmt' => $post->post['post_date_gmt'], 127 | 'post_modified' => $post->post['post_modified'], 128 | 'post_modified_gmt' => $post->post['post_modified_gmt'], 129 | ); 130 | 131 | if (!$postId) { 132 | $postId = wp_insert_post($args); 133 | } else { 134 | $args['ID'] = $postId; 135 | wp_update_post($args); 136 | } 137 | 138 | $existingMeta = get_post_meta($postId); 139 | foreach ($post->post['post_meta'] as $meta => $value) { 140 | if (isset($existingMeta[$meta])) { 141 | $existingMetaItem = $existingMeta[$meta]; 142 | } 143 | $i = 0; 144 | foreach ($value as $val) { 145 | if ($helpers->isSerialized($val)) { 146 | $val = unserialize($val); 147 | } 148 | if (isset($existingMetaItem[$i])) { 149 | update_post_meta($postId, $meta, $val, $existingMetaItem[$i]); 150 | } else { 151 | update_post_meta($postId, $meta, $val); 152 | } 153 | } 154 | } 155 | $post->id = $postId; 156 | } 157 | 158 | /** 159 | * Import media file and meta data 160 | */ 161 | private function importMedia() 162 | { 163 | $app = Bootstrap::getApplication(); 164 | $cli = $app['cli']; 165 | $uploadDir = wp_upload_dir(); 166 | $yaml = new Yaml(); 167 | 168 | // check all the media. 169 | foreach (glob(WPBOOT_BASEPATH.'/bootstrap/media/*') as $dir) { 170 | $item = $yaml->parse(file_get_contents("$dir/meta")); 171 | 172 | // does this image have an imported post as it's parent? 173 | $parentId = $this->parentId($item['post_parent'], $this->posts); 174 | if ($parentId != 0) { 175 | $cli->debug("Media is attached to post {$item['ID']} $parentId"); 176 | } 177 | 178 | // does an imported post have this image as thumbnail? 179 | $isAThumbnail = $this->isAThumbnail($item['ID']); 180 | if ($isAThumbnail) { 181 | $cli->debug('Media is thumbnail to (at least) one post ' . $item['ID']); 182 | } 183 | 184 | $args = array( 185 | 'name' => $item['post_name'], 186 | 'post_type' => $item['post_type'], 187 | ); 188 | 189 | $file = $item['post_meta']['_wp_attached_file'][0]; 190 | 191 | $existing = new \WP_Query($args); 192 | if (!$existing->have_posts()) { 193 | $args = array( 194 | 'post_title' => $item['post_title'], 195 | 'post_name' => $item['post_name'], 196 | 'post_type' => $item['post_type'], 197 | 'post_parent' => $parentId, 198 | 'post_status' => $item['post_status'], 199 | 'post_mime_type' => $item['post_mime_type'], 200 | 'guid' => $uploadDir['basedir'].'/'.$file, 201 | ); 202 | $id = wp_insert_post($args); 203 | } else { 204 | $id = $existing->post->ID; 205 | } 206 | update_post_meta($id, '_wp_attached_file', $file); 207 | 208 | // move the file 209 | $src = $dir.'/'.basename($file); 210 | $trg = $uploadDir['basedir'].'/'.$file; 211 | @mkdir($uploadDir['basedir'].'/'.dirname($file), 0777, true); 212 | 213 | if (file_exists($src) && file_exists($trg)) { 214 | if (filesize($src) == filesize($trg)) { 215 | // set this image as a thumbnail and then exit 216 | if ($isAThumbnail) { 217 | $this->setAsThumbnail($item->ID, $id); 218 | } 219 | continue; 220 | } 221 | } 222 | 223 | if (file_exists($src)) { 224 | copy($src, $trg); 225 | // create metadata and other sizes 226 | $attachData = wp_generate_attachment_metadata($id, $trg); 227 | wp_update_attachment_metadata($id, $attachData); 228 | } 229 | 230 | // set this image as a thumbnail if needed 231 | if ($isAThumbnail) { 232 | $this->setAsThumbnail($item['ID'], $id); 233 | } 234 | } 235 | } 236 | 237 | /** 238 | * Finds a post parent based on it's original id. If found, returns the new (after import) id 239 | * 240 | * @param int $foreignParentId 241 | * @param array $objects 242 | * @return int 243 | */ 244 | private function parentId($foreignParentId, $objects) 245 | { 246 | foreach ($objects as $object) { 247 | if ($object->post['ID'] == $foreignParentId) { 248 | return $object->id; 249 | } 250 | } 251 | 252 | return 0; 253 | } 254 | 255 | /** 256 | * Checks if a media/attachment is serves as a thumbnail for another post 257 | * 258 | * @param int $id 259 | * @return bool 260 | */ 261 | private function isAThumbnail($id) 262 | { 263 | foreach ($this->posts as $post) { 264 | if (isset($post->post['post_meta']['_thumbnail_id'])) { 265 | $thumbId = $post->post['post_meta']['_thumbnail_id'][0]; 266 | if ($thumbId == $id) { 267 | return true; 268 | } 269 | } 270 | } 271 | 272 | return false; 273 | } 274 | 275 | /** 276 | * Assigns an attachment as a thumbnail 277 | * 278 | * @param int $oldId 279 | * @param int $newId 280 | */ 281 | private function setAsThumbnail($oldId, $newId) 282 | { 283 | foreach ($this->posts as $post) { 284 | if (isset($post->post['post_meta']['_thumbnail_id'])) { 285 | $thumbId = $post->post['post_meta']['_thumbnail_id'][0]; 286 | if ($thumbId == $oldId) { 287 | set_post_thumbnail($post->id, $newId); 288 | } 289 | } 290 | } 291 | } 292 | } 293 | -------------------------------------------------------------------------------- /src/Import/ImportSidebars.php: -------------------------------------------------------------------------------- 1 | getFiles($dir) as $sidebar) { 35 | if (!is_dir(WPBOOT_BASEPATH."/bootstrap/sidebars/$sidebar")) { 36 | continue; 37 | } 38 | $subdir = WPBOOT_BASEPATH."/bootstrap/sidebars/$sidebar"; 39 | $manifest = WPBOOT_BASEPATH."/bootstrap/sidebars/{$sidebar}_manifest"; 40 | $newSidebar = new \stdClass(); 41 | $newSidebar->slug = $sidebar; 42 | $newSidebar->items = array(); 43 | $newSidebar->meta = $yaml->parse(file_get_contents($manifest)); 44 | 45 | foreach ($newSidebar->meta as $key => $widgetRef) { 46 | $widget = new \stdClass(); 47 | $parts = explode('-', $widgetRef); 48 | $ord = end($parts); 49 | $type = substr($widgetRef, 0, -1 * strlen('-'.$ord)); 50 | 51 | $widget->type = $type; 52 | $widget->ord = $ord; 53 | $widget->meta = $yaml->parse(file_get_contents($subdir.'/'.$widgetRef)); 54 | $newSidebar->items[] = $widget; 55 | } 56 | $this->sidebars[] = $newSidebar; 57 | } 58 | 59 | $helpers->fieldSearchReplace($this->sidebars, Bootstrap::NEUTRALURL, $baseUrl); 60 | $this->process(); 61 | } 62 | 63 | /** 64 | * The main import process 65 | */ 66 | private function process() 67 | { 68 | $app = Bootstrap::getApplication(); 69 | $helpers = $app['helpers']; 70 | $baseUrl = get_option('siteurl'); 71 | 72 | $currentSidebars = get_option('sidebars_widgets', array()); 73 | foreach ($this->sidebars as $sidebar) { 74 | $current = array(); 75 | $new = array(); 76 | if (isset($currentSidebars[$sidebar->slug])) { 77 | $current = $currentSidebars[$sidebar->slug]; 78 | } 79 | foreach ($current as $key => $widgetRef) { 80 | $this->unsetWidget($widgetRef); 81 | } 82 | 83 | foreach ($sidebar->items as $widget) { 84 | $currentWidgetDef = get_option('widget_'.$widget->type, array()); 85 | $ord = $this->findFirstFree($currentWidgetDef); 86 | 87 | $helpers->fieldSearchReplace($widget->meta, Bootstrap::NEUTRALURL, $baseUrl); 88 | $currentWidgetDef[$ord] = $widget->meta; 89 | update_option('widget_'.$widget->type, $currentWidgetDef); 90 | 91 | $newKey = $widget->type.'-'.$ord; 92 | $new[] = $newKey; 93 | } 94 | 95 | $currentSidebars[$sidebar->slug] = $new; 96 | update_option('sidebars_widgets', $currentSidebars); 97 | } 98 | } 99 | 100 | /** 101 | * Unset the current widget settings in the options table 102 | * 103 | * @param string $widgetRef 104 | */ 105 | private function unsetWidget($widgetRef) 106 | { 107 | $parts = explode('-', $widgetRef); 108 | $ord = end($parts); 109 | $type = substr($widgetRef, 0, -1 * strlen('-'.$ord)); 110 | 111 | $currentWidgetDef = get_option('widget_'.$type); 112 | unset($currentWidgetDef[$ord]); 113 | update_option('widget_'.$type, $currentWidgetDef); 114 | } 115 | 116 | /** 117 | * Finds a free slot in a Widget option struct. 118 | * 119 | * @param array $arr 120 | * @return int 121 | */ 122 | private function findFirstFree($arr) 123 | { 124 | ksort($arr); 125 | $expected = 0; 126 | foreach ($arr as $key => $value) { 127 | if ($key == '_multiwidget') { 128 | continue; 129 | } 130 | 131 | ++$expected; 132 | if ($key == $expected) { 133 | continue; 134 | } 135 | if ($key > $expected) { 136 | return $expected; 137 | } 138 | } 139 | ++$expected; 140 | 141 | return $expected; 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /src/Import/ImportTaxonomies.php: -------------------------------------------------------------------------------- 1 | getFiles($dir) as $subdir) { 39 | if (!is_dir("$dir/$subdir")) { 40 | continue; 41 | } 42 | $taxonomy = new \stdClass(); 43 | $taxonomy->slug = $subdir; 44 | $taxonomy->terms = array(); 45 | $this->readManifest($taxonomy, $subdir); 46 | foreach ($helpers->getFiles($dir.'/'.$subdir) as $file) { 47 | $term = new \stdClass(); 48 | $term->done = false; 49 | $term->id = 0; 50 | $term->slug = $file; 51 | $term->term = $yaml->parse(file_get_contents($dir.'/'.$subdir.'/'.$file)); 52 | $taxonomy->terms[] = $term; 53 | } 54 | $this->taxonomies[] = $taxonomy; 55 | } 56 | 57 | $currentTaxonomies = get_taxonomies(); 58 | foreach ($this->taxonomies as &$taxonomy) { 59 | if (isset($currentTaxonomies[$taxonomy->slug])) { 60 | $this->processTaxonomy($taxonomy); 61 | } 62 | } 63 | } 64 | 65 | /** 66 | * Searches through a imported posts to identify all taxonomy terms assignments 67 | * for each post and assigns the post to the terms in the current WP install 68 | */ 69 | public function assignObjects() 70 | { 71 | // Posts 72 | $app = Bootstrap::getApplication(); 73 | $cli = $app['cli']; 74 | $import = $app['import']; 75 | 76 | $cli->debug('Assigning objects to taxonomies'); 77 | $posts = $import->posts; 78 | foreach ($this->taxonomies as &$taxonomy) { 79 | foreach ($posts as $post) { 80 | if (isset($post->post['taxonomies'][$taxonomy->slug])) { 81 | $newTerms = array(); 82 | foreach ($post->post['taxonomies'][$taxonomy->slug] as $orgSlug) { 83 | $termSlug = $this->findNewTerm($taxonomy, $orgSlug); 84 | $newTerms[] = $termSlug; 85 | } 86 | $cli->debug("adding terms to post {$post->slug}", $newTerms); 87 | wp_set_object_terms($post->id, $newTerms, $taxonomy->slug, false); 88 | } 89 | } 90 | } 91 | } 92 | 93 | /** 94 | * Process individual taxonomy 95 | * 96 | * @param \stdClass $taxonomy 97 | */ 98 | private function processTaxonomy(&$taxonomy) 99 | { 100 | $app = Bootstrap::getApplication(); 101 | $cli = $app['cli']; 102 | 103 | $currentTerms = get_terms($taxonomy->slug, array('hide_empty' => false)); 104 | $done = false; 105 | while (!$done) { 106 | $deferred = 0; 107 | foreach ($taxonomy->terms as &$term) { 108 | if (!$term->done) { 109 | $parentId = $this->parentId($term->term['parent'], $taxonomy); 110 | $cli->debug("Importing term {$term->term['name']}/{$term->term['slug']} parent: $parentId"); 111 | if ($parentId || $term->term['parent'] == 0) { 112 | $args = array( 113 | 'description' => $term->term['description'], 114 | 'parent' => $parentId, 115 | 'slug' => $term->term['slug'], 116 | 'term_group' => $term->term['term_group'], 117 | 'name' => $term->term['name'], 118 | ); 119 | switch ($taxonomy->type) { 120 | case 'postid': 121 | $this->adjustTypePostId($term->term, $args); 122 | break; 123 | } 124 | $existingTermId = $this->findExistingTerm($term, $currentTerms); 125 | if ($existingTermId > 0) { 126 | wp_update_term($existingTermId, $taxonomy->slug, $args); 127 | $term->id = $existingTermId; 128 | } else { 129 | $id = wp_insert_term($term->term['name'], $taxonomy->slug, $args); 130 | $term->id = $id['term_id']; 131 | } 132 | $term->done = true; 133 | } else { 134 | ++$deferred; 135 | } 136 | } 137 | if ($deferred == 0) { 138 | $done = true; 139 | } 140 | } 141 | } 142 | } 143 | 144 | /** 145 | * Handles name/slug adjustments for terms in a "postid" taxonomy 146 | * 147 | * @param \stdClass $term 148 | * @param array $args 149 | */ 150 | private function adjustTypePostId(&$term, &$args) 151 | { 152 | // slug refers to a postid. 153 | $importPosts = $this->import->posts; 154 | $newId = $importPosts->findTargetPostId($term['slug']); 155 | 156 | if ($newId) { 157 | $args['slug'] = strval($newId); 158 | $args['name'] = strval($newId); 159 | $term['name'] = strval($newId); 160 | $term['slug'] = strval($newId); 161 | } 162 | } 163 | 164 | /** 165 | * Finds a term in the existing WP install based on what that terms 166 | * was named in the old. 167 | * 168 | * @param \stdClass $taxonomy 169 | * @param string $orgSlug 170 | * @return string 171 | */ 172 | private function findNewTerm($taxonomy, $orgSlug) 173 | { 174 | foreach ($taxonomy->terms as $term) { 175 | if ($term->slug == $orgSlug) { 176 | return $term->term['slug']; 177 | } 178 | } 179 | 180 | return $orgSlug; 181 | } 182 | 183 | /** 184 | * Finds a the current parent id for a term based on its old value 185 | * 186 | * @param int $foreignParentId 187 | * @param \stdClass $taxonomy 188 | * @return int 189 | */ 190 | private function parentId($foreignParentId, $taxonomy) 191 | { 192 | foreach ($taxonomy->terms as $term) { 193 | if ($term->term['term_id'] == $foreignParentId) { 194 | return $term->id; 195 | } 196 | } 197 | 198 | return 0; 199 | } 200 | 201 | /** 202 | * Searches all current terms in a taxonomy and returns the id 203 | * for the searched term slug 204 | * 205 | * @param string $term 206 | * @param array $currentTerms 207 | * @return int 208 | */ 209 | private function findExistingTerm($term, $currentTerms) 210 | { 211 | foreach ($currentTerms as $currentTerm) { 212 | if ($currentTerm->slug == $term->term['slug']) { 213 | return $currentTerm->term_id; 214 | } 215 | } 216 | 217 | return 0; 218 | } 219 | 220 | /** 221 | * Parse the manifest file generated for the taxonomy 222 | * 223 | * @param \stdClass $taxonomy 224 | * @param string $taxonomyName 225 | */ 226 | private function readManifest(&$taxonomy, $taxonomyName) 227 | { 228 | $yaml = new Yaml(); 229 | $taxonomy->type = 'standard'; 230 | $taxonomy->termDescriptor = 'indirect'; 231 | $file = WPBOOT_BASEPATH."/bootstrap/taxonomies/{$taxonomyName}_manifest"; 232 | if (file_exists($file)) { 233 | $manifest = $yaml->parse(file_get_contents($file)); 234 | if (isset($manifest['type'])) { 235 | $taxonomy->type = $manifest['type']; 236 | } 237 | if (isset($manifest['termDescriptor'])) { 238 | $taxonomy->termDescriptor = $manifest['termDescriptor']; 239 | } 240 | } 241 | } 242 | } 243 | -------------------------------------------------------------------------------- /src/Providers/ApplicationParametersProvider.php: -------------------------------------------------------------------------------- 1 | get_runner(); 17 | 18 | $pimple['path'] = $runner->config['path']; 19 | $pimple['args'] = $runner->arguments; 20 | $pimple['assocArgs'] = $runner->assoc_args; 21 | $pimple['ymlPath'] = $runner->project_config_path; 22 | 23 | $this->readEnvironment($pimple); 24 | $this->readDotEnv($pimple); 25 | $this->loadAppsettings($pimple); 26 | } 27 | 28 | /** 29 | * @param Container $pimple 30 | */ 31 | private function readEnvironment(Container $pimple) 32 | { 33 | $pimple['environment'] = '[notset]'; 34 | if (file_exists($pimple['ymlPath'])) { 35 | $yaml = new Yaml(); 36 | $config = $yaml->parse(file_get_contents($pimple['ymlPath'])); 37 | if (isset($config['environment'])) { 38 | $pimple['environment'] = $config['environment']; 39 | } 40 | } 41 | } 42 | 43 | /** 44 | * @param Container $pimple 45 | */ 46 | private function readDotEnv(Container $pimple) 47 | { 48 | if (file_exists(WPBOOT_BASEPATH . "/.env")) { 49 | $dotEnv = new Dotenv(WPBOOT_BASEPATH); 50 | $dotEnv->load(); 51 | } 52 | 53 | $file = '.env-' . $pimple['environment']; 54 | if (file_exists(WPBOOT_BASEPATH . "/$file")) { 55 | $dotEnv = new Dotenv(WPBOOT_BASEPATH, $file); 56 | $dotEnv->overload(); 57 | } 58 | } 59 | 60 | /** 61 | * @param Container $pimple 62 | */ 63 | private function loadAppsettings(Container $pimple) 64 | { 65 | if (file_exists(WPBOOT_BASEPATH . '/appsettings.yml')) { 66 | $yaml = new Yaml(); 67 | $settings = $yaml->parse(file_get_contents(WPBOOT_BASEPATH . '/appsettings.yml')); 68 | $pimple['settings'] = $settings; 69 | return; 70 | } 71 | 72 | if (file_exists(WPBOOT_BASEPATH . '/appsettings.json')) { 73 | $settings = json_decode(file_get_contents(WPBOOT_BASEPATH . '/appsettings.json')); 74 | $settings = (array)$settings; 75 | $pimple['settings'] = $settings; 76 | return; 77 | } 78 | 79 | $pimple['settings'] = array(); 80 | } 81 | } -------------------------------------------------------------------------------- /src/Providers/CliUtilsWrapper.php: -------------------------------------------------------------------------------- 1 | import = $app['import']; 22 | } 23 | 24 | /** 25 | * Iterates an array with references to options in the wp_options table. 26 | * Each value is checked to see if it points to an imported post/term and if so, 27 | * updates the option value to the new post or term id 28 | * 29 | * @param array $references 30 | * @param string $type 31 | */ 32 | public function resolveOptionReferences($references, $type) 33 | { 34 | wp_cache_flush(); 35 | 36 | foreach ($references as $key => $value) { 37 | if (is_integer($key)) { 38 | $currentValue = get_option($value, 0); 39 | $newId = $this->import->findTargetObjectId($currentValue, $type); 40 | if ($newId != 0) { 41 | update_option($value, $newId); 42 | } 43 | } elseif (is_string($key) && is_string($value)) { 44 | $path = $value; 45 | $currentStruct = get_option($key, 0); 46 | try { 47 | $currentValue = $this->getValue($currentStruct, $path); 48 | $newId = $this->import->findTargetObjectId($currentValue, $type); 49 | if ($newId != 0) { 50 | $this->setValue($currentStruct, $path, $newId); 51 | update_option($key, $currentStruct); 52 | } 53 | } catch (\Exception $e) { 54 | continue; 55 | } 56 | } elseif (is_string($key) && is_array($value)) { 57 | $currentStruct = get_option($key, 0); 58 | $paths = $value; 59 | foreach ($paths as $path) { 60 | $currentValue = $this->getValue($currentStruct, $path); 61 | $newId = $this->import->findTargetObjectId($currentValue, $type); 62 | if ($newId != 0) { 63 | $this->setValue($currentStruct, $path, $newId); 64 | } 65 | } 66 | update_option($key, $currentStruct); 67 | } 68 | } 69 | } 70 | 71 | /** 72 | * Iterates an array with references to post_meta fields. 73 | * Each value is checked to see if it points to an imported post/term and if so, 74 | * updates the post_meta value to the new post or term id 75 | * 76 | * @param array $references 77 | * @param string $type 78 | */ 79 | public function resolvePostMetaReferences($references, $type) 80 | { 81 | foreach ($references as $key => $value) { 82 | if (is_integer($key)) { 83 | foreach ($this->import->posts as $post) { 84 | if (isset($post->post['post_meta'][$value])) { 85 | foreach ($post->post['post_meta'][$value] as $currentValue) { 86 | $newValue = $this->calcNewValue($currentValue, $type); 87 | if ($newValue !== false) { 88 | update_post_meta($post->id, $value, $newValue, $currentValue); 89 | } 90 | } 91 | } 92 | } 93 | } 94 | } 95 | } 96 | 97 | /** 98 | * Returns the new value for a replaced field based on it's current value. 99 | * If the value is a string, a regex search is made to see the old value might 100 | * refer to a post/term 101 | * 102 | * @param $currentValue 103 | * @param $type 104 | * @return bool|int|mixed 105 | */ 106 | private function calcNewValue($currentValue, $type) 107 | { 108 | if (is_integer($currentValue)) { 109 | $newId = $this->import->findTargetObjectId($currentValue, $type); 110 | if ($newId != 0) { 111 | return $newId; 112 | } 113 | } 114 | $count = preg_match_all('!\d+!', $currentValue, $matches); 115 | if ($count == 1) { 116 | $oldId = $matches[0][0]; 117 | $newId = $this->import->findTargetObjectId($oldId, $type); 118 | if ($newId != 0) { 119 | $newValue = str_replace($oldId, $newId, $currentValue); 120 | 121 | return $newValue; 122 | } 123 | } 124 | 125 | return false; 126 | } 127 | 128 | /** 129 | * Use PHP eval to evaluate an expression that refers to a value 130 | * inside an object or array and return that value 131 | * 132 | * @param mixed $obj 133 | * @param string $path 134 | * @return mixed 135 | */ 136 | private function getValue($obj, $path) 137 | { 138 | return eval('return $obj'.$path.';'); 139 | } 140 | 141 | /** 142 | * Use PHP eval to update a member in a object or array with a new value 143 | * 144 | * @param mixed $obj 145 | * @param string $path 146 | * @param $value 147 | */ 148 | private function setValue(&$obj, $path, $value) 149 | { 150 | $evalStr = '$obj'.$path.'='.$value.'; return $obj;'; 151 | $obj = eval($evalStr); 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /tests/InstallTest.php: -------------------------------------------------------------------------------- 1 | register(new Providers\DefaultObjectProvider()); 23 | $app['environment'] = 'test'; 24 | $app['path'] = '/dev/null/wordpress'; 25 | 26 | $stub = $this->getMockCliWrapper(); 27 | $stub->expects($this->once()) 28 | ->method('log'); 29 | $stub->expects($this->once()) 30 | ->method('warning'); 31 | $app['cli'] = $stub; 32 | 33 | WpCli::setApplication($app); 34 | 35 | $installer = $app['install']; 36 | $installer->run(null, null); 37 | } 38 | 39 | private function getMockCliWrapper() 40 | { 41 | $stub = $this->getMockBuilder('CliWrapper') 42 | ->setMethods(['get_runner', 'log', 'warning']) 43 | ->getMock(); 44 | 45 | $stub->method('get_runner')->willReturn('foo'); 46 | $stub->method('log')->willReturn(null); 47 | $stub->method('warning')->willReturn(null); 48 | return $stub; 49 | } 50 | } -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eriktorsner/wp-bootstrap/a167760282831974c418f577a45d35a1383a9e80/tests/bootstrap.php -------------------------------------------------------------------------------- /wpcli.php: -------------------------------------------------------------------------------- 1 |