├── .editorconfig ├── .gitignore ├── LICENSE.md ├── README.md ├── composer.json ├── config └── content-migration.php └── src ├── ClearContent.php ├── Console └── ContentMigrationCommand.php ├── ContentMigration.php ├── Facades ├── ClearContent.php └── ContentMigration.php └── Providers └── ContentMigrationServiceProvider.php /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | insert_final_newline = true 7 | indent_style = space 8 | indent_size = 2 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | 14 | [*.php] 15 | indent_size = 4 16 | 17 | [*.blade.php] 18 | indent_size = 2 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor 2 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) istogram - Web Development Studio 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 7 | of the Software, and to permit persons to whom the Software is furnished to do 8 | so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Wordpress API Content Migration 2 | 3 | This Acorn package provides Artisan commands to migrate a WP site's content using the WP REST API. 4 | 5 | ## Installation 6 | 7 | You can install this package with Composer: 8 | 9 | ```bash 10 | composer require istogram/wp-api-content-migration 11 | ``` 12 | 13 | You can publish the config file with: 14 | 15 | ```shell 16 | wp acorn vendor:publish --provider="istogram\WpApiContentMigration\Providers\ContentMigrationServiceProvider" 17 | ``` 18 | 19 | ## Configuration 20 | 21 | ### Allow SVG media uploads 22 | 23 | If you want to allow SVG media uploads you will need to set the config option: 24 | 25 | ```php 26 | 'allow_svg_media' => true 27 | ``` 28 | 29 | ## Usage 30 | 31 | To migrate WP content from a WP site, using the WP REST API, to the local site use this command replacing {domain} with the domain of the Live WP site : 32 | 33 | ```shell 34 | wp acorn migrate:content {domain} 35 | ``` 36 | 37 | When no options are applied, the command will proceed step by step, asking for confirmation before each step is applied. 38 | 39 | If you want to clear the current taxonomies, media, posts and pages of the local site you may use this option : 40 | 41 | ```shell 42 | wp acorn migrate:content {domain} --clear-all 43 | ``` 44 | 45 | You may also use this option if you want to migrate all WP content without confirmations : 46 | 47 | ```shell 48 | wp acorn migrate:content {domain} --clear-all --migrate-all 49 | ``` 50 | 51 | Please be aware that if you choose to clear any of the existing taxonomies, media, posts or pages this will delete entirely all the relevant content from the local site DB. This action is irreversible, so it's safer to have a DB backup first. 52 | 53 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "istogram/wp-api-content-migration", 3 | "type": "package", 4 | "description": "An Acorn package to migrate content from a WP API to a local WP installation", 5 | "license": "MIT", 6 | "authors": [ 7 | { 8 | "name": "Timos Zabetakis", 9 | "email": "timoszab@gmail.com" 10 | } 11 | ], 12 | "autoload": { 13 | "psr-4": { 14 | "istogram\\WpApiContentMigration\\": "src/" 15 | } 16 | }, 17 | "require": { 18 | "php": "^8.0" 19 | }, 20 | "extra": { 21 | "acorn": { 22 | "providers": [ 23 | "istogram\\WpApiContentMigration\\Providers\\ContentMigrationServiceProvider" 24 | ], 25 | "aliases": { 26 | "ClearContent": "istogram\\WpApiContentMigration\\Facades\\ClearContent", 27 | "ContentMigration": "istogram\\WpApiContentMigration\\Facades\\ContentMigration" 28 | } 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /config/content-migration.php: -------------------------------------------------------------------------------- 1 | '%current%/%max% [%bar%] %elapsed%', 23 | 24 | /* 25 | |-------------------------------------------------------------------------- 26 | | Allow SVG media 27 | |-------------------------------------------------------------------------- 28 | | 29 | | Set to true to allow SVG media files to be imported. 30 | */ 31 | 'allow_svg_media' => false, 32 | ]; 33 | -------------------------------------------------------------------------------- /src/ClearContent.php: -------------------------------------------------------------------------------- 1 | app = $app; 24 | } 25 | 26 | /** 27 | * Clear WP taxonomies. This will delete all categories and tags. 28 | * 29 | * @return void 30 | */ 31 | public function clearTaxonomies() 32 | { 33 | try { 34 | // delete all terms 35 | $terms = get_terms([ 36 | 'taxonomy' => 'category', 37 | 'hide_empty' => false, 38 | ]); 39 | 40 | foreach ($terms as $term) { 41 | wp_delete_term($term->term_id, 'category'); 42 | } 43 | 44 | $terms = get_terms([ 45 | 'taxonomy' => 'post_tag', 46 | 'hide_empty' => false, 47 | ]); 48 | 49 | foreach ($terms as $term) { 50 | wp_delete_term($term->term_id, 'post_tag'); 51 | } 52 | 53 | // delete all term metadata 54 | $this->clearImportedMeta('category'); 55 | $this->clearImportedMeta('tag'); 56 | 57 | // return response 58 | return 'Taxonomies cleared'; 59 | } catch (\Exception $e) { 60 | return $e->getMessage(); 61 | } 62 | } 63 | 64 | /** 65 | * Clear WP media. This will delete all media files and their metadata. 66 | * 67 | * @return void 68 | */ 69 | public function clearMedia() 70 | { 71 | try { 72 | // delete all media 73 | $media = get_posts([ 74 | 'post_type' => 'attachment', 75 | 'numberposts' => -1, 76 | 'post_status' => null, 77 | ]); 78 | 79 | foreach ($media as $medium) { 80 | wp_delete_attachment($medium->ID, true); 81 | } 82 | 83 | // delete all media metadata 84 | $this->app->db->table('postmeta')->where('meta_key', '_wp_attached_file')->delete(); 85 | $this->app->db->table('postmeta')->where('meta_key', '_wp_attachment_metadata')->delete(); 86 | $this->clearImportedMeta('featured_media'); 87 | 88 | // delete files in uploads directory 89 | $uploads_dir = wp_upload_dir(); 90 | 91 | $files = glob($uploads_dir['basedir'].'/*'); 92 | 93 | foreach ($files as $file) { 94 | if (is_file($file)) { 95 | unlink($file); 96 | } 97 | } 98 | 99 | return 'Media cleared'; 100 | } catch (\Exception $e) { 101 | return $e->getMessage(); 102 | } 103 | } 104 | 105 | /** 106 | * Clear WP posts. This will also clear all post metadata. 107 | * 108 | * @return void 109 | */ 110 | public function clearPosts() 111 | { 112 | try { 113 | // delete all posts 114 | $posts = get_posts([ 115 | 'post_type' => 'post', 116 | 'numberposts' => -1, 117 | 'post_status' => null, 118 | ]); 119 | 120 | foreach ($posts as $post) { 121 | wp_delete_post($post->ID, true); 122 | } 123 | 124 | // delete all post metadata 125 | $this->clearImportedMeta('post'); 126 | 127 | return 'Posts cleared'; 128 | } catch (\Exception $e) { 129 | return $e->getMessage(); 130 | } 131 | } 132 | 133 | /** 134 | * Clear WP pages. This method also clears all imported page metadata. 135 | * 136 | * @return void 137 | */ 138 | public function clearPages() 139 | { 140 | try { 141 | // delete all pages 142 | $pages = get_posts([ 143 | 'post_type' => 'page', 144 | 'numberposts' => -1, 145 | 'post_status' => null, 146 | ]); 147 | 148 | foreach ($pages as $page) { 149 | wp_delete_post($page->ID, true); 150 | } 151 | 152 | // delete all page metadata 153 | $this->clearImportedMeta('page'); 154 | 155 | return 'Pages cleared'; 156 | } catch (\Exception $e) { 157 | return $e->getMessage(); 158 | } 159 | } 160 | 161 | /** 162 | * Clear imported meta data. This method is used to clear meta data 163 | * that was imported from WP API. 164 | * 165 | * @return void 166 | */ 167 | public function clearImportedMeta($type) 168 | { 169 | switch ($type) { 170 | case 'category': 171 | $this->app->db->table('termmeta')->where('meta_key', 'wp_api_prev_category_id')->delete(); 172 | break; 173 | case 'tag': 174 | $this->app->db->table('termmeta')->where('meta_key', 'wp_api_prev_tag_id')->delete(); 175 | break; 176 | case 'featured_media': 177 | $this->app->db->table('postmeta')->where('meta_key', 'wp_api_prev_featured_media_id')->delete(); 178 | break; 179 | case 'post': 180 | $this->app->db->table('postmeta')->where('meta_key', 'wp_api_prev_post_id')->delete(); 181 | break; 182 | case 'page': 183 | $this->app->db->table('postmeta')->where('meta_key', 'wp_api_prev_page_id')->delete(); 184 | $this->app->db->table('postmeta')->where('meta_key', 'wp_api_prev_page_parent_id')->delete(); 185 | break; 186 | } 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /src/Console/ContentMigrationCommand.php: -------------------------------------------------------------------------------- 1 | argument('domain')) { 34 | $this->argument('domain', $this->ask('What is the domain of the WP site?')); 35 | } 36 | 37 | switch ($this->option('clear-all')) { 38 | case true: 39 | if ($this->confirm('Do you want to clear all content?')) { 40 | $this->info('Clearing all content'); 41 | $this->clearTaxonomies(); 42 | $this->clearMedia(); 43 | $this->clearPosts(); 44 | $this->clearPages(); 45 | } 46 | break; 47 | case false: 48 | $this->info('Clearing content'); 49 | $this->confirmClear('taxonomies') ? $this->clearTaxonomies() : null; 50 | $this->confirmClear('media') ? $this->clearMedia() : null; 51 | $this->confirmClear('posts') ? $this->clearPosts() : null; 52 | $this->confirmClear('pages') ? $this->clearPages() : null; 53 | break; 54 | } 55 | 56 | switch ($this->option('migrate-all')) { 57 | case true: 58 | $this->info('Migrating all content'); 59 | $this->migrateCategories(); 60 | $this->migrateTags(); 61 | $this->migrateMedia(); 62 | $this->migratePosts(); 63 | $this->migratePages(); 64 | $this->clearImportedMeta(); 65 | break; 66 | case false: 67 | $this->info('Migrating content'); 68 | $this->confirmMigrate('categories') ? $this->migrateCategories() : null; 69 | $this->confirmMigrate('tags') ? $this->migrateTags() : null; 70 | $this->confirmMigrate('media') ? $this->migrateMedia() : null; 71 | $this->confirmMigrate('posts') ? $this->migratePosts() : null; 72 | $this->confirmMigrate('pages') ? $this->migratePages() : null; 73 | break; 74 | } 75 | } 76 | 77 | /** 78 | * Clear taxonomies (categories and tags). This will delete all categories and tags. 79 | * 80 | * @return void 81 | */ 82 | public function clearTaxonomies() 83 | { 84 | $response = ClearContent::clearTaxonomies(); 85 | 86 | return $this->info($response); 87 | } 88 | 89 | /** 90 | * Clear media (attachments). This will delete all media files and their metadata. 91 | * 92 | * @return void 93 | */ 94 | public function clearMedia() 95 | { 96 | $response = ClearContent::clearMedia(); 97 | 98 | $this->info($response); 99 | } 100 | 101 | /** 102 | * Clear posts (articles). This will delete all posts and their metadata. 103 | * 104 | * @return void 105 | */ 106 | public function clearPosts() 107 | { 108 | $response = ClearContent::clearPosts(); 109 | 110 | $this->info($response); 111 | } 112 | 113 | /** 114 | * Clear pages (static pages). This will delete all pages and their metadata. 115 | * 116 | * @return void 117 | */ 118 | public function clearPages() 119 | { 120 | $response = ClearContent::clearPages(); 121 | 122 | $this->info($response); 123 | } 124 | 125 | /** 126 | * Clear imported meta data. 127 | * 128 | * @return void 129 | */ 130 | public function clearImportedMeta() 131 | { 132 | $types = ['category', 'tag', 'featured_media', 'post', 'page']; 133 | 134 | foreach ($types as $type) { 135 | ClearContent::clearImportedMeta($type); 136 | } 137 | 138 | $this->info('Cleared imported meta'); 139 | } 140 | 141 | /** 142 | * Confirm clear data. 143 | * 144 | * @param string $type 145 | * 146 | * @return void 147 | */ 148 | public function confirmClear($type) 149 | { 150 | // check if user wants to clear data 151 | $confirm = $this->confirm("Are you sure you want to clear all $type?"); 152 | 153 | if ($confirm) { 154 | return true; 155 | } 156 | } 157 | 158 | /** 159 | * Confirm migrate data. 160 | * 161 | * @param string $type 162 | * 163 | * @return void 164 | */ 165 | public function confirmMigrate($type) 166 | { 167 | // check if user wants to migrate data 168 | $confirm = $this->confirm("Are you sure you want to migrate all $type?"); 169 | 170 | if ($confirm) { 171 | return true; 172 | } 173 | } 174 | 175 | /** 176 | * Fetch data from WP API. This method is used to fetch data from WP API. 177 | * 178 | * @param string $endpoint 179 | * 180 | * @return void 181 | */ 182 | public function fetchData($endpoint) 183 | { 184 | $response = wp_remote_get($endpoint); 185 | 186 | if (is_wp_error($response)) { 187 | $error_message = $response->get_error_message(); 188 | $this->error("Error: $error_message"); 189 | 190 | return; 191 | } 192 | 193 | return json_decode(wp_remote_retrieve_body($response)); 194 | } 195 | 196 | /** 197 | * Fetch total pages from WP API. This method is used to fetch the total number of pages from WP API. 198 | * 199 | * @param string $endpoint 200 | * 201 | * @return void 202 | */ 203 | public function fetchTotalPages($endpoint) 204 | { 205 | $response = wp_remote_get($endpoint); 206 | 207 | if (is_wp_error($response)) { 208 | $error_message = $response->get_error_message(); 209 | $this->error("Error: $error_message"); 210 | 211 | return; 212 | } 213 | 214 | return wp_remote_retrieve_header($response, 'X-WP-TotalPages'); 215 | } 216 | 217 | /** 218 | * Fetch page data from WP API. This method is used to fetch data from a specific page. 219 | * 220 | * @param string $endpoint 221 | * @param int $page 222 | * 223 | * @return void 224 | */ 225 | public function fetchPageData($endpoint, $page) 226 | { 227 | $response = wp_remote_get($endpoint.'?page='.$page); 228 | 229 | if (is_wp_error($response)) { 230 | $error_message = $response->get_error_message(); 231 | $this->error("Error: $error_message"); 232 | 233 | return; 234 | } 235 | 236 | return json_decode(wp_remote_retrieve_body($response)); 237 | } 238 | 239 | /** 240 | * Migrate categories. This method is used to migrate categories from WP API. 241 | * 242 | * @return void 243 | */ 244 | public function migrateCategories() 245 | { 246 | $this->info('Migrating WP categories'); 247 | $this->line(''); 248 | 249 | // set categories endpoint 250 | $categories_endpoint = $this->argument('domain').'/wp-json/wp/v2/categories'; 251 | 252 | // get categories 253 | $categories = $this->fetchData($categories_endpoint); 254 | 255 | // get total pages 256 | $total_pages = $this->fetchTotalPages($categories_endpoint); 257 | 258 | // create progress bar 259 | $progressBar = $this->output->createProgressBar($total_pages); 260 | 261 | // set progress bar format 262 | $progressBar->setFormat(config('content-migration.progress_bar_format')); 263 | 264 | // loop through all pages 265 | for ($page = 1; $page <= $total_pages; ++$page) { 266 | // get categories 267 | $categories = $this->fetchPageData($categories_endpoint, $page); 268 | 269 | // filter parent categories 270 | $parent_categories = array_filter($categories, function ($category) { 271 | return $category->parent === 0; 272 | }); 273 | 274 | // create parent categories 275 | foreach ($parent_categories as $category) { 276 | ContentMigration::createCategory($category); 277 | } 278 | 279 | // filter child categories 280 | $child_categories = array_filter($categories, function ($category) { 281 | return $category->parent !== 0; 282 | }); 283 | 284 | // create child categories 285 | foreach ($child_categories as $category) { 286 | ContentMigration::createCategory($category); 287 | } 288 | 289 | // output progress 290 | $progressBar->advance(); 291 | 292 | // break if last page 293 | if ($page === $total_pages) { 294 | break; 295 | } 296 | } 297 | 298 | $progressBar->finish(); 299 | $this->printFormattedEndMessage('Migrated categories'); 300 | } 301 | 302 | /** 303 | * Migrate tags. This method is used to migrate tags from WP API. 304 | * 305 | * @return void 306 | */ 307 | public function migrateTags() 308 | { 309 | $this->info('Migrating tags'); 310 | $this->line(''); 311 | 312 | // get tags 313 | $tags_endpoint = $this->argument('domain').'/wp-json/wp/v2/tags'; 314 | 315 | $tags = $this->fetchData($tags_endpoint); 316 | 317 | // get total pages 318 | $total_pages = $this->fetchTotalPages($tags_endpoint); 319 | 320 | // create progress bar 321 | $progressBar = $this->output->createProgressBar($total_pages); 322 | 323 | // set progress bar format 324 | $progressBar->setFormat(config('content-migration.progress_bar_format')); 325 | 326 | // loop through all pages 327 | for ($page = 1; $page <= $total_pages; ++$page) { 328 | $tags = $this->fetchPageData($tags_endpoint, $page); 329 | 330 | // create tags 331 | foreach ($tags as $tag) { 332 | ContentMigration::createTag($tag); 333 | } 334 | 335 | // output progress 336 | $progressBar->advance(); 337 | 338 | // break if last page 339 | if ($page === $total_pages) { 340 | break; 341 | } 342 | } 343 | 344 | $progressBar->finish(); 345 | $this->printFormattedEndMessage('Migrated tags'); 346 | } 347 | 348 | /** 349 | * Migrate media. This method is used to migrate media from WP API. 350 | * 351 | * @return void 352 | */ 353 | public function migrateMedia() 354 | { 355 | $this->info('Migrating media'); 356 | $this->line(''); 357 | 358 | // set media endpoint 359 | $media_endpoint = $this->argument('domain').'/wp-json/wp/v2/media'; 360 | 361 | // get media 362 | $media = $this->fetchData($media_endpoint); 363 | 364 | // get total pages 365 | $total_pages = $this->fetchTotalPages($media_endpoint); 366 | 367 | // create progress bar 368 | $progressBar = $this->output->createProgressBar($total_pages); 369 | 370 | // set progress bar format 371 | $progressBar->setFormat(config('content-migration.progress_bar_format')); 372 | 373 | // loop through all pages 374 | for ($page = 1; $page <= $total_pages; ++$page) { 375 | $media = $this->fetchPageData($media_endpoint, $page); 376 | 377 | // create media 378 | foreach ($media as $medium) { 379 | ContentMigration::createMedia($medium); 380 | } 381 | 382 | // output progress 383 | $progressBar->advance(); 384 | 385 | // break if last page 386 | if ($page === $total_pages) { 387 | break; 388 | } 389 | } 390 | 391 | $progressBar->finish(); 392 | $this->printFormattedEndMessage('Migrated media'); 393 | } 394 | 395 | /** 396 | * Migrate posts. This method is used to migrate posts from WP API. 397 | * 398 | * @return void 399 | */ 400 | public function migratePosts() 401 | { 402 | $this->info('Migrating posts'); 403 | $this->line(''); 404 | 405 | // set posts endpoint 406 | $posts_endpoint = $this->argument('domain').'/wp-json/wp/v2/posts'; 407 | 408 | // get posts 409 | $posts = $this->fetchData($posts_endpoint); 410 | 411 | // get total pages 412 | $total_pages = $this->fetchTotalPages($posts_endpoint); 413 | 414 | // create progress bar 415 | $progressBar = $this->output->createProgressBar($total_pages); 416 | 417 | // set progress bar format 418 | $progressBar->setFormat(config('content-migration.progress_bar_format')); 419 | 420 | // loop through all pages 421 | for ($page = 1; $page <= $total_pages; ++$page) { 422 | $posts = $this->fetchPageData($posts_endpoint, $page); 423 | 424 | // create posts 425 | foreach ($posts as $post) { 426 | ContentMigration::createPost($post); 427 | } 428 | 429 | // output progress 430 | $progressBar->advance(); 431 | 432 | // break if last page 433 | if ($page === $total_pages) { 434 | break; 435 | } 436 | } 437 | 438 | $progressBar->finish(); 439 | $this->printFormattedEndMessage('Migrated posts'); 440 | } 441 | 442 | /** 443 | * Migrate pages. This method is used to migrate pages from WP API. 444 | * 445 | * @return void 446 | */ 447 | public function migratePages() 448 | { 449 | $this->info('Migrating pages'); 450 | $this->line(''); 451 | 452 | // set endpoint 453 | $pages_endpoint = $this->argument('domain').'/wp-json/wp/v2/pages'; 454 | 455 | // get pages 456 | $pages = $this->fetchData($pages_endpoint); 457 | 458 | // get total pages 459 | $total_pages = $this->fetchTotalPages($pages_endpoint); 460 | 461 | // create progress bar 462 | $progressBar = $this->output->createProgressBar($total_pages); 463 | 464 | // set progress bar format 465 | $progressBar->setFormat(config('content-migration.progress_bar_format')); 466 | 467 | // loop through all pages 468 | for ($page = 1; $page <= $total_pages; ++$page) { 469 | $pages = $this->fetchPageData($pages_endpoint, $page); 470 | 471 | // filter parent pages 472 | $parent_pages = array_filter($pages, function ($page) { 473 | return $page->parent === 0; 474 | }); 475 | 476 | // create parent pages 477 | foreach ($parent_pages as $pageToMigrate) { 478 | ContentMigration::createPage($pageToMigrate); 479 | } 480 | 481 | // filter child pages 482 | $child_pages = array_filter($pages, function ($page) { 483 | return $page->parent !== 0; 484 | }); 485 | 486 | // create child pages 487 | foreach ($child_pages as $pageToMigrate) { 488 | ContentMigration::createPage($pageToMigrate); 489 | } 490 | 491 | // output progress 492 | $progressBar->advance(); 493 | 494 | // break if last page 495 | if ($page === $total_pages) { 496 | break; 497 | } 498 | } 499 | 500 | $progressBar->finish(); 501 | $this->printFormattedEndMessage('Migrated pages'); 502 | } 503 | 504 | /** 505 | * Print formatted end message. 506 | * 507 | * @param string $message 508 | * 509 | * @return void 510 | */ 511 | public function printFormattedEndMessage($message) 512 | { 513 | $this->line(''); 514 | $this->line(''); 515 | $this->info($message); 516 | $this->line(''); 517 | } 518 | } 519 | -------------------------------------------------------------------------------- /src/ContentMigration.php: -------------------------------------------------------------------------------- 1 | app = $app; 24 | 25 | if ($this->app['config']->get('content-migration.allow_svg_media')) { 26 | add_filter( 'upload_mimes', function ( $mimes ){ 27 | $mimes['svg'] = 'image/svg+xml'; 28 | return $mimes; 29 | }); 30 | } 31 | } 32 | 33 | /** 34 | * Create WP category. This method also sets the parent category if it exists. 35 | * 36 | * @param object $category 37 | * 38 | * @return void 39 | */ 40 | public function createCategory($category) 41 | { 42 | $params = [ 43 | 'slug' => $category->slug, 44 | ]; 45 | 46 | if ($category->parent !== 0) { 47 | $parent_category = $this->app->db->table('termmeta')->where('meta_key', 'wp_api_prev_category_id')->where('meta_value', $category->parent)->value('term_id'); 48 | $params['parent'] = $parent_category; 49 | } 50 | 51 | try { 52 | // check if category exists 53 | $category_exists = get_term_by('slug', $category->slug, 'category'); 54 | 55 | if (empty($category_exists)) { 56 | // create WP term using name and slug and parent 57 | $term = wp_insert_term($category->name, 'category', $params); 58 | 59 | // save term meta for category 60 | update_term_meta($term['term_id'], 'wp_api_prev_category_id', $category->id); 61 | } 62 | } catch (\Exception $e) { 63 | $this->app->log->info('Error creating WP category : '.$e->getMessage()); 64 | } 65 | } 66 | 67 | /** 68 | * Create WP tag. This method also sets the slug for the tag. 69 | * 70 | * @param object $tag 71 | * 72 | * @return void 73 | */ 74 | public function createTag($tag) 75 | { 76 | try { 77 | // create WP term using name and slug 78 | $term_id = wp_insert_term($tag->name, 'post_tag', [ 79 | 'slug' => $tag->slug, 80 | ]); 81 | 82 | // save term meta for tag 83 | update_term_meta($term_id['term_id'], 'wp_api_prev_tag_id', $tag->id); 84 | } catch (\Exception $e) { 85 | $this->app->log->info('Error creating WP tag : '.$e->getMessage()); 86 | } 87 | } 88 | 89 | /** 90 | * Create WP media. This method also sets the alt text for the media. 91 | * 92 | * @param object $media 93 | * 94 | * @return void 95 | */ 96 | public function createMedia($media) 97 | { 98 | // set params 99 | $params = [ 100 | 'file' => $media->source_url, 101 | ]; 102 | 103 | try { 104 | // check if media exists 105 | $media_exists = get_posts([ 106 | 'post_type' => 'attachment', 107 | 'meta_key' => 'source_url', 108 | 'meta_value' => $media->source_url, 109 | 'numberposts' => 1, 110 | ]); 111 | 112 | if (!empty($media_exists)) { 113 | $this->app->log->info('Media already exists : '.$media->source_url); 114 | return; 115 | } 116 | 117 | // download to temp dir 118 | $temp_file = download_url($params['file']); 119 | 120 | if (is_wp_error($temp_file)) { 121 | $this->app->log->info('Error downloading WP media : '.$temp_file->get_error_message()); 122 | return false; 123 | } 124 | 125 | // move the temp file into the uploads directory 126 | $file = [ 127 | 'name' => basename($params['file']), 128 | 'type' => mime_content_type($temp_file), 129 | 'tmp_name' => $temp_file, 130 | 'size' => filesize($temp_file), 131 | ]; 132 | 133 | $upload = wp_handle_sideload( 134 | $file, 135 | [ 136 | 'test_form' => false, // no needs to check 'action' parameter 137 | ] 138 | ); 139 | 140 | if (!empty($sideload['error'])) { 141 | return false; 142 | } 143 | 144 | $caption = !empty($media->caption->rendered) ? $media->caption->rendered : $media->title->rendered; 145 | $description = !empty($media->description->rendered) ? $media->description->rendered : $media->caption->rendered; 146 | 147 | // create attachment 148 | $attachment = [ 149 | 'post_title' => $media->title->rendered, 150 | 'post_excerpt' => sanitize_text_field($caption), 151 | 'post_content' => sanitize_text_field($description), 152 | 'post_status' => 'inherit', 153 | 'post_mime_type' => $media->mime_type, 154 | ]; 155 | 156 | $attach_id = wp_insert_attachment($attachment, $upload['file']); 157 | 158 | // set attachment metadata 159 | wp_update_attachment_metadata($attach_id, wp_generate_attachment_metadata($attach_id, $upload['file'])); 160 | 161 | // save media meta 162 | update_post_meta($attach_id, 'wp_api_prev_featured_media_id', $media->id); 163 | 164 | // update alt text 165 | update_post_meta($attach_id, '_wp_attachment_image_alt', sanitize_text_field($media->alt_text ?? $media->caption->rendered)); 166 | } catch (\Exception $e) { 167 | $this->app->log->info('Error creating WP media : '.$e->getMessage()); 168 | } 169 | } 170 | 171 | /** 172 | * Create WP post. This method also sets the featured image, categories and tags. 173 | * 174 | * @param object $post 175 | * 176 | * @return void 177 | */ 178 | public function createPost($post) 179 | { 180 | // set post content 181 | $content = $post->content->rendered; 182 | 183 | // find previous image urls and replace with new urls 184 | $content = preg_replace_callback('/]+src="([^">]+)"/', function ($matches) use ($post) { 185 | // get attachment_id for meta_key 'wp_api_prev_featured_media_id' 186 | $media_id = $this->app->db->table('postmeta')->where('meta_key', 'wp_api_prev_featured_media_id')->where('meta_value', $post->featured_media)->value('post_id'); 187 | 188 | if (!empty($media_id)) { 189 | $media_url = wp_get_attachment_url($media_id); 190 | 191 | return str_replace($matches[1], $media_url, $matches[0]); 192 | } 193 | 194 | return $matches[0]; 195 | }, $content); 196 | 197 | // find links around images and remove 198 | $content = preg_replace('/]+>(]+>)<\/a>/', '$1', $content); 199 | 200 | // set post excerpt 201 | $excerpt = $post->excerpt->rendered; 202 | 203 | // strip html tags from excerpt 204 | $excerpt = strip_tags($excerpt); 205 | 206 | // set post status 207 | $status = $post->status; 208 | 209 | // set post type 210 | $type = $post->type; 211 | 212 | // set post title 213 | $title = $post->title->rendered; 214 | 215 | // set post slug 216 | $slug = $post->slug; 217 | 218 | // set post author 219 | $author = $post->author; 220 | 221 | // set post created date 222 | $created = $post->date; 223 | 224 | // set post categories from saved meta 225 | $categories = []; 226 | 227 | foreach ($post->categories as $category) { 228 | // get term_id for meta_key 'wp_api_prev_category_id' 229 | $categories[] = $this->app->db->table('termmeta')->where('meta_key', 'wp_api_prev_category_id')->where('meta_value', $category)->value('term_id'); 230 | } 231 | 232 | // set post tags from saved meta 233 | $tags = []; 234 | 235 | foreach ($post->tags as $tag) { 236 | // get term_id for meta_key 'wp_api_prev_tag_id' 237 | $tag_id = $this->app->db->table('termmeta')->where('meta_key', 'wp_api_prev_tag_id')->where('meta_value', $tag)->value('term_id'); 238 | 239 | // check if tag exists 240 | if (!empty($tag_id)) { 241 | $tags[] = get_term($tag_id)->name; 242 | } 243 | } 244 | 245 | // get post_id for meta_key 'wp_api_prev_featured_media_id' 246 | $media = $this->app->db->table('postmeta')->where('meta_key', 'wp_api_prev_featured_media_id')->where('meta_value', $post->featured_media)->value('post_id'); 247 | 248 | // set post meta 249 | $meta = $post->meta; 250 | 251 | try { 252 | // create WP post 253 | $post_id = wp_insert_post([ 254 | 'post_content' => $content, 255 | 'post_excerpt' => $excerpt, 256 | 'post_status' => $status, 257 | 'post_type' => $type, 258 | 'post_title' => $title, 259 | 'post_name' => $slug, 260 | 'post_author' => $author, 261 | 'post_category' => $categories, 262 | 'tags_input' => $tags, 263 | 'meta_input' => $meta, 264 | 'post_date' => $created, 265 | ]); 266 | 267 | // check if post has media 268 | if (!empty($media)) { 269 | // add featured image to post 270 | set_post_thumbnail($post_id, $media); 271 | } 272 | } catch (\Exception $e) { 273 | $this->app->log->info('Error creating WP post : '.$e->getMessage()); 274 | } 275 | } 276 | 277 | /** 278 | * Create WP page. This method also sets the parent page if it exists. 279 | * 280 | * @param object $page 281 | * 282 | * @return void 283 | */ 284 | public function createPage($page) 285 | { 286 | // set page content 287 | $content = $page->content->rendered; 288 | 289 | // process content to remove anything that includes [] 290 | $content = preg_replace('/\[[^\]]+\]/', '', $content); 291 | 292 | // set page excerpt 293 | $excerpt = $page->excerpt->rendered; 294 | 295 | // set page status 296 | $status = $page->status; 297 | 298 | // set page title 299 | $title = $page->title->rendered; 300 | 301 | // set page author 302 | $author = $page->author; 303 | 304 | // set page meta 305 | $meta = $page->meta; 306 | 307 | // set parent page from saved meta 308 | $parentId = $this->app->db->table('postmeta')->where('meta_key', 'wp_api_prev_page_id')->where('meta_value', $page->parent)->value('post_id'); 309 | 310 | try { 311 | // create WP page 312 | $page_id = wp_insert_post([ 313 | 'post_content' => $content, 314 | 'post_excerpt' => $excerpt, 315 | 'post_status' => $status, 316 | 'post_type' => 'page', 317 | 'post_title' => $title, 318 | 'post_author' => $author, 319 | 'meta_input' => $meta, 320 | 'post_parent' => $parentId, 321 | ]); 322 | 323 | // save parent page to meta 324 | if (!empty($page->parent)) { 325 | update_post_meta($page_id, 'wp_api_prev_page_parent_id', $page->parent); 326 | } 327 | 328 | update_post_meta($page_id, 'wp_api_prev_page_id', $page->id); 329 | } catch (\Exception $e) { 330 | $this->app->log->info('Error creating WP page : '.$e->getMessage()); 331 | } 332 | } 333 | } 334 | -------------------------------------------------------------------------------- /src/Facades/ClearContent.php: -------------------------------------------------------------------------------- 1 | app->singleton('ContentMigration', function () { 20 | return new ContentMigration($this->app); 21 | }); 22 | 23 | $this->app->singleton('ClearContent', function () { 24 | return new ClearContent($this->app); 25 | }); 26 | 27 | $this->mergeConfigFrom( 28 | __DIR__.'/../../config/content-migration.php', 29 | 'content-migration' 30 | ); 31 | } 32 | 33 | /** 34 | * Bootstrap any application services. 35 | * 36 | * @return void 37 | */ 38 | public function boot() 39 | { 40 | $this->publishes([ 41 | __DIR__.'/../../config/content-migration.php' => $this->app->configPath('content-migration.php'), 42 | ], 'config'); 43 | 44 | $this->commands([ 45 | ContentMigrationCommand::class, 46 | ]); 47 | 48 | $this->app->make('ContentMigration'); 49 | 50 | $this->app->make('ClearContent'); 51 | } 52 | } 53 | --------------------------------------------------------------------------------