Changeset 3359514
- Timestamp:
- 09/11/2025 02:39:51 AM (7 months ago)
- Location:
- btw-importer
- Files:
-
- 27 added
- 5 edited
-
assets/screenshot-1.png (modified) (previous)
-
assets/screenshot-2.png (added)
-
assets/screenshot-3.png (added)
-
assets/screenshot-4.png (added)
-
tags/2.0.0 (added)
-
tags/2.0.0/LICENSE (added)
-
tags/2.0.0/assets (added)
-
tags/2.0.0/assets/screenshot-1.png (added)
-
tags/2.0.0/assets/screenshot-2.png (added)
-
tags/2.0.0/assets/screenshot-3.png (added)
-
tags/2.0.0/assets/screenshot-4.png (added)
-
tags/2.0.0/btw-importer.js (added)
-
tags/2.0.0/btw-importer.php (added)
-
tags/2.0.0/changelog.md (added)
-
tags/2.0.0/index.php (added)
-
tags/2.0.0/languages (added)
-
tags/2.0.0/readme.md (added)
-
tags/2.0.0/readme.txt (added)
-
tags/2.0.0/redirect-log.php (added)
-
tags/2.0.0/redirect.php (added)
-
trunk/assets/screenshot-1.png (added)
-
trunk/assets/screenshot-2.png (added)
-
trunk/assets/screenshot-3.png (added)
-
trunk/assets/screenshot-4.png (added)
-
trunk/btw-importer.js (modified) (3 diffs)
-
trunk/btw-importer.php (modified) (1 diff)
-
trunk/changelog.md (added)
-
trunk/languages (added)
-
trunk/readme.md (modified) (6 diffs)
-
trunk/readme.txt (modified) (3 diffs)
-
trunk/redirect-log.php (added)
-
trunk/redirect.php (added)
Legend:
- Unmodified
- Added
- Removed
-
btw-importer/trunk/btw-importer.js
r3357624 r3359514 1 // btw-importer.js2 1 jQuery(document).ready(function($) { 3 let posts = []; 4 let currentIndex = 0; 2 let isImporting = false; 3 4 // Enable button after checking the notice 5 $('#agreeNotice').on('change', function() { 6 if ($(this).is(':checked')) { 7 $('#startImport').prop('disabled', false); 8 $('#importNotice').slideUp(); // collapse nicely 9 } else { 10 $('#startImport').prop('disabled', true); 11 $('#importNotice').slideDown(); // show again if unchecked 12 } 13 }); 5 14 6 function logProgress(message) { 7 $('#progress').append($('<div>').text(message)); 8 } 9 10 function importNextPost() { 11 if (currentIndex >= posts.length) { 12 logProgress('✅ Import complete.'); 13 return; 14 } 15 16 $.post(btw_importer_data.ajaxUrl, { 17 action: 'btw_importer_import_single_post', 18 nonce: btw_importer_data.nonce, 19 post: posts[currentIndex] 20 }, function(response) { 21 if (response.success) { 22 response.data.forEach(logProgress); 23 } else { 24 logProgress('❌ Error: ' + response.data); 25 } 26 currentIndex++; 27 importNextPost(); 28 }); 29 } 30 31 $('#startImport').on('click', function() { 15 $('#startImport').click(function() { 32 16 const fileInput = $('#atomFile')[0]; 33 17 if (!fileInput.files.length) { 34 alert('Please select a n Atom (.xml) file first.');18 alert('Please select a .atom file first!'); 35 19 return; 36 20 } … … 39 23 reader.onload = function(e) { 40 24 const atomContent = e.target.result; 25 $('#progress').html('📦 Parsing... Please wait... Do not reload or leave this page.'); 41 26 42 // Show parsing message 43 logProgress('📦 Parsing Atom file...'); 44 45 $.post(btw_importer_data.ajaxUrl, { 27 isImporting = true; // start importing 28 $('#importOverlay').show(); 29 $.post(btwImporter.ajaxUrl, { 46 30 action: 'btw_importer_prepare_import', 47 nonce: btw _importer_data.nonce,31 nonce: btwImporter.nonce, 48 32 atom_content: atomContent 49 33 }, function(response) { 50 if (response.success) { 51 posts = response.data.posts; 52 logProgress('✅ Found ' + posts.length + ' posts. Starting import...'); 53 importNextPost(); 34 if (!response.success) { 35 $('#progress').append('<br>❌ ' + escapeHtml(response.data)); 36 isImporting = false; // stop on error 37 return; 38 } 39 40 const allItems = response.data.posts || []; 41 if (!allItems.length) { 42 $('#progress').append('<br>⚠ No posts/pages found.'); 43 isImporting = false; 44 return; 45 } 46 47 const posts = allItems.filter(item => item.post_type === 'post'); 48 const pages = allItems.filter(item => item.post_type === 'page'); 49 50 $('#progress').append('<br>✅ Found: ' + posts.length + ' posts and ' + pages.length + ' pages'); 51 52 if (posts.length) { 53 importNext(0, posts, function() { 54 if (pages.length) { 55 $('#progress').append('<br>📦 Now importing pages...'); 56 importNext(0, pages, function() { 57 $('#progress').append('<br>🎉 All posts & pages imported!'); 58 isImporting = false; 59 $('#importOverlay').hide(); 60 }); 61 } else { 62 $('#progress').append('<br>🎉 All posts imported!'); 63 isImporting = false; 64 $('#importOverlay').hide(); 65 } 66 }); 67 } else if (pages.length) { 68 $('#progress').append('<br>📦 Only pages to import...'); 69 importNext(0, pages, function() { 70 $('#progress').append('<br>🎉 All pages imported!'); 71 isImporting = false; 72 $('#importOverlay').hide(); 73 }); 54 74 } else { 55 logProgress('❌ Failed to parse Atom file: ' + response.data); 75 $('#progress').append('<br>⚠ Nothing to import.'); 76 isImporting = false; 77 $('#importOverlay').hide(); 56 78 } 57 79 }); … … 59 81 reader.readAsText(fileInput.files[0]); 60 82 }); 83 84 function importNext(index, items, doneCallback) { 85 if (index >= items.length) { 86 doneCallback(); 87 return; 88 } 89 90 const post = items[index]; 91 $('#progress').append('<hr>'); 92 $('#progress').append('<br>📄 Importing ' + escapeHtml(post.post_type) + ': ' + escapeHtml(post.title)); 93 scrollToBottom(); 94 95 $.post(btwImporter.ajaxUrl, { 96 action: 'btw_importer_import_single_post', 97 nonce: btwImporter.nonce, 98 post: post 99 }, function(response) { 100 if (response.success && Array.isArray(response.data)) { 101 response.data.forEach(msg => { 102 let cleanMsg = escapeHtml(msg); 103 if (msg.includes('Created category') || msg.includes('Using category')) { 104 $('#progress').append('<br>🏷 ' + cleanMsg); 105 } else if (msg.includes('Finished create 301 redirect')) { 106 $('#progress').append('<br>🔁 ' + cleanMsg); 107 } else { 108 $('#progress').append('<br>' + cleanMsg); 109 } 110 }); 111 $('#progress').append('<br>----------------------------------------'); 112 } else { 113 $('#progress').append('<br>❌ Failed: ' + escapeHtml(response.data)); 114 } 115 scrollToBottom(); 116 importNext(index + 1, items, doneCallback); 117 }).fail(function(xhr, status, error) { 118 $('#progress').append('<br>❌ AJAX error: ' + escapeHtml(error)); 119 scrollToBottom(); 120 importNext(index + 1, items, doneCallback); // continue anyway 121 }); 122 } 123 124 function scrollToBottom() { 125 const progress = $('#progress'); 126 progress.scrollTop(progress[0].scrollHeight); 127 } 128 129 function escapeHtml(text) { 130 return $('<div>').text(text).html(); 131 } 132 133 // Warn user before leaving if import is running 134 window.addEventListener('beforeunload', function(e) { 135 if (isImporting) { 136 e.preventDefault(); 137 e.returnValue = 'Are you sure want to stop the import proccess?'; // standard way to show confirm dialog 138 } 139 }); 61 140 }); -
btw-importer/trunk/btw-importer.php
r3357624 r3359514 1 <?php 2 /* 3 Plugin Name: BtW Importer V1 4 Plugin URI: https://github.com/mnasikin/btw-importer 5 Description: Simple yet powerful plugin to migrate Blogger to WordPress in one click. Import .atom from Google Takeout, scan & download first image, replace URLs, set featured image, and show live progress. 6 Version: 1.0.0 7 Author: Nasikin 8 License: MIT 9 Network: true 10 Requires PHP: 7.4 11 */ 12 13 if ( ! defined( 'ABSPATH' ) ) { 14 exit; 15 } 16 17 class Btw_Importer { 18 19 public function __construct() { 20 // Register menu and scripts 21 add_action( 'admin_menu', array( $this, 'btw_importer_add_menu' ) ); 22 add_action( 'admin_enqueue_scripts', array( $this, 'btw_importer_enqueue_scripts' ) ); 23 add_action( 'wp_ajax_btw_importer_prepare_import', array( $this, 'btw_importer_ajax_prepare_import' ) ); 24 add_action( 'wp_ajax_btw_importer_import_single_post', array( $this, 'btw_importer_ajax_import_single_post' ) ); 25 } 26 27 public function btw_importer_add_menu() { 28 add_menu_page( 29 'BtW Importer', 30 'BtW Importer', 31 'manage_options', 32 'btw_importer', 33 array( $this, 'btw_importer_import_page' ), 34 'dashicons-upload' 35 ); 36 } 37 38 public function btw_importer_enqueue_scripts( $hook ) { 39 if ( 'toplevel_page_btw_importer' !== $hook ) { 40 return; 41 } 42 wp_enqueue_script('btw_importer_script', plugin_dir_url(__FILE__) . 'btw-importer.js', array('jquery'), '1.0', true); 43 wp_localize_script('btw_importer_script', 'btw_importer_data', array( 44 'ajaxUrl' => admin_url('admin-ajax.php'), 45 'nonce' => wp_create_nonce('btw_importer_nonce'), 46 )); 47 } 48 49 public function btw_importer_import_page() { 50 // Only admins 51 if ( ! current_user_can( 'manage_options' ) ) { 52 wp_die( 'Insufficient permissions' ); 53 } 54 echo '<div class="wrap"> 55 <h1>BtW Import Blogger .atom</h1> 56 <input type="file" id="atomFile" accept=".xml,.atom" /> 57 <button id="startImport" class="button button-primary">Start Import</button> 58 <div id="progress" style="margin-top:20px; max-height:400px; overflow:auto; background:#fff; padding:10px; border:1px solid #ddd;"></div> 59 </div>'; 60 } 61 62 public function btw_importer_ajax_prepare_import() { 63 // Only admins 64 if ( ! current_user_can( 'manage_options' ) ) { 65 wp_send_json_error( 'Unauthorized' ); 66 } 67 check_ajax_referer('btw_importer_nonce', 'nonce'); 68 69 // Retrieve raw XML without sanitizing tags 70 $raw_input = filter_input(INPUT_POST, 'atom_content', FILTER_UNSAFE_RAW, FILTER_REQUIRE_SCALAR); 71 $raw_input = null === $raw_input ? '' : wp_unslash($raw_input); 72 $raw_input = preg_replace('/^\x{FEFF}/u', '', $raw_input); 73 $raw_input = preg_replace('/[^\P{C}\n\r\t]+/u', '', $raw_input); 74 75 if ( empty($raw_input) ) { 76 wp_send_json_error('No data received.'); 77 } 78 79 libxml_use_internal_errors(true); 80 $xml = simplexml_load_string($raw_input); 81 if (false === $xml) { 82 $errors = libxml_get_errors(); 83 $messages = array_map(function($e){ return trim($e->message); }, $errors); 84 libxml_clear_errors(); 85 wp_send_json_error('XML parse errors: ' . implode('; ', $messages)); 86 } 87 88 $namespaces = $xml->getNamespaces(true); 89 $entries = $xml->entry; 90 if (empty($entries) && isset($namespaces['atom'])) { 91 $xml->registerXPathNamespace('a', $namespaces['atom']); 92 $entries = $xml->xpath('//a:entry'); 93 } 94 95 $posts = array(); 96 foreach ($entries as $entry) { 97 $title = (string) $entry->title; 98 $content = isset($entry->content) ? (string) $entry->content : (string) $entry->summary; 99 $dateStr = isset($entry->published) ? (string) $entry->published : (string) $entry->updated; 100 $author = isset($entry->author) ? (string) $entry->author->name : ''; 101 102 $posts[] = array( 103 'title' => sanitize_text_field($title), 104 'content' => wp_kses_post($content), 105 'date' => sanitize_text_field(date_i18n('Y-m-d H:i:s', strtotime($dateStr))), 106 'author' => sanitize_text_field($author), 107 ); 108 } 109 110 wp_send_json_success(array('posts' => $posts)); 111 } 112 113 public function btw_importer_ajax_import_single_post() { 114 // Only admins 115 if ( ! current_user_can( 'manage_options' ) ) { 116 wp_send_json_error( 'Unauthorized' ); 117 } 118 check_ajax_referer('btw_importer_nonce', 'nonce'); 119 120 $raw_posts = filter_input(INPUT_POST, 'post', FILTER_DEFAULT, FILTER_REQUIRE_ARRAY); 121 $raw_posts = is_array($raw_posts) ? array_map('wp_unslash', $raw_posts) : array(); 122 $sanitized = array_map('sanitize_text_field', $raw_posts); 123 124 if (empty($sanitized)) { 125 wp_send_json_error('Missing post data.'); 126 } 127 128 $title = $sanitized['title'] ?? ''; 129 $raw_content = $raw_posts['content'] ?? ''; 130 $date = $sanitized['date'] ?? ''; 131 $author = $sanitized['author'] ?? ''; 132 133 $msgs = array('📄 Importing post: ' . esc_html($title)); 134 $author_id = 1; 135 if ($author) { 136 $user = get_user_by('login', sanitize_user($author, true)); 137 if ($user) { 138 $author_id = $user->ID; 1 <?php 2 /* 3 Plugin Name: BtW Importer 4 Plugin URI: https://github.com/mnasikin/btw-importer 5 Description: Simple yet powerful plugin to Migrate Blogger to WordPress in one click. Import .atom from Google Takeout and the plugin will scan & download first image, replace URLs, set featured image, show live progress. 6 Version: 2.0.0 7 Author: Nasikin 8 Author URI: https://github.com/mnasikin/ 9 License: MIT 10 Domain Path: /languages 11 Text Domain: btw-importer 12 Requires PHP: 7.4 13 GitHub Plugin URI: https://github.com/mnasikin/btw-importer 14 Primary Branch: main 15 */ 16 17 class Btw_Importer { 18 private $downloaded_images = []; // cache 19 20 public function __construct() { 21 add_action('admin_menu', [$this, 'add_menu']); 22 add_action('admin_enqueue_scripts', [$this, 'enqueue_scripts']); 23 add_action('wp_ajax_btw_importer_prepare_import', [$this, 'ajax_prepare_import']); 24 add_action('wp_ajax_btw_importer_import_single_post', [$this, 'ajax_import_single_post']); 25 } 26 27 public function add_menu() { // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query 28 add_menu_page( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query 29 'BtW Importer', 'BtW Importer', 'manage_options', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query 30 'btw-importer', [$this, 'import_page'], 'dashicons-upload' // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query 31 ); 32 } 33 34 public function enqueue_scripts($hook) { 35 if ($hook !== 'toplevel_page_btw-importer') return; 36 wp_enqueue_script('btw-importer', plugin_dir_url(__FILE__).'btw-importer.js', ['jquery'], '1.2.2', true); 37 wp_localize_script('btw-importer', 'btwImporter', [ 38 'ajaxUrl' => admin_url('admin-ajax.php'), 39 'nonce' => wp_create_nonce('btw_importer_importer_nonce') 40 ]); 41 } 42 43 public function import_page() { 44 echo '<div class="wrap"> 45 <h1>BtW Importer</h1> 46 <p>A powerful yet simple migration tool, BtW Importer helps you seamlessly transfer posts, images, and formatting from Blogger (Blogspot) to WordPress. Don't forget to share this plugin if you found it's usefull</p> 47 <div id="importNotice" style="margin:20px;"> 48 <h2>⚠️ Please Read Before Importing ⚠️</h2> 49 <ul> 50 <li>🛑 ️This plugin doesn't overwrite existing posts with the same name. If you've previously used an importer, it's recommended to manually delete the previously imported content.</li> 51 <li>🛑 301 redirects only work if you previously used a custom domain on Blogspot and you're moving that domain to WordPress.</li> 52 <li>🛑 Make sure not to leave this page while the process is underway, or the import will stop, and you'll need to start from the beginning.</li> 53 <li>🛑 301 redirects work if this plugin is active and you have already run the importer.</li> 54 <li>🛑 Only image from Google/Blogspot will be downloaded.</li> 55 <li>🛑 Be sure to manually check your content after the import process is complete.</li> 56 </ul> 57 <input type="checkbox" id="agreeNotice"> 58 <label for="agreeNotice"> 59 I've read all of them and I want to start the importer. 60 </label> 61 </div> 62 <input type="file" id="atomFile" accept=".xml,.atom" /> 63 <button id="startImport" class="button button-primary" disabled>Start Import</button><br> 64 <label for="atomFile">Accepted File: .xml,.atom</label> 65 <hr> 66 <div id="importOverlay" style="display: none; position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.7); color: #fff; font-size: 20px; z-index: 9999; text-align: center; padding-top: 20%;"> 67 ⚠ Import in progress... Please don’t close, reload, or navigate away. 68 </div> 69 <div id="progress" style="margin-top:20px; max-height:100vh; max-width;100%; overflow:auto; background:#fff; padding:10px; border:1px solid #ddd;"></div> 70 </div>'; 71 } 72 73 public function ajax_prepare_import() { 74 check_ajax_referer('btw_importer_importer_nonce', 'nonce'); 75 $atom_content = isset($_POST['atom_content']) ? wp_unslash($_POST['atom_content']) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized 76 if (!$atom_content) wp_send_json_error('No data received.'); 77 78 libxml_use_internal_errors(true); 79 $xml = simplexml_load_string($atom_content); 80 if (!$xml) wp_send_json_error('Failed to parse XML.'); 81 82 $posts = []; 83 foreach ($xml->entry as $entry) { 84 $bloggerType = strtolower((string)$entry->children('blogger', true)->type); 85 $post_type = ($bloggerType === 'page') ? 'page' : 'post'; 86 87 $title = sanitize_text_field((string)$entry->title); 88 $content = (string)$entry->content; 89 $author = isset($entry->author->name) ? sanitize_text_field((string)$entry->author->name) : ''; 90 91 $published_raw = (string)$entry->published; 92 $date_gmt = gmdate('Y-m-d H:i:s', strtotime($published_raw)); 93 $date_local = get_date_from_gmt($date_gmt, 'Y-m-d H:i:s'); 94 95 // get categories 96 $categories = []; 97 foreach ($entry->category as $cat) { 98 $term = (string)$cat['term']; 99 if ($term && strpos($term, '#') !== 0) { 100 $categories[] = sanitize_text_field($term); 139 101 } 140 102 } 141 103 142 if (!function_exists('media_handle_sideload')) { 143 require_once ABSPATH . 'wp-admin/includes/media.php'; 144 require_once ABSPATH . 'wp-admin/includes/file.php'; 145 require_once ABSPATH . 'wp-admin/includes/image.php'; 146 } 147 148 $post_id = wp_insert_post(array( 149 'post_title' => $title, 150 'post_content' => '', 151 'post_status' => 'publish', 152 'post_date' => $date, 153 'post_author' => $author_id, 154 )); 155 156 if (is_wp_error($post_id)) { 157 wp_send_json_error('❌ Failed to insert post: ' . $title); 158 } 159 160 preg_match_all('/https?:\/\/[^"\']+\.(jpg|jpeg|png|gif|webp|bmp|svg|tiff|avif|ico)/i', $raw_content, $matches); 161 $urls = array_unique($matches[0]); 162 if (!empty($urls)) { 163 $first = $urls[0]; 164 $msgs[] = '⏳ Downloading image: ' . esc_url($first); 165 $tmp = download_url($first); 166 if (is_wp_error($tmp)) { 167 $msgs[] = '⚠ Failed to download image'; 104 // get old permalink from <blogger:filename> 105 $filename = (string)$entry->children('blogger', true)->filename; 106 $filename = trim($filename); 107 108 $posts[] = [ 109 'title' => $title, 110 'content' => $content, 111 'author' => $author, 112 'post_type' => $post_type, 113 'date' => $date_local, 114 'date_gmt' => $date_gmt, 115 'categories' => $categories, 116 'filename' => $filename 117 ]; 118 } 119 120 wp_send_json_success(['posts' => $posts]); 121 } 122 123 public function ajax_import_single_post() { 124 check_ajax_referer('btw_importer_importer_nonce', 'nonce'); 125 $raw_post = isset($_POST['post']) ? wp_unslash($_POST['post']) : []; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized 126 if (!$raw_post) wp_send_json_error('Missing post data.'); 127 128 $title = sanitize_text_field($raw_post['title'] ?? ''); 129 $author = sanitize_text_field($raw_post['author'] ?? ''); 130 $post_type = in_array($raw_post['post_type'], ['post','page']) ? $raw_post['post_type'] : 'post'; 131 $date = sanitize_text_field($raw_post['date'] ?? ''); 132 $date_gmt = sanitize_text_field($raw_post['date_gmt'] ?? ''); 133 $categories = $raw_post['categories'] ?? []; 134 $filename = sanitize_text_field($raw_post['filename'] ?? ''); 135 $allowed_tags = wp_kses_allowed_html('post'); 136 $allowed_tags['iframe'] = ['src'=>true,'width'=>true,'height'=>true,'frameborder'=>true,'allowfullscreen'=>true,'class'=>true,'youtube-src-id'=>true]; 137 $content = wp_kses($raw_post['content'] ?? '', $allowed_tags); 138 139 $msgs = []; 140 141 $author_id = 1; 142 if ($author) { 143 $user = get_user_by('login', sanitize_user($author, true)); 144 if ($user) $author_id = $user->ID; 145 } 146 147 require_once ABSPATH.'wp-admin/includes/image.php'; 148 require_once ABSPATH.'wp-admin/includes/file.php'; 149 require_once ABSPATH.'wp-admin/includes/media.php'; 150 151 $post_id = wp_insert_post([ 152 'post_title' => $title, 153 'post_content' => $content, 154 'post_status' => 'publish', 155 'post_date' => $date, 156 'post_date_gmt' => $date_gmt, 157 'post_author' => $author_id, 158 'post_type' => $post_type 159 ]); 160 161 if (is_wp_error($post_id)) wp_send_json_error('❌ Failed to insert: '.$title); 162 163 // add redirect meta & log redirect creation 164 if ($filename) { 165 if ($filename[0] !== '/') $filename = '/' . $filename; 166 add_post_meta($post_id, '_btw_importer_old_permalink', $filename, true); 167 $new_url = get_permalink($post_id); 168 $msgs[] = '✅ Finished create 301 redirect: '.$filename.' → '.$new_url; 169 } 170 171 // create categories 172 if (!empty($categories) && $post_type === 'post') { 173 $cat_ids = []; 174 foreach ($categories as $cat_name) { 175 $term = term_exists($cat_name, 'category'); 176 if (!$term) { 177 $new_term = wp_create_category($cat_name); 178 if (!is_wp_error($new_term)) { 179 $cat_ids[] = $new_term; 180 $msgs[] = '✅ Created category: '.$cat_name; 181 } 168 182 } else { 169 $desc = basename(wp_parse_url($first, PHP_URL_PATH)); 170 $file = array('name' => $desc, 'tmp_name' => $tmp); 171 $mid = media_handle_sideload($file, $post_id); 172 if (is_wp_error($mid)) { 173 wp_delete_file($tmp); 174 $msgs[] = '⚠ Failed to sideload image'; 175 } else { 176 $new = wp_get_attachment_url($mid); 177 foreach ($urls as $old) { 178 $raw_content = str_replace($old, $new, $raw_content); 179 $msgs[] = '✅ Replaced: ' . esc_url($old); 180 } 181 set_post_thumbnail($post_id, $mid); 182 $msgs[] = '⭐ Featured image set'; 183 $cat_ids[] = $term['term_id']; 184 $msgs[] = '✅ Using category: '.$cat_name; 185 } 186 } 187 if (!empty($cat_ids)) wp_set_post_categories($post_id, $cat_ids); 188 } 189 190 // find unique blogger/googleusercontent images by basename (after /sXXX/) 191 preg_match_all('/https?:\/\/[^"\']+\.(jpg|jpeg|png|gif|webp|bmp|svg)/i', $content, $matches); 192 $image_by_basename = []; 193 foreach (array_unique($matches[0]) as $img_url) { 194 if (!preg_match('/(blogspot|googleusercontent)/i', $img_url)) continue; 195 196 if (preg_match('#/s\d+/(.+)$#', $img_url, $m)) { 197 $basename = $m[1]; 198 } else { 199 $basename = basename(wp_parse_url($img_url, PHP_URL_PATH)); 200 } 201 202 if (!isset($image_by_basename[$basename])) { 203 $image_by_basename[$basename] = $img_url; 204 } else { 205 // prefer bigger /sXXX/ number 206 if (preg_match('#/s(\d+)/#', $img_url, $m1) && preg_match('#/s(\d+)/#', $image_by_basename[$basename], $m2)) { 207 if ((int)$m1[1] > (int)$m2[1]) { 208 $image_by_basename[$basename] = $img_url; 183 209 } 184 210 } 185 211 } 186 187 $content = wp_kses_post($raw_content); 188 wp_update_post(array('ID' => $post_id, 'post_content' => $content)); 189 190 $msgs[] = '✅ Completed: ' . esc_html($title); 191 wp_send_json_success($msgs); 192 } 193 } 194 195 new Btw_Importer(); 212 } 213 214 $first_media_id = null; 215 foreach ($image_by_basename as $img_url) { 216 if (isset($this->downloaded_images[$img_url])) { 217 $new_url = $this->downloaded_images[$img_url]; 218 $content = str_replace($img_url, $new_url, $content); 219 $msgs[]='✅ Used cached: '.$new_url; 220 continue; 221 } 222 223 $msgs[]='⏳ Downloading: '.$img_url; 224 $tmp = download_url($img_url); 225 if (is_wp_error($tmp)) { $msgs[]='⚠ Failed to download'; continue; } 226 227 $file = ['name'=>basename(wp_parse_url($img_url, PHP_URL_PATH)),'tmp_name'=>$tmp]; 228 $media_id = media_handle_sideload($file,$post_id); 229 if (is_wp_error($media_id)) { wp_delete_file($tmp); $msgs[]='⚠ Failed to attach'; continue; } 230 231 $new_url = wp_get_attachment_url($media_id); 232 if ($new_url) { 233 $this->downloaded_images[$img_url] = $new_url; 234 $content = str_replace($img_url, $new_url, $content); 235 $msgs[]='✅ Replaced: '.$img_url.' → '.$new_url; 236 if (!$first_media_id) $first_media_id = $media_id; 237 } 238 } 239 240 wp_update_post(['ID'=>$post_id,'post_content'=>$content]); 241 if ($first_media_id) { 242 set_post_thumbnail($post_id, $first_media_id); 243 $msgs[]='⭐ Successfully Set featured image'; 244 } 245 246 $msgs[]='✅ Finished '.$post_type.': '.$title; 247 wp_send_json_success($msgs); 248 } 249 } 250 251 new Btw_Importer(); 252 require_once plugin_dir_path(__FILE__) . 'redirect.php'; 253 require_once plugin_dir_path(__FILE__) . 'redirect-log.php'; -
btw-importer/trunk/readme.md
r3357624 r3359514 1 [](https:// nasikin.web.id/download/btw-importer.zip)1 [](https://github.com/mnasikin/btw-importer/releases/tag/v2.0.0) 2 2 3 3 # BtW Importer … … 6 6 7 7 A powerful yet simple migration tool, BtW Importer helps you seamlessly transfer posts, images, and formatting from Blogger (Blogspot) to WordPress. Whether you're a casual blogger or managing a large archive, this plugin handles the complex parts so you don’t have to. 8 9 ## ⚔️ Note 10 Make sure to check your content after you import contents. Also, this plugin doesn't overwrite current post or pages, so if you've imported posts or pages and want to import again, kindly delete the previous imported posts, pages, and images. 11 8 12 9 13 ## ✨ Features … … 14 18 - Displays real-time progress during import 15 19 - Supports image formats: `jpg`, `jpeg`, `png`, `gif`, `webp`, `bmp`, `svg`, `tiff`, `avif`, `ico`. Undownloaded images and videos still embedded, but with external files. 20 - Import content based on post type 21 - Keep external embedded content 22 - Posts or Pages date sync as date in the .atom file (eg. your blogspot post published on 2022/02/02, then the post in wordpress also 2022/02/02) 23 - Categories added or use existing category based on .atom file 24 - Only blogspot/google images downloaded, others external (saving your hosting storage, especially if you use external CDN) 25 - Only download originial size images (avoid duplicated) 26 - Automatically add 301 redirect from blogspot permalink to new wordpress URL to keep your SEO (only for post with `/YYYY/MM/slug.html` format) 27 - Redirect log page to check list of redirection has beed made, also option to clear redirection logs 16 28 17 29 ## 📝 Requirements 18 30 19 31 - PHP `7.4` or later 20 - cURLPHP extension32 - `cURL` PHP extension 21 33 - `allow_url_fopen` enabled 22 34 - Writable `wp-content/uploads` folder (default configuration meets this) … … 29 41 30 42 ## 📷 Screenshots 43 1. Importer Page 44  45 2. Import Process 46  47 3. Done Importing 48  49 4. Redirect Log 50  31 51 32 33 52 34 53 ## 🚀 Usage … … 44 63 ## 🧾 Changelog 45 64 46 ### 1.0.0 – 2025-07-08 65 ### 2.0.0 66 🔥 Major Update 🔥 67 - Add notice before you start importing (required) 68 - Add warning on leaving, reloading, or closing page during import to avoid accidentaly stop the process 69 - Add redirect log page to check list of redirection has beed made, also option to clear redirection logs 70 - Add 301 redirect from blogspot permalink to new wordpress URL to keep your SEO (only for post with `/YYYY/MM/slug.html` format). Only work if your previous blogspot using same Domain Name 71 - Posts or Pages date now sync as date in the .atom file (eg. your blogspot post published on 2022/02/02, then the post in wordpress also 2022/02/02) 72 - Categories added or use existing category based on .atom file 73 - Only blogspot/google images downloaded, others external (saving your hosting storage, especially if you use external CDN) 74 - Only download originial size images (avoid duplicated) 75 76 77 ### 1.0.0 47 78 - Initial release 48 79 - Replaced `parse_url()` with `wp_parse_url()` … … 53 84 ## 📢 Upgrade Notice 54 85 55 ### 1.0.0 56 Initial release of BtW Importer with Blogger `.atom` file support, media handling, and migration enhancements. 86 ### 2.0.0 57 87 58 --- 88 Major Update! This release adds many features for your import process including add notice before import, add warning on leaving page while import in process, add redirect 301 from old blogspot permalink, add redirect log and clear redirect log, sync post and page published date, add or use category based on .atom file, only download image hosted on blogspot/google, only download original image to avoid duplicated image, security update, and some UI change. -
btw-importer/trunk/readme.txt
r3357624 r3359514 1 1 === BtW Importer === 2 2 Contributors: silversh 3 Donate link: https://paypal.me/StoreDot24 3 Tags: blogger, blogspot, blogger importer, blogspot importer, import blogspot 5 Requires at least: 6.8 4 Requires at least: 6.8.1 6 5 Tested up to: 6.8 7 Stable tag: 1.0.06 Stable tag: 2.0.0 8 7 Requires PHP: 7.4 9 8 License: MIT … … 19 18 Designed to be fast, reliable, and compatible with WordPress 6.8+, this plugin streamlines the process and saves you hours of manual work. 20 19 20 == Features == 21 21 22 * Scans and downloads embedded images 22 * Replaces outdated URLs 23 * Sets featured images from the first post image 24 * Shows live progress during import 23 * Replaces outdated Blogger URLs with WordPress-friendly links 24 * Sets featured images using the first image in each post 25 * Displays real-time progress during import 26 * Supports image formats: `jpg, jpeg, png, gif, webp, bmp, svg, tiff, avif, ico`. Undownloaded images and videos still embedded, but with external files. 27 * Import content based on post type 28 * Keep external embedded content 29 * Posts or Pages date sync as date in the .atom file (e.g. your Blogspot post published on 2022/02/02, then the post in WordPress also 2022/02/02) 30 * Categories added or use existing category based on .atom file 31 * Only Blogspot/Google images downloaded, others external (saving your hosting storage, especially if you use external CDN) 32 * Only download original size images (avoid duplicated) 33 * Automatically add 301 redirect from Blogspot permalink to new WordPress URL to keep your SEO (only for post with `/YYYY/MM/slug.html` format) 34 * Redirect log page to check list of redirection has been made, also option to clear redirection logs 25 35 26 Supports image formats: jpg, jpeg, png, gif, webp, bmp, svg, tiff, avif, ico. 36 == Note == 37 Make sure to check your content after you import contents. Also, this plugin doesn't overwrite current post or pages, so if you've imported posts or pages and want to import again, kindly delete the previous imported posts, pages, and images. 27 38 28 To get your `.atom` file: 29 Blogger → Settings → Back Up → Download → Redirects to Google Takeout 39 40 == Usage == 41 42 1. Download your `.atom` file: 43 Blogger → Settings → Back Up → Download → redirects to Google Takeout 44 2. Open the BtW Importer menu in WordPress 45 3. Upload the `.atom` file from your local storage 46 4. Click Start Import 47 5. Monitor the live progress 48 6. Done! Your Blogger content is now in WordPress 30 49 31 50 == Requirements == 32 * PHP 7. 2or later51 * PHP 7.4 or later 33 52 * cURL PHP Extension 34 53 * `allow_url_fopen` enabled … … 43 62 1. Preview of the import process interface 44 63 45 == Usage == 46 1. Download your Blogger `.atom` file from Google Takeout 47 2. Open the **BtW Importer** menu in WordPress 48 3. Upload the `.atom` file from your local storage 49 4. Click **Start Import** 50 5. Monitor the live progress 51 6. Done! Your Blogger content is now available in WordPress 64 == Changelog == 65 = 2.0.0 = 66 🔥 Major Update 🔥 52 67 53 == Changelog == 54 = 1.0.0 – 2025-07-08 = 55 * Initial release 56 * Replaced `parse_url()` with `wp_parse_url()` 57 * Used `wp_delete_file()` instead of `unlink()` 58 * Sanitized input using `wp_unslash()` 59 * Sanitized content with `wp_kses_post()` 68 * Add notice before you start importing (required) 69 * Add warning on leaving, reloading, or closing page during import to avoid accidentally stopping the process 70 * Add redirect log page to check list of redirection has been made, also option to clear redirection logs 71 * Add 301 redirect from Blogspot permalink to new WordPress URL to keep your SEO (only for post with `/YYYY/MM/slug.html` format). Only works if your previous Blogspot used the same Domain Name 72 * Posts or Pages date now sync as date in the .atom file (e.g. your Blogspot post published on 2022/02/02, then the post in WordPress also 2022/02/02) 73 * Categories added or use existing category based on .atom file 74 * Only Blogspot/Google images downloaded, others external (saving your hosting storage, especially if you use external CDN) 75 * Only download original size images (avoid duplicated) 60 76 61 77 == Upgrade Notice == 62 = 1.0.0 =63 Initial release of BtW Importer with basic Blogger migration features. 78 = 2.0.0 = 79 Major Update! Please check the Changelog for more information
Note: See TracChangeset
for help on using the changeset viewer.