{"id":360356,"date":"2026-09-03T07:38:47","date_gmt":"2026-09-03T07:38:47","guid":{"rendered":"https:\/\/wordpress.org\/plugins\/lux-content-migration\/"},"modified":"2026-09-03T09:20:08","modified_gmt":"2026-09-03T09:20:08","slug":"lux-content-migration","status":"publish","type":"plugin","link":"https:\/\/ibo.wordpress.org\/plugins\/lux-content-migration\/","author":23546060,"comment_status":"closed","ping_status":"closed","template":"","meta":{"version":"3.0.6","stable_tag":"3.0.6","tested":"7.1","requires":"6.0","requires_php":"7.4","requires_plugins":null,"header_name":"LUX Content Migration","header_author":"LUX Digest","header_description":"\uc6cc\ub4dc\ud504\ub808\uc2a4 \uc0ac\uc774\ud2b8\ub97c \ub2e4\ub978 \uc6cc\ub4dc\ud504\ub808\uc2a4 \uc0ac\uc774\ud2b8\ub85c \uc774\uc804\ud560 \ub54c, \uae00\/\ud398\uc774\uc9c0\/\ub313\uae00\/\uc791\uc131\uc790\/\ucee4\uc2a4\ud140 \ud544\ub4dc\ub97c \uc548\uc804\ud558\uac8c \uc62e\uae41\ub2c8\ub2e4(\ub0b4\ubcf4\ub0b4\uae30\/\uac00\uc838\uc624\uae30). \uc774\ubbf8\uc9c0 \uc2e4\uc81c \uc7ac\ud638\uc2a4\ud305, \ub9ac\ub514\ub809\ud2b8 \uad00\ub9ac, \ubc31\uadf8\ub77c\uc6b4\ub4dc \ucc98\ub9ac \ub4f1 \ud655\uc7a5 \uae30\ub2a5\uc740 \ubcc4\ub3c4\uc758 Pro \uc560\ub4dc\uc628\uc5d0\uc11c \uc81c\uacf5\ub429\ub2c8\ub2e4.","assets_banners_color":"","last_updated":"2026-09-03 09:20:08","external_support_url":"","external_repository_url":"","donate_link":"","header_plugin_uri":"https:\/\/wpcm.luxdigest.com","header_author_uri":"https:\/\/luxdigest.com","rating":0,"author_block_rating":0,"active_installs":0,"downloads":60,"num_ratings":0,"support_threads":0,"support_threads_resolved":0,"author_block_count":0,"sections":["description","installation","faq","changelog"],"tags":{"3.0.6":{"tag":"3.0.6","author":"luxsnap","date":"2026-09-03 09:20:08","revision":3679428}},"upgrade_notice":{"2.4.0":"<p>Import screen redesigned to adapt to what a file actually contains - fixes two real gaps where content simply couldn&#039;t be imported at all before: posts with no category, and files built from &quot;\ubbf8\ub514\uc5b4\ub9cc \ub0b4\ubcf4\ub0b4\uae30&quot; with no posts\/pages.<\/p>","2.3.0":"<p>Source sites now request &quot;\uc815\uc2dd(full)&quot; by default (matches the product being sold as a source+destination pair) instead of defaulting to export_only and requiring a manual click - requires decoblocks-marketplace 5.6.0+ on the server side. Sites already recorded as export_only from an earlier version need one &quot;\uc815\uc2dd \uc804\ud658&quot; click to move past that.<\/p>","2.2.2":"<p>Fixes export download links themselves erroring out (a nonce mismatch was showing an unrelated WordPress error page) - the nonce requirement is removed for this read-only, capability-gated download, and failures now say exactly what went wrong.<\/p>","2.2.1":"<p>Fixes &quot;\uc815\uc2dd \uc804\ud658&quot; appearing to do nothing (the result was rendering on the wrong screen), and replaces export auto-download with plain click-to-download links - every automated download trick tried so far has been silently blocked by some browser under some condition.<\/p>","2.2.0":"<p>Fixes stale-cache reads that could make a valid license appear unverified (e.g. on Redirect Manager) and could make a finished export silently fail to download - 13 option reads across the plugin now bypass the object cache consistently. Also removes the local-folder-picker export option; both export buttons now just download to your browser&#039;s normal downloads folder.<\/p>","2.1.1":"<p>Licensing server domain changed to wpcm.luxdigest.com - update if you were on an earlier version and license checks stop working after the old domain is retired.<\/p>","2.1.0":"<p>Renamed to LUX Content Migration. &quot;\uc774\ubbf8\uc9c0 \uc8fc\uc18c \uc7ac\uc801\uc6a9&quot; is now hidden entirely on a full license (already automatic) and, on the free tier, explains the license requirement instead of silently doing nothing useful.<\/p>","2.0.1":"<p>Replaces wp_handle_upload() with the direct temp-file copy that established migration plugins use for proprietary archive formats (likely the real import blocker), and removes the leftover blank browser tab after export.<\/p>","2.0.0":"<p>File format rearchitected to eliminate base64 encoding of media (the root design flaw behind the recurring import memory failures), and the two-step export flow collapsed into a single click-and-download. Existing .wpmpx files must be re-exported.<\/p>","1.9.7":"<p>Fixes auto-download after export being silently blocked by the browser (synthetic click several async steps after the real click no longer counted as a user gesture) - now uses a pre-opened tab pattern that isn&#039;t subject to that blocking.<\/p>","1.9.6":"<p>Fixes the actual cause of every import (regardless of size) crashing - a leftover PHP parse error introduced in 1.9.5 that no amount of error-handling could have caught, since it broke the file at compile time. This is the fix that should resolve the recurring &quot;\uc2ec\uac01\ud55c \uc624\ub958&quot; screen.<\/p>","1.9.5":"<p>Fixes the real memory-crash cause on import (true streaming decompression, not just post-processing), adds an individual &quot;\uae00 \uc120\ud0dd&quot; post picker, and makes exports auto-download instead of requiring a second click.<\/p>","1.9.4":"<p>Fixes the real cause of the blank &quot;\uc2ec\uac01\ud55c \uc624\ub958&quot; screen on import (embedded-file memory blowup) and a regression that silently ignored full-backup embedded images. Errors are now shown on-screen directly - no server log access needed.<\/p>","1.9.3":"<p>Fixes &quot;\uc804\uccb4 \ubbf8\ub514\uc5b4 \ubc31\uc5c5&quot; failing with a license-slot error on legitimately licensed single-site accounts - it no longer tries to consume a second &#039;full&#039; activation slot for what is a one-time export action.<\/p>","1.9.2":"<p>Adds a visible red-banner alert for any JavaScript error on the export screen - if a button still appears to do nothing, this version will show you exactly why instead of failing silently.<\/p>","1.9.1":"<p>Fixes &quot;\uc804\uccb4 \ubbf8\ub514\uc5b4 \ubc31\uc5c5&quot; appearing to loop on the folder picker with no feedback - folder selection and starting the backup are now separate steps, and failures show a real error message instead of silently resetting.<\/p>","1.9.0":"<p>Export selection redesigned into four clear modes (\uc804\uccb4\/\uce74\ud14c\uace0\ub9ac\ubcc4\/\ud398\uc774\uc9c0 \uc120\ud0dd\/\ubbf8\ub514\uc5b4\ub9cc) - choosing a category or page now automatically includes its actual images\/video, no separate media checkbox needed, and a real individual-page picker replaces the old all-pages-only toggle.<\/p>","1.8.0":"<p>&quot;\uc644\uc804 \ud3ec\ud568\ud615&quot; renamed to &quot;\uc804\uccb4 \ubbf8\ub514\uc5b4 \ubc31\uc5c5&quot; and moved out of Settings into its own button next to the normal export - was previously unreachable for most users due to a license-state check almost no source site satisfies. Now auto-upgrades the license when clicked, and (Chrome\/Edge) offers to save parts directly into a chosen local folder.<\/p>","1.7.0":"<p>New full-license &quot;\uc644\uc804 \ud3ec\ud568\ud615&quot; export embeds actual image\/video files (read from local disk, not re-downloaded) so the source site can be deleted immediately after export - includes a live per-site disk-space pre-check (no fixed assumption like 2GB) and clear warnings for all three image-handling modes.<\/p>","1.6.0":"<p>Export Prep now has real content-type and category selection (was previously all-or-nothing with no options), plus an item count on the completed export so you can sanity-check file size against actual post\/page\/media counts.<\/p>","1.5.3":"<p>Fixes site role settings that appeared to reset\/re-ask repeatedly - forces a cache-bypass on every read and verifies the save actually landed, addressing likely object-cache staleness. Diagnostics page now shows raw DB value vs. cached value side by side.<\/p>","1.5.2":"<p>Menu registration switched from a same-slug trick to WordPress&#039;s standard documented method (each submenu has its own slug now), plus a hidden diagnostics page for troubleshooting menu issues without guesswork.<\/p>","1.5.1":"<p>UI cleanup: Export Prep is now just the export button (license\/redirect clutter removed), Settings split into two real tabs instead of one long page, role picker collapsed to &quot;current role + change&quot; instead of always showing the full form, and a stray &quot;XML \uc5c5\ub85c\ub4dc&quot; label corrected to match the actual .wpmpx format.<\/p>","1.5.0":"<p>Export generation is now fully batch\/streaming-based (safe for any site size, won&#039;t hit memory\/time limits regardless of hosting), with a visible progress bar and &quot;don&#039;t navigate away&quot; notice while it runs. .wpmpx format marker bumped to v2 - re-export any file you haven&#039;t imported yet.<\/p>","1.4.0":"<p>Split export\/import for large sites (was completely missing before) - export now breaks into numbered parts under a configurable size, and the import screen accepts all parts at once and reassembles them automatically.<\/p>","1.3.0":"<p>Exports are now this plugin&#039;s own exclusive .wpmpx format (no longer standard WordPress WXR\/.xml, no longer readable by other tools), and attachments with unrecognized extensions (notably all video files) are no longer silently mislabeled as .jpg.<\/p>","1.2.0":"<p>Navigation redesign (page-switcher vs. in-page section rail no longer mixed) and one-click export directly from the plugin&#039;s own screen instead of handing off to WordPress&#039;s generic Tools \u2192 Export screen.<\/p>","1.1.3":"<p>Fixes Import\/Export Prep screens loading with no CSS\/JS at all (wrong hook-name guess in 1.1.1\/1.1.2), and makes the stuck-license-role fix from 1.1.2 fully automatic instead of requiring you to resubmit the role form.<\/p>","1.1.2":"<p>Fixes source-site license verification getting permanently stuck requesting the paid &quot;full&quot; role instead of the free &quot;export_only&quot; role after any prior full-role request - update if your export\/source site&#039;s license shows &quot;\ud655\uc778\ub418\uc9c0 \uc54a\uc74c&quot; despite a correct key.<\/p>","1.1.1":"<p>Fixes a dead first menu item and missing CSS\/JS on the Import\/Export screens (both introduced in 1.1.0&#039;s menu restructuring) - update if clicking the top menu item did nothing, or the screens looked unstyled.<\/p>","1.1.0":"<p>Source-site &quot;Export Prep&quot; screen added (was missing since 1.0.0) - update if the old site&#039;s menu only showed Redirects with nothing about exporting.<\/p>","1.0.0":"<p>Initial release.<\/p>"},"ratings":[],"assets_icons":{"icon-128x128.png":{"filename":"icon-128x128.png","revision":3679234,"resolution":"128x128","location":"assets","locale":"","width":128,"height":128},"icon-256x256.png":{"filename":"icon-256x256.png","revision":3679234,"resolution":"256x256","location":"assets","locale":"","width":256,"height":256}},"assets_banners":[],"assets_blueprints":{},"all_blocks":[],"tagged_versions":["3.0.6"],"block_files":[],"assets_screenshots":[],"screenshots":{"1":"Role selection on first activation.","2":"Category-by-category import screen with live progress."}},"plugin_section":[],"plugin_tags":[1859,87,84,4155,727],"plugin_category":[50,59],"plugin_contributors":[275590],"plugin_business_model":[],"class_list":["post-360356","plugin","type-plugin","status-publish","hentry","plugin_tags-export","plugin_tags-import","plugin_tags-media","plugin_tags-migration","plugin_tags-redirect","plugin_category-media","plugin_category-utilities-and-tools","plugin_contributors-luxsnap","plugin_committers-luxsnap"],"banners":[],"icons":{"svg":false,"icon":"https:\/\/ps.w.org\/lux-content-migration\/assets\/icon-128x128.png?rev=3679234","icon_2x":"https:\/\/ps.w.org\/lux-content-migration\/assets\/icon-256x256.png?rev=3679234","generated":false},"screenshots":[],"raw_content":"<!--section=description-->\n<p>LUX Content Migration is installed on <strong>both<\/strong> sides of a WordPress-to-WordPress migration:<\/p>\n\n<ul>\n<li>On the <strong>old (source) site<\/strong>, it builds a self-contained export file in this plugin's own format (.wpmpx), with an automatic split into numbered parts if your hosting's upload limit is small.<\/li>\n<li>On the <strong>new (destination) site<\/strong>, it imports that .wpmpx file category by category, bringing over posts, pages, comments (including reply\/parent structure), and authors (automatically matched by username when the same login exists on the destination site).<\/li>\n<\/ul>\n\n<p>This plugin is fully functional with no limits on the number of posts, pages, or sites - there is no license key, no usage cap, and no locked feature anywhere in this codebase.<\/p>\n\n<h4>What this plugin does<\/h4>\n\n<ul>\n<li>Export from the source site (including automatic splitting into parts if needed) and import into the destination site, both unlimited.<\/li>\n<li>Comments (including reply\/parent structure), author linking, and custom field \/ ACF relational ID reconnection.<\/li>\n<li>Dry-run preview \u2014 see exactly how many posts, comments, and images an import will involve before you run it, without creating anything.<\/li>\n<li>Rollback \u2014 undo a finished category or page import, deleting the posts it created (downloaded images are left in place since other posts may still reference them).<\/li>\n<li>Images referenced in migrated content keep pointing at the old site's own URLs rather than being re-downloaded - so the old site needs to stay online for those images to keep displaying. A separate add-on plugin (distributed outside WordPress.org, see the plugin's own site for details) adds actual image rehosting, redirect\/404 management, background processing, and full media embedding on top of this plugin via standard WordPress filters\/actions - this base plugin works completely on its own without it.<\/li>\n<\/ul>\n\n<h4>Security notes<\/h4>\n\n<ul>\n<li>Uploaded .wpmpx files are verified by a fixed signature and checksum before being parsed, and legacy WXR\/XML uploads are parsed with a hardened XML reader (any <code>&lt;!DOCTYPE&gt;<\/code> declaration is stripped before parsing and external entity\/network loading is disabled) to prevent XXE-style attacks from a malicious export file.<\/li>\n<li>File uploads are validated against the request's own upload metadata via <code>is_uploaded_file()<\/code> and moved through the WordPress Filesystem API into a non-listable, non-executable private folder that is cleared automatically after each file is processed.<\/li>\n<\/ul>\n\n<h3>External services<\/h3>\n\n<p>This plugin does not connect to any external service. All processing (reading the export file, creating posts\/pages\/comments\/authors, custom field reconnection) happens entirely on your own WordPress site's server.<\/p>\n\n<!--section=installation-->\n<ol>\n<li>Install and activate LUX Content Migration on <strong>both<\/strong> the new site and the old site.<\/li>\n<li>On first visit to \"LUX Content Migration\" in the admin menu, choose whether this site is the migration destination (new site) or source (old site).<\/li>\n<li>On the source site: go to LUX Content Migration \u2192 Export, choose what to export, and download the resulting .wpmpx file (it may be split into numbered parts).<\/li>\n<li>On the destination site: go to LUX Content Migration \u2192 Import, upload that .wpmpx file (select all parts together if it was split), and import it category by category.<\/li>\n<\/ol>\n\n<!--section=faq-->\n<dl>\n<dt id=\"do%20i%20need%20to%20install%20this%20on%20the%20old%20site%20too%3F\"><h3>Do I need to install this on the old site too?<\/h3><\/dt>\n<dd><p>Yes - install and activate it on both the old (source) site and the new (destination) site. The source site builds the .wpmpx export file that the destination site then imports.<\/p><\/dd>\n<dt id=\"will%20my%20images%20move%20too%3F\"><h3>Will my images move too?<\/h3><\/dt>\n<dd><p>The URLs referencing images in your migrated posts\/pages are kept exactly as they were on the old site - this plugin does not download or re-host image files. As long as the old site stays online, those images continue to display normally on the new site.<\/p><\/dd>\n<dt id=\"does%20this%20plugin%20have%20any%20limits%20on%20the%20number%20of%20posts%2C%20pages%2C%20or%20sites%3F\"><h3>Does this plugin have any limits on the number of posts, pages, or sites?<\/h3><\/dt>\n<dd><p>No. Every feature in this plugin - export, import, comments, author linking, custom field\/ACF reconnection, dry-run preview, rollback - works with no count limit, no time limit, and no license key required.<\/p><\/dd>\n\n<\/dl>\n\n<!--section=changelog-->\n<h4>3.0.6<\/h4>\n\n<ul>\n<li>Fix: <code>fwrite()<\/code> return values are now checked when writing the split .wpmpx export parts in <code>WPMP_Exporter::finalize_job()<\/code>. A partial write is retried; if the disk genuinely can't be written to, the export aborts, deletes the partial part files, and returns a clear error instead of silently producing a truncated\/corrupted export archive.<\/li>\n<li>Hardening: <code>$_POST['source_category']<\/code> \/ <code>$_POST['category_name']<\/code> are now passed through <code>sanitize_text_field()<\/code> before use in <code>WPMP_Import_Admin<\/code> (category-start, rollback, and preview handlers), consistent with the rest of the plugin's input handling.<\/li>\n<li>Clarity: the .wpmpx metadata-length header is now packed\/unpacked with the explicit 32-bit big-endian <code>N<\/code> format code (two halves) instead of the 64-bit <code>J<\/code>\/<code>P<\/code> codes, to remove any ambiguity about byte order for future readers\/reviewers. Output bytes are unchanged, so this is fully compatible with .wpmpx files created by earlier 3.0.x versions.<\/li>\n<\/ul>\n\n<h4>3.0.5<\/h4>\n\n<ul>\n<li><strong>Critical fix: with the paid add-on's full license active, real image\/video rehosting silently did nothing<\/strong> - content kept pointing at the old site's URLs and featured images were always left empty, even though the add-on was correctly downloading files and creating attachments on the destination site. Root cause: <code>WPMP_Importer::process_attachment_batch()<\/code> passes its <code>$global<\/code> state (the URL-rewrite map and the old-attachment-ID \u2192 new-attachment-ID map) into <code>apply_filters( 'lux_content_migration_process_attachment_batch', ... )<\/code> - but PHP\/WordPress break by-reference passing across <code>apply_filters()<\/code>, so the add-on's own callback (which correctly re-reads and re-saves this state on every batch) was having its save immediately overwritten by this plugin's own caller, which was still holding - and re-saving - the state exactly as it was <em>before<\/em> that batch ran. This happened on every single batch tick, so the URL map and attachment-ID map could never actually accumulate any entries, no matter how many images the add-on successfully rehosted. Fixed by re-reading the freshly-saved state back into this function's reference parameter immediately after a filter callback reports it handled the batch, so this plugin's own follow-up save can no longer clobber it. Free-plugin-only fix - no change needed on the add-on side, and no change for anyone running the free plugin standalone (which never populates this state in the first place).<\/li>\n<\/ul>\n\n<h4>3.0.4<\/h4>\n\n<ul>\n<li><strong>Fixed a real bug:<\/strong> the \"\ub0b4\ubcf4\ub0b4\uae30 \uc900\ube44\" (Export) screen had a second \"\ud83d\udce6 \uc804\uccb4 \ubbf8\ub514\uc5b4 \ubc31\uc5c5\" button since the very first release, but this plugin's own <code>ajax_export_start()<\/code> AJAX handler unconditionally rejected any request that button sent (<code>embed_files: 1<\/code>), regardless of whether the paid add-on was installed or licensed - so that button never actually worked for anyone, including fully-licensed paying customers of the add-on. It's been removed, and the underlying handler now asks the add-on (via a new filter, <code>lux_content_migration_embed_files_available<\/code>) whether the request should be allowed before rejecting it - the same request that used to always fail now succeeds when the add-on is present and licensed for full access.<\/li>\n<li>Replaced that removed button with a single \"\uc774\ubbf8\uc9c0\/\uc601\uc0c1\uae4c\uc9c0 \ud3ec\ud568\ud574\uc11c \uc644\uc804\ud788 \ub2f4\uae30\" (include images\/video) card next to the main export button. When the add-on reports (via the new filter above) that this is actually available, it renders the real, working checkbox and its own guidance text via two more new hooks (<code>lux_content_migration_render_embed_files_option<\/code> action, <code>lux_content_migration_set_embed_files_preference<\/code> action to persist the choice) - otherwise this plugin shows a \"log in to your license\" or \"learn about the Pro add-on\" link depending on whether the add-on is installed at all (reusing the <code>lux_content_migration_pro_addon_installed<\/code>\/<code>lux_content_migration_pro_settings_url<\/code> filters from 3.0.2).<\/li>\n<li>Rewrote the guidance text on the export screen to precisely spell out all four real-world combinations (free vs. licensed add-on, crossed with \"keeping the old site online\" vs. \"closing\/moving it soon\") instead of the previous one-line summary, based directly on user feedback about how confusing the previous wording was.<\/li>\n<li><code>render_screen_switcher()<\/code> now also shows a \"\uc790\ub3d9 \ub9ac\ub514\ub809\ud2b8 \ucd94\ucc9c\" (automatic redirect suggestions) pill on the destination-site role, linking to the same redirect-management screen the source role already links to - the add-on renders different content there depending on which role visits it. Previously this cross-screen link only appeared for the source role.<\/li>\n<\/ul>\n\n<h4>3.0.3<\/h4>\n\n<ul>\n<li><strong>Screen reorganization (user feedback):<\/strong> the \"\uc124\uc815 \/ \ub77c\uc774\uc120\uc2a4\" (Settings \/ License) screen defaulted to a \"Pro \uc560\ub4dc\uc628\" tab (generic promotional copy, or - since 3.0.2 - a link to the add-on's own license screen) shown before the \"\uc0ac\uc774\ud2b8 \uc5ed\ud560\" (Site Role) tab, even though site role is this screen's actual, original purpose and the Pro-related content duplicates the add-on's own \"Pro \uc124\uc815\" menu item once installed. Removed the tab switcher entirely: this screen (renamed \"\uc5ed\ud560 \uc124\uc815\" \/ Role Setting, in both its menu label and page title) now shows the site-role card directly, with the Pro add-on card kept as a secondary section below it (still needed since a few \"Pro \uc560\ub4dc\uc628 \uc54c\uc544\ubcf4\uae30\" links elsewhere in this plugin point here).<\/li>\n<\/ul>\n\n<h4>3.0.2<\/h4>\n\n<ul>\n<li>Fixed the \"Pro \uc560\ub4dc\uc628\" tab on this plugin's own \"\uc124\uc815 \/ \ub77c\uc774\uc120\uc2a4\" (Settings \/ License) screen always showing the generic \"buy now\" promotional copy, even when the paid add-on was already installed - so a customer who had already purchased and installed the add-on, and naturally looked for the license field on this plugin's own settings screen, would find no way to enter their license key there at all (the real license field lives on a separate screen the add-on registers for itself, under a different menu label). This screen now checks two new neutral filters, <code>lux_content_migration_pro_addon_installed<\/code> and <code>lux_content_migration_pro_settings_url<\/code> (both default to false\/empty when no add-on is present, so standalone use of this free plugin is unaffected), and when an add-on is detected, shows a direct link to its actual license\/settings screen instead of the promotional copy.<\/li>\n<\/ul>\n\n<h4>3.0.0<\/h4>\n\n<ul>\n<li>Major structural change in response to WordPress.org review feedback (Guideline 5, Trialware): removed every license-gated feature's implementation from this plugin entirely, rather than merely hiding it behind a check. Image rehosting, full media backup (embedding attachment bytes), redirect\/404 management, automatic redirect-suggestion CSV export, and background\/unattended processing are no longer present in this codebase in any form - not even as dormant code behind a filter. This plugin now always does exactly what it does for free (posts, pages, comments, authors, custom fields, dry-run preview, rollback - no limits), with no license concept, no license screen, and no external license-verification calls anywhere in the code.<\/li>\n<li>The removed functionality is available as a separate add-on plugin, distributed outside WordPress.org, which extends this plugin via a small set of neutral WordPress filters\/actions (<code>lux_content_migration_is_pro<\/code>, <code>lux_content_migration_process_attachment_batch<\/code>, <code>lux_content_migration_embed_attachment_file<\/code>, <code>lux_content_migration_export_embed_item<\/code>, <code>lux_content_migration_post_imported<\/code>, <code>lux_content_migration_retry_failed_attachment<\/code>, <code>lux_content_migration_render_admin_notices<\/code>, <code>lux_content_migration_role_saved<\/code>, <code>lux_content_migration_deactivate<\/code>, <code>lux_content_migration_reset_all<\/code>, <code>lux_content_migration_pro_addon_installed<\/code>, <code>lux_content_migration_pro_settings_url<\/code>, <code>lux_content_migration_embed_files_available<\/code>, <code>lux_content_migration_render_embed_files_option<\/code>, <code>lux_content_migration_set_embed_files_preference<\/code>) - the same extensibility pattern used by other WordPress.org-hosted freemium plugins for their paid add-ons.<\/li>\n<li>Removed bundled translation <code>.po<\/code>\/<code>.mo<\/code> files and the <code>load_plugin_textdomain()<\/code> call - translations for WordPress.org-hosted plugins are handled automatically via translate.wordpress.org since WordPress 4.6.<\/li>\n<li>Re-added the nonce check on the export-part download endpoint that had been removed in an earlier version based on a since-corrected misdiagnosis (the actual cause of that download failure was an unrelated double-<code>sanitize_file_name()<\/code> bug, already fixed separately).<\/li>\n<\/ul>\n\n<h4>3.0.1<\/h4>\n\n<ul>\n<li>Fixed a fatal error on \"reset everything\": <code>WPMP_Store::reset_all()<\/code> still called <code>WPMP_Importer::disable_background_mode()<\/code>, a method that had already been deleted from <code>WPMP_Importer<\/code> when background processing moved to the Pro add-on. Replaced with a <code>lux_content_migration_reset_all<\/code> action so a Pro add-on can clean up its own background-processing state on reset, without the free plugin needing to know anything about it.<\/li>\n<li>Fixed the export manifest always reporting <code>embed_files<\/code> as false: the value returned by the <code>lux_content_migration_export_embed_item<\/code> filter was computed per-batch but never written back onto the persisted export job, so <code>finalize_job()<\/code> could never see it. A Pro add-on embedding attachment bytes into the export file is now correctly reflected in the finished manifest.<\/li>\n<li>Fixed the plugin package itself containing a duplicated, slightly-stale nested copy of its own files from an earlier draft - no functional impact on a correctly-installed copy, but cleaned up for a tidy WordPress.org package.<\/li>\n<li>Rewrote <code>uninstall.php<\/code>: it still dropped the <code>wp_redirects<\/code>\/<code>wp_redirect_404_log<\/code> database tables and deleted license\/redirect\/usage-limit options left over from the removed features. This plugin no longer creates any of that data (it belongs to the separate Pro add-on now), so uninstalling this plugin no longer touches it - each plugin only cleans up what it actually created.<\/li>\n<\/ul>\n\n<h4>2.8.2<\/h4>\n\n<ul>\n<li>Fixed two leftover code comments still referencing the old main file name (<code>wp-content-migration.php<\/code>) instead of the current one (<code>lux-content-migration.php<\/code>) - harmless (comments aren't executed), but corrected for accuracy.<\/li>\n<\/ul>\n\n<h4>2.8.1<\/h4>\n\n<ul>\n<li>Fix (WordPress.org Plugin Check automated scan failures): replaced <code>move_uploaded_file()<\/code> (a forbidden direct filesystem function) with the WP_Filesystem API's <code>move()<\/code> method for handling uploaded .wpmpx files - the <code>is_uploaded_file()<\/code> security check that verifies the file genuinely came from this request's upload is kept in place, since WP_Filesystem doesn't perform that check itself. Also updated \"Tested up to\" from 7.0 to 7.1 (current WordPress release).<\/li>\n<\/ul>\n\n<h4>2.7.3<\/h4>\n\n<ul>\n<li>Fix: <code>Contributors<\/code> in readme.txt was set to <code>luxdigest<\/code> (the display name) instead of the actual WordPress.org login username (<code>luxsnap<\/code>) - required to match the account submitting this plugin for review.<\/li>\n<\/ul>\n\n<h4>2.7.2<\/h4>\n\n<ul>\n<li>Updated \"Tested up to\" from 6.6 to 7.0 (current WordPress release) ahead of WordPress.org submission.<\/li>\n<\/ul>\n\n<h4>2.7.1<\/h4>\n\n<ul>\n<li>Renamed the \"\ud300\" pricing tier to \"\uc5d0\uc774\uc804\uc2dc(\uba40\ud2f0\uc0ac\uc774\ud2b8)\" throughout, and corrected the license description that still said \"\uc2ac\ub86f 1\uac1c\" (one slot) for a single license - it's actually 2 (source + target, a pair), matching the fix made earlier this cycle.<\/li>\n<\/ul>\n\n<h4>2.7.0<\/h4>\n\n<ul>\n<li>Added a complete English translation (<code>languages\/wp-content-migration-en_US.po<\/code>\/<code>.mo<\/code>) covering all 407 translatable strings in the plugin - on an English-locale WordPress install, every screen now displays in English instead of the Korean source text, which matters for WordPress.org review (typically done on an English test site) and for this plugin's primarily English-speaking market. The Korean source strings in the code itself are unchanged - this is purely an additional translation layer, loaded automatically by WordPress via <code>load_plugin_textdomain()<\/code> based on the site's locale. Verified the compiled <code>.mo<\/code> file parses correctly with Python's own <code>gettext<\/code> module before shipping.<\/li>\n<\/ul>\n\n<h4>2.6.8<\/h4>\n\n<ul>\n<li>Fix (WordPress.org submission blocker): the text domain was still <code>wp-media-porter<\/code> throughout the codebase (plugin header, <code>load_plugin_textdomain()<\/code>, and all 477 translation function calls) even though the plugin was renamed to <code>wp-content-migration<\/code> back in 2.1.0 - WordPress.org requires the text domain to exactly match the plugin slug, and a mismatch here would fail automated review. Corrected everywhere.<\/li>\n<li>Removed the bundled <code>wp-media-porter-*.mo\/.po\/.pot<\/code> translation files - they referenced the old (now wrong) text domain and, being from before roughly half of this plugin's current screens existed, covered only a fraction of today's actual strings. Regenerated <code>languages\/wp-content-migration.pot<\/code> from scratch against the current codebase (407 unique translatable strings) using a custom extractor built for this session (no gettext toolchain was available to run <code>wp i18n make-pot<\/code>). Translated <code>.po<\/code>\/<code>.mo<\/code> files for specific languages (en_US, ja, etc.) are not included - see the note below.<\/li>\n<\/ul>\n\n<h4>2.6.7<\/h4>\n\n<ul>\n<li>Reworded the \"\uc804\uccb4 \ubbf8\ub514\uc5b4 \ubc31\uc5c5\" caption to lead with its biggest selling point: the file itself is a standalone backup - it can sit unused for weeks or months and still be imported later to recreate the site, not just a one-time transfer artifact. Also renamed \"\uc790\ub3d9 \ub9ac\ub514\ub809\ud2b8 \ucd94\ucc9c\"\/\"\ubc31\uadf8\ub77c\uc6b4\ub4dc \ubb34\uc778 \ucc98\ub9ac\" to \"\ub9ac\ub514\ub809\ud2b8 \uad00\ub9ac\"\/\"\ubc31\uadf8\ub77c\uc6b4\ub4dc \ud504\ub85c\uc138\uc2a4\" in this caption for a punchier, more marketing-appropriate phrasing (the fuller technical description on the Settings\/License screen is unchanged).<\/li>\n<\/ul>\n\n<h4>2.6.6<\/h4>\n\n<ul>\n<li>Reworded the caption under \"\uc804\uccb4 \ubbf8\ub514\uc5b4 \ubc31\uc5c5\" to stop repeating the \"\ubbf8\ub514\uc5b4 \uc8fc\uc18c \uc790\ub3d9 \uc801\uc6a9\" line already covered by the paragraph above it, and instead briefly point out the two other paid-only features this plugin has (\uc790\ub3d9 \ub9ac\ub514\ub809\ud2b8 \ucd94\ucc9c, \ubc31\uadf8\ub77c\uc6b4\ub4dc \ubb34\uc778 \ucc98\ub9ac) that aren't specific to this button but are worth knowing about while looking at a paid-tier option.<\/li>\n<\/ul>\n\n<h4>2.6.5<\/h4>\n\n<ul>\n<li>Fix: \"\uc120\ud0dd\ud55c \ud56d\ubaa9 \ub0b4\ubcf4\ub0b4\uae30\" is usable on both free and paid tiers (unlike \"\uc804\uccb4 \ubbf8\ub514\uc5b4 \ubc31\uc5c5\", which is paid-only) - the caption under it now shows both outcomes (\"\ubb34\ub8cc: \uc801\uc6a9 \ubd88\uac00\" \/ \"\uc720\ub8cc: \uc790\ub3d9 \uc801\uc6a9, \uc6d0\ubcf8 \uc0ac\uc774\ud2b8\uac00 \uac00\uc838\uc624\uae30 \ub05d\ub0a0 \ub54c\uae4c\uc9c0 \ucf1c\uc838 \uc788\uc5b4\uc57c \ud568\") instead of a single line that only described one of them.<\/li>\n<\/ul>\n\n<h4>2.6.4<\/h4>\n\n<ul>\n<li>Simplified the per-button captions under \"\uc120\ud0dd\ud55c \ud56d\ubaa9 \ub0b4\ubcf4\ub0b4\uae30\"\/\"\uc804\uccb4 \ubbf8\ub514\uc5b4 \ubc31\uc5c5\" to one line each (\"\uc0c8\ub85c\uc6b4 \ubbf8\ub514\uc5b4 \uc8fc\uc18c \uc801\uc6a9 \ubd88\uac00\" \/ \"\uc790\ub3d9 \uc801\uc6a9\") - the fuller explanation was already given in the paragraph just above, so repeating it under each button read as cluttered. Added a \"\ud83d\uded2 \uc815\uc2dd \ub77c\uc774\uc120\uc2a4 \uad6c\ub9e4\ud558\uae30\" button directly under the locked PRO option so there's an immediate path to buy without needing to go to Settings first.<\/li>\n<\/ul>\n\n<h4>2.6.3<\/h4>\n\n<ul>\n<li>Reworded the free-tier export copy (\"\uc774\ubbf8\uc9c0\/\uc601\uc0c1\uc740 \uc8fc\uc18c\ub9cc \ud3ec\ud568\ub429\ub2c8\ub2e4\") - it read as if the addresses were being handled\/processed somehow, when in fact nothing changes: the old site's URLs are kept as-is with no rewriting at all, so closing or moving the source site later breaks every image\/video and each one must be manually re-pointed. The paid \"\uc804\uccb4 \ubbf8\ub514\uc5b4 \ubc31\uc5c5\" copy is now explicit about the actual contrast: it automatically re-addresses images\/video to the new site.<\/li>\n<\/ul>\n\n<h4>2.6.2<\/h4>\n\n<ul>\n<li>Fix (real gap): the license screen had no way to actually buy a license - it only assumed you already had a key and were entering it. Someone landing here on the free tier had no path forward. Added a \"\ud83d\uded2 \uc815\uc2dd \ub77c\uc774\uc120\uc2a4 \uad6c\ub9e4\ud558\uae30\" button (linking to wpcm.luxdigest.com's pricing section) shown prominently when no key is saved yet, plus a smaller link for existing customers who need a key for another site.<\/li>\n<\/ul>\n\n<h4>2.6.1<\/h4>\n\n<ul>\n<li>Import screen now shows a progress bar (matching the export screen) instead of only raw counts, and fixes a real bug where the \"\ucc98\ub9ac \uc911\uc785\ub2c8\ub2e4...\" message stayed on screen even after the job actually finished (counts showed 90\/90 and 8\/8 complete, but the status text and lack of visual completion made it look stuck) - the text now switches to \"\uc644\ub8cc\ub418\uc5c8\uc2b5\ub2c8\ub2e4\" and the bar fills to 100% the moment the job's last poll reports done, instead of only silently revealing the finalize button.<\/li>\n<\/ul>\n\n<h4>2.6.0<\/h4>\n\n<ul>\n<li>Fix (the actual, deeper root cause of images not being relinked - upstream of the 2.5.2 fix, which only addressed rewriting and never actually fired for most images): <code>collect_needed_attachments()<\/code> decided whether an image was \"needed\" by checking if the exact original attachment URL string appeared in post content. Since WordPress normally inserts a resized variant into content (not the original), this exact-match check failed for the majority of real in-content images - meaning they were never even queued for download in the first place, so there was nothing for the URL rewriter to work with regardless of how good it was. Detection now also matches by filename (independent of any resize suffix), and separately scans post content directly for <code>src=<\/code>\/<code>srcset=<\/code>\/Gutenberg-block image URLs pointing at any other host - including images that were never registered as a WordPress attachment on the source site at all - and queues those too as on-the-fly entries.<\/li>\n<li>Changed <code>rewrite_urls()<\/code> to replace any old-host reference to a given filename (with or without a resize suffix) with the new site's plain original URL, rather than trying to reconstruct a matching resize suffix on the new site (which may not exist). Guarantees a working image over a possibly-broken exact-size match.<\/li>\n<\/ul>\n\n<h4>2.5.2<\/h4>\n\n<ul>\n<li>Fix (real, likely the main cause of \"\uc774\ubbf8\uc9c0\uac00 \uc7ac\uc815\uc758\uac00 \uc548 \ub41c\ub2e4\"): <code>rewrite_urls()<\/code> only replaced post content URLs that matched an attachment's original URL <em>exactly<\/em>. But WordPress normally inserts a resized variant into post content when you add an image via the editor (e.g. <code>photo-300x200.jpg<\/code> for \"Medium\" size), not the raw original filename - so the one case this whole feature exists for (images actually appearing in post content) was frequently left pointing at the old site, while only exact-match references (like a featured image, which does store the original URL) got rewritten correctly. Added a second pass that recognizes the <code>filename-WxHeight.ext<\/code> resize pattern and rewrites it to the same size suffix on the new site's uploaded copy (WordPress regenerates the same intermediate sizes automatically on upload, so this size normally exists). The existing srcset-stripping safety net still runs afterward for anything neither pass caught.<\/li>\n<\/ul>\n\n<h4>2.5.1<\/h4>\n\n<ul>\n<li>Moved the split-part indicator from the end of the filename to the front: <code>NEWS-LUX-...-074327.wpmpx.part1of2<\/code> becomes <code>part1of2-NEWS-LUX-...-074327.wpmpx<\/code>. The old suffix placement buried the only thing that differs between parts at the very end of an already-long, mostly-identical filename, making multiple parts hard to tell apart at a glance in a file picker or folder view. The import side still recognizes both the new prefix format and the old suffix format, so parts already exported with an earlier version can still be uploaded.<\/li>\n<\/ul>\n\n<h4>2.5.0<\/h4>\n\n<ul>\n<li>Renamed the \"\uce74\ud14c\uace0\ub9ac\ubcc4 \uac00\uc838\uc624\uae30\"\/\"\ud398\uc774\uc9c0 \uac00\uc838\uc624\uae30\" section labels to \"\ud30c\uc77c \uac00\uc838\uc624\uae30\" - the whole screen is one unified \"upload a file, it sorts itself out\" action, and naming a sub-step after \"category\" made it read as a separate, category-specific action rather than part of that.<\/li>\n<li>Once a section is fully done, it no longer keeps showing the full instructional card (table headers, feature-help accordion, per-row status column) as if action were still needed - it collapses to a compact \"\u2705 \uc644\ub8cc\" summary with just a rollback link per item. If a file has both finished and unfinished categories, only the unfinished ones stay in the working table; finished ones move to the compact summary above it.<\/li>\n<\/ul>\n\n<h4>2.4.1<\/h4>\n\n<ul>\n<li>Category import no longer requires manually picking or typing a target category. Leaving both fields blank now auto-matches by exact name (uses an existing category with the same name, or creates one) - matches the actual intent of this whole screen being \"upload a file, it sorts itself out\" rather than a manual mapping step. Manual selection is still available for when you want a different target.<\/li>\n<\/ul>\n\n<h4>2.4.0<\/h4>\n\n<ul>\n<li>Redesigned the import screen from a fixed two-section layout (\"\ud398\uc774\uc9c0 \uac00\uc838\uc624\uae30\" + \"\uce74\ud14c\uace0\ub9ac\ubcc4 \uac00\uc838\uc624\uae30\", always both shown regardless of what the file actually contained) into a content-adaptive one. Uploading a file now shows a summary of what's actually inside it (categorized posts, uncategorized posts, pages, standalone media), and only the sections that apply to that file appear - no more an empty\/irrelevant \"\uce74\ud14c\uace0\ub9ac\ubcc4 \uac00\uc838\uc624\uae30\" table on a pages-only file.<\/li>\n<li>Fix (real gap, not previously reachable at all): posts with no category attached were invisible to the old catalog table (it only ever grouped posts that had at least one category term), so a <code>.wpmpx<\/code> built from \"\uae00 \uc120\ud0dd\" or any export containing uncategorized posts had no way to import them. They now get their own \"\ubbf8\ubd84\ub958 \uae00 \uac00\uc838\uc624\uae30\" section, importable as one flat batch like pages already were.<\/li>\n<li>Fix (also a real gap): files built with \"\ubbf8\ub514\uc5b4\ub9cc \ub0b4\ubcf4\ub0b4\uae30\" (posts\/pages both empty, media only) had no import path at all - there was no button anywhere that would actually bring those attachments in. Added a \"\ubbf8\ub514\uc5b4 \uac00\uc838\uc624\uae30\" section for this case specifically (only shown when the file has no posts or pages at all, since media referenced by posts\/pages is already picked up automatically while importing those).<\/li>\n<li>Internal: generalized the \"flat\" (non-category) import engine - what used to be page-specific job\/preview\/rollback\/record-keeping code is now shared by pages, uncategorized posts, and media-only imports, keyed by type.<\/li>\n<\/ul>\n\n<h4>2.3.0<\/h4>\n\n<ul>\n<li>Fix (real cause of the source site never becoming \"\uc815\uc2dd(full)\" automatically): this client defaulted the source site's requested role to <code>export_only<\/code>, requiring a manual \"\uc815\uc2dd \uc804\ud658\" click before Redirect Manager would ever unlock - but the product itself is sold as a pair (source + destination both become full on a single-tier purchase; see the matching marketplace fix). The default requested role is now <code>full<\/code> for both site roles - entering a valid key activates both sites automatically, no separate step. A source site that already has an <code>export_only<\/code> success recorded from an earlier version still needs one manual \"\uc815\uc2dd \uc804\ud658\" click to move off that recorded state, after which this no longer applies.<\/li>\n<li>Companion server-side fix required: <code>decoblocks-marketplace<\/code> 5.6.0 changes <code>SINGLE_MAX_ACTIVATIONS<\/code> from 1 to 2 to actually grant both slots on a single-tier purchase - this client-side default alone does nothing without that.<\/li>\n<\/ul>\n\n<h4>2.2.4<\/h4>\n\n<ul>\n<li>Fix (real, confirmed cause of every export download failing): the download handler ran the requested filename through <code>sanitize_file_name()<\/code> a second time. WordPress's own filename sanitizer inserts an underscore when it sees a multi-dot name with an unrecognized \"middle extension\" (exactly what <code>name.wpmpx.part1of2<\/code> looks like to it) as a defense against extension-spoofing attacks - so the requested filename silently became <code>name.wpmpx_.part1of2<\/code>, which never matched the real file. The actual security boundary here is the whitelist check against the export manifest (already present), not sanitization, so the redundant second pass was removed entirely.<\/li>\n<li>Clarified: the Redirect Manager lock screen and its diagnostic table now explicitly distinguish \"\ub77c\uc774\uc120\uc2a4 \ud0a4\uac00 \uc720\ud6a8\ud568\" (the key itself checks out) from \"\uc774 \uc0ac\uc774\ud2b8\uac00 \uc815\uc2dd(role=full)\" (this specific site has been granted the full tier) - these are different things, and a site can legitimately show \"\uc720\ud6a8\ud568\" while still being export_only, which isn't a bug.<\/li>\n<\/ul>\n\n<h4>2.2.3<\/h4>\n\n<ul>\n<li>The Redirect Manager lock screen now shows the raw license state stored on that specific site directly on the page: the key (masked), whether the server's last response said valid\/invalid, when it was last checked, and the exact failure reason the server returned. No more guessing whether a key is missing, a request failed, or something else - it's on screen.<\/li>\n<\/ul>\n\n<h4>2.2.2<\/h4>\n\n<ul>\n<li>Fix: clicking a real export download link (added in 2.2.1) could still fail. The download handler required a nonce (<code>check_admin_referer()<\/code>) - if it didn't match for any reason, WordPress shows its own generic \"\ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694\" page completely unrelated to this plugin, which just looked like \"clicking gives an error.\" Removed the nonce requirement for this action (capability check alone still protects it - it's a read-only download of the admin's own data), eliminating this failure mode outright. The three distinct failure cases inside <code>stream_part()<\/code> (no manifest, filename not recognized, file missing from disk) also no longer share one generic message - each now states exactly what it found, so a future failure is diagnosable from the screen alone.<\/li>\n<\/ul>\n\n<h4>2.2.1<\/h4>\n\n<ul>\n<li>Fix (real cause of \"\uc815\uc2dd \uc804\ud658\uc744 \ub20c\ub7ec\ub3c4 \uadf8\ub300\ub85c\ub2e4\"): clicking \"\uc815\uc2dd \uc804\ud658\" on the Redirect Manager screen redirected the result notice to the <em>Settings<\/em> screen instead - and the notice-display code was hard-restricted to only render on Settings besides. If you stayed on Redirect Manager (the natural expectation, since that's where you clicked), you'd never see the success or failure message at all - it silently rendered on a page you weren't looking at. Both the redirect target and the notice-display condition now point at Redirect Manager, so the result shows exactly where the click happened. Note: if the request itself still fails, the reason is now visible (\"\uc815\uc2dd \uc804\ud658\uc5d0 \uc2e4\ud328\ud588\uc2b5\ub2c8\ub2e4 - \ub0a8\uc740 \ud65c\uc131\ud654 \uc790\ub9ac\uac00 \uc5c6\uc744 \uc218 \uc788\uc2b5\ub2c8\ub2e4\") - on a single-site license already fully activated on another site (e.g. the destination site), this is the intended limit, not a bug; a multi-site license is required to activate a second site.<\/li>\n<li>Changed: removed the export auto-download entirely (iframe\/tab\/folder-picker attempts across 1.9.5-2.2.0 were all silently blocked by the browser under some condition or other, with no visible error - \"\uc644\ub8cc\ub410\ub2e4\ub294\ub370 \ud30c\uc77c\uc774 \uc5c6\ub2e4\" kept recurring). Completed exports now show plain, real download links you click directly - a genuine click can never be silently blocked by a browser, unlike any of the automated approaches tried so far.<\/li>\n<\/ul>\n\n<h4>2.2.0<\/h4>\n\n<ul>\n<li>Fix (real root cause of \"\ub0b4\ubcf4\ub0b4\uae30\ud588\ub294\ub370 \ud3f4\ub354\uc5d0 \ud30c\uc77c\uc774 \uc5c6\ub2e4\" and \"\ub9ac\ub514\ub809\ud2b8\uac00 \ub77c\uc774\uc120\uc2a4 \uc624\ub958\uc5d0 \uac78\ub9b0\ub2e4\"): this hosting's persistent object cache doesn't reliably reflect option values immediately after <code>update_option()<\/code> - the same class of bug already fixed once for <code>get_site_role()<\/code>. It turned out 13 other option reads across the plugin had the identical exposure, including the two most consequential ones: <code>WPMP_License::get_key()<\/code>\/<code>get_state()<\/code> (the single source of truth for every \"is this license valid\" check in the plugin, so a stale read here could make a freshly-verified license look invalid on the very next screen, e.g. Redirect Manager) and <code>WPMP_Exporter::get_manifest()<\/code> (read by the hidden download iframe moments after the export job finishes - a stale \"no export found\" read meant the download silently never happened, even though the UI showed a completion message). Rather than patching each call site individually, added one shared <code>wpmp_get_option_fresh()<\/code> helper (cache-bypassing <code>get_option()<\/code>) and moved every one of this plugin's own option reads through it.<\/li>\n<li>Simplified: removed the \"\uc804\uccb4 \ubbf8\ub514\uc5b4 \ubc31\uc5c5\" local-folder-picker feature (File System Access API) entirely - it only worked in Chromium browsers and had repeatedly caused confusing failures (blank tabs, popup blocking, the picker re-prompting with no visible progress). Both \"\uc120\ud0dd\ud55c \ud56d\ubaa9 \ub0b4\ubcf4\ub0b4\uae30\" and \"\uc804\uccb4 \ubbf8\ub514\uc5b4 \ubc31\uc5c5\" now behave identically: click, wait for the progress bar, and the finished file(s) download straight to the browser's normal downloads folder - no folder selection step for either.<\/li>\n<\/ul>\n\n<h4>2.1.1<\/h4>\n\n<ul>\n<li>Changed the licensing server domain from wpmediaporter.luxdigest.com to wpcm.luxdigest.com (<code>API_BASE<\/code> constant, Plugin URI). This is the address this plugin contacts to verify a license key - see \"External services\" above.<\/li>\n<\/ul>\n\n<h4>2.1.0<\/h4>\n\n<ul>\n<li>Renamed the plugin from \"WP Media Porter\" to \"LUX Content Migration\" - all user-facing text (plugin name, screen headings, notices) updated. Internal technical identifiers (class names, option keys, the .wpmpx file extension, the product slug used to talk to the licensing server) are unchanged, since those are invisible to users and renaming them would require also updating the separate licensing server in lockstep for no visible benefit.<\/li>\n<li>Fix: \"\uc774\ubbf8\uc9c0 \uc8fc\uc18c \uc7ac\uc801\uc6a9\" now behaves correctly per license tier instead of being a plain always-available button regardless of paid status. On a full license, image rehosting already happens automatically during import, so the button is no longer shown at all. On the free tier, the button is still shown (so people understand the feature exists) but clicking it no longer performs the action - it explains that image rehosting requires a full license and links to Settings, both in the UI and enforced again server-side in the AJAX handler.<\/li>\n<\/ul>\n\n<h4>2.0.1<\/h4>\n\n<ul>\n<li>Fix (likely the real cause of import failures): the upload handler used <code>wp_handle_upload()<\/code>, WordPress's helper for normal Media Library uploads. That function applies filename sanitization, extension checks, and upload-directory rules meant for regular media - none of which fit a proprietary format with no registered mime type (<code>.wpmpx<\/code>, <code>.wpmpx.part1of3<\/code>), so it could reject or mangle these files even with <code>test_type<\/code> disabled. Studying All-in-One WP Migration showed it deliberately does <em>not<\/em> use <code>wp_handle_upload()<\/code> for its own <code>.wpress<\/code> archives - it simply copies PHP's already-received temp file to its destination. Switched to the same approach: <code>is_uploaded_file()<\/code> verification, a sanitized filename, and <code>move_uploaded_file()<\/code> into the plugin's private, externally-blocked folder. Authenticity is still verified afterward by the parser's own magic-byte check.<\/li>\n<li>Fix: export opened a new browser tab to trigger the download, which then stayed open showing a blank page (downloads don't navigate, so nothing ever rendered there). Downloads are now triggered through a hidden iframe - no new tab appears at all, and nothing is left behind for you to close.<\/li>\n<\/ul>\n\n<h4>2.0.0<\/h4>\n\n<ul>\n<li>Rearchitected the .wpmpx file format (v3) after studying how established migration plugins actually do this. The previous design base64-encoded every attachment's bytes into JSON text - which inflates size ~33%, costs an encode\/decode pass on every file, and forces the JSON parser to handle enormous strings, all of which contributed to the memory failures seen throughout the 1.9.x series. The format is now <code>[magic][8-byte metadata length][gzipped text metadata][raw binary section]<\/code>: attachment bytes are appended verbatim with no encoding, and each item stores only an offset and size pointing into that section. Export copies file bytes 64KB at a time into the binary section during batch collection; import seeks directly to each offset and copies the exact byte range straight to disk. No base64 anywhere.<\/li>\n<li>Removed the two-step export flow entirely - the export screen no longer keeps a \"\uc644\ub8cc\ub41c \ud30c\uc77c\" panel with separate \ub2e4\uc6b4\ub85c\ub4dc buttons and a \uc870\uac01 \uc815\ub9ac button. Clicking export now runs the job and downloads the resulting file(s) directly, then returns to a clean screen.<\/li>\n<li>Removed the now-unnecessary whole-file fallback parser and legacy base64 code paths.<\/li>\n<li>Note: .wpmpx files produced by 1.9.x and earlier cannot be read by this version (the format changed). Re-export from the source site.<\/li>\n<\/ul>\n\n<h4>1.9.8<\/h4>\n\n<ul>\n<li>Hardening: <code>parse_wpmpx()<\/code>'s streaming path now also checks <code>defined('ZLIB_ENCODING_GZIP')<\/code> before using it, not just <code>function_exists('inflate_init')<\/code> - on PHP 8+, referencing an undefined constant is a fatal Error rather than a warning, so this closes a possible (if narrow) crash path on unusual PHP\/zlib builds by falling back to the whole-file method automatically instead.<\/li>\n<li>Verification: re-checked every file touched this session with three independent methods (a string\/comment-aware brace scanner, a class-body-statement scanner, and a Pygments PHP-tokenizer-based scanner) - no further syntax issues found. A real PHP interpreter was not available in this environment to compile-check directly; if the import still fails after this version, the on-screen error added in 1.9.4 (or the exact wording of whatever screen appears) is needed to diagnose further, since static analysis alone has been exhausted.<\/li>\n<\/ul>\n\n<h4>1.9.7<\/h4>\n\n<ul>\n<li>Fix: the auto-download added in 1.9.5 created a hidden <code>&lt;a download&gt;<\/code> element and clicked it several async steps (batch polling, finalize) after the original button click - browsers commonly treat that as no longer tied to a real user gesture and silently block it, so nothing visibly happened even though the code \"worked.\" Replaced with the standard reliable pattern: a blank tab is opened synchronously at the moment of the click (still within the trusted user-gesture window, so never blocked), and once the finished file's URL is known, that already-open tab's location is set to it - setting the location of an already-open window isn't subject to the same popup-blocking rules as opening a new one later.<\/li>\n<\/ul>\n\n<h4>1.9.6<\/h4>\n\n<ul>\n<li>Fix (the actual cause of every import - large or tiny - crashing with a blank \"\uc2ec\uac01\ud55c \uc624\ub958\" screen, unaffected by any of the 1.9.2-1.9.5 fixes): the 1.9.5 rewrite of <code>parse_wpmpx()<\/code> left a leftover fragment of the <em>previous<\/em> version's code sitting directly in the class body, outside any function - a bare <code>return<\/code> statement  &hellip;<\/li>\n<\/ul>","raw_excerpt":"Safely move a WordPress site&#039;s posts, pages, comments, authors, and custom fields to a new site \u2014 dry-run preview and rollback included.","jetpack_sharing_enabled":true,"_links":{"self":[{"href":"https:\/\/ibo.wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin\/360356","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/ibo.wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin"}],"about":[{"href":"https:\/\/ibo.wordpress.org\/plugins\/wp-json\/wp\/v2\/types\/plugin"}],"replies":[{"embeddable":true,"href":"https:\/\/ibo.wordpress.org\/plugins\/wp-json\/wp\/v2\/comments?post=360356"}],"author":[{"embeddable":true,"href":"https:\/\/ibo.wordpress.org\/plugins\/wp-json\/wporg\/v1\/users\/luxsnap"}],"wp:attachment":[{"href":"https:\/\/ibo.wordpress.org\/plugins\/wp-json\/wp\/v2\/media?parent=360356"}],"wp:term":[{"taxonomy":"plugin_section","embeddable":true,"href":"https:\/\/ibo.wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_section?post=360356"},{"taxonomy":"plugin_tags","embeddable":true,"href":"https:\/\/ibo.wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_tags?post=360356"},{"taxonomy":"plugin_category","embeddable":true,"href":"https:\/\/ibo.wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_category?post=360356"},{"taxonomy":"plugin_contributors","embeddable":true,"href":"https:\/\/ibo.wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_contributors?post=360356"},{"taxonomy":"plugin_business_model","embeddable":true,"href":"https:\/\/ibo.wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_business_model?post=360356"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}