Plugin Directory

Changeset 3359514


Ignore:
Timestamp:
09/11/2025 02:39:51 AM (7 months ago)
Author:
silversh
Message:

Update version to 2.0.0

Location:
btw-importer
Files:
27 added
5 edited

Legend:

Unmodified
Added
Removed
  • btw-importer/trunk/btw-importer.js

    r3357624 r3359514  
    1 // btw-importer.js
    21jQuery(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    });
    514
    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() {
    3216        const fileInput = $('#atomFile')[0];
    3317        if (!fileInput.files.length) {
    34             alert('Please select an Atom (.xml) file first.');
     18            alert('Please select a .atom file first!');
    3519            return;
    3620        }
     
    3923        reader.onload = function(e) {
    4024            const atomContent = e.target.result;
     25            $('#progress').html('📦 Parsing... Please wait... Do not reload or leave this page.');
    4126
    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, {
    4630                action: 'btw_importer_prepare_import',
    47                 nonce: btw_importer_data.nonce,
     31                nonce: btwImporter.nonce,
    4832                atom_content: atomContent
    4933            }, 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                    });
    5474                } else {
    55                     logProgress('❌ Failed to parse Atom file: ' + response.data);
     75                    $('#progress').append('<br>⚠ Nothing to import.');
     76                    isImporting = false;
     77                    $('#importOverlay').hide();
    5678                }
    5779            });
     
    5981        reader.readAsText(fileInput.files[0]);
    6082    });
     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    });
    61140});
  • 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/*
     3Plugin Name:        BtW Importer
     4Plugin URI:         https://github.com/mnasikin/btw-importer
     5Description:        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.
     6Version:            2.0.0
     7Author:             Nasikin
     8Author URI:         https://github.com/mnasikin/
     9License:            MIT
     10Domain Path:        /languages
     11Text Domain:        btw-importer
     12Requires PHP:       7.4
     13GitHub Plugin URI:  https://github.com/mnasikin/btw-importer
     14Primary Branch:     main
     15*/
     16
     17class 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&apos;t forget to share this plugin if you found it&apos;s usefull</p>
     47            <div id="importNotice" style="margin:20px;">
     48            <h2>⚠️ Please Read Before Importing ⚠️</h2>
     49            <ul>
     50                <li>🛑 ️This plugin doesn&apos;t overwrite existing posts with the same name. If you&apos;ve previously used an importer, it&apos;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&apos;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&apos;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&apos;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);
    139101                }
    140102            }
    141103
    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                    }
    168182                } 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;
    183209                    }
    184210                }
    185211            }
    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
     251new Btw_Importer();
     252require_once plugin_dir_path(__FILE__) . 'redirect.php';
     253require_once plugin_dir_path(__FILE__) . 'redirect-log.php';
  • btw-importer/trunk/readme.md

    r3357624 r3359514  
    1 [![Download Plugin](https://img.shields.io/badge/download_plugin-000?style=for-the-badge&logo=download&logoColor=white)](https://nasikin.web.id/download/btw-importer.zip)
     1[![Download Plugin](https://img.shields.io/badge/download_plugin-000?style=for-the-badge&logo=download&logoColor=white)](https://github.com/mnasikin/btw-importer/releases/tag/v2.0.0)
    22
    33# BtW Importer
     
    66
    77A 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
     10Make 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
    812
    913## ✨ Features
     
    1418- Displays real-time progress during import 
    1519- 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
    1628
    1729## 📝 Requirements
    1830
    1931- PHP `7.4` or later 
    20 - cURL PHP extension 
     32- `cURL` PHP extension 
    2133- `allow_url_fopen` enabled 
    2234- Writable `wp-content/uploads` folder (default configuration meets this)
     
    2941
    3042## 📷 Screenshots
     431. Importer Page
     44![Importer Page](https://ik.imagekit.io/vbsmdqxuemd/btw-importer/v2.0.0/screenshot-1.png)
     452. Import Process
     46![Import Process](https://ik.imagekit.io/vbsmdqxuemd/btw-importer/v2.0.0/screenshot-2.png)
     473. Done Importing
     48![Done Importing](https://ik.imagekit.io/vbsmdqxuemd/btw-importer/v2.0.0/screenshot-3.png)
     494. Redirect Log
     50![Redirect Log](https://ik.imagekit.io/vbsmdqxuemd/btw-importer/v2.0.0/screenshot-4.png)
    3151
    32 ![Process Screenshot](https://raw.githubusercontent.com/mnasikin/btw-importer/refs/heads/main/assets/screenshot.png)
    3352
    3453## 🚀 Usage
     
    4463## 🧾 Changelog
    4564
    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
    4778- Initial release 
    4879- Replaced `parse_url()` with `wp_parse_url()` 
     
    5384## 📢 Upgrade Notice
    5485
    55 ### 1.0.0
    56 Initial release of BtW Importer with Blogger `.atom` file support, media handling, and migration enhancements.
     86### 2.0.0
    5787
    58 ---
     88Major 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  
    11=== BtW Importer ===
    22Contributors: silversh 
    3 Donate link: https://paypal.me/StoreDot2 
    43Tags: blogger, blogspot, blogger importer, blogspot importer, import blogspot 
    5 Requires at least: 6.8 
     4Requires at least: 6.8.1 
    65Tested up to: 6.8 
    7 Stable tag: 1.0.0 
     6Stable tag: 2.0.0 
    87Requires PHP: 7.4 
    98License: MIT 
     
    1918Designed to be fast, reliable, and compatible with WordPress 6.8+, this plugin streamlines the process and saves you hours of manual work.
    2019
     20== Features ==
     21
    2122* 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
    2535
    26 Supports image formats: jpg, jpeg, png, gif, webp, bmp, svg, tiff, avif, ico.
     36== Note ==
     37Make 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.
    2738
    28 To get your `.atom` file:
    29 Blogger → Settings → Back Up → Download → Redirects to Google Takeout
     39
     40== Usage ==
     41
     421. Download your `.atom` file: 
     43   Blogger → Settings → Back Up → Download → redirects to Google Takeout 
     442. Open the BtW Importer menu in WordPress 
     453. Upload the `.atom` file from your local storage 
     464. Click Start Import 
     475. Monitor the live progress 
     486. Done! Your Blogger content is now in WordPress
    3049
    3150== Requirements ==
    32 * PHP 7.2 or later 
     51* PHP 7.4 or later 
    3352* cURL PHP Extension 
    3453* `allow_url_fopen` enabled 
     
    43621. Preview of the import process interface
    4463
    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 🔥
    5267
    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)
    6076
    6177== 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.