=== LUX Content Migration ===
Contributors: luxsnap
Tags: migration, import, export, redirect, media
Requires at least: 6.0
Tested up to: 7.1
Requires PHP: 7.4
Stable tag: 3.0.6
License: GPLv2 or later
License URI: https://www.gnu.org/licenses/gpl-2.0.html

Safely move a WordPress site's posts, pages, comments, authors, and custom fields to a new site — dry-run preview and rollback included.

== Description ==

LUX Content Migration is installed on **both** sides of a WordPress-to-WordPress migration:

* On the **old (source) site**, 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.
* On the **new (destination) site**, 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).

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.

= What this plugin does =

* Export from the source site (including automatic splitting into parts if needed) and import into the destination site, both unlimited.
* Comments (including reply/parent structure), author linking, and custom field / ACF relational ID reconnection.
* Dry-run preview — see exactly how many posts, comments, and images an import will involve before you run it, without creating anything.
* Rollback — 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).
* 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.

= Security notes =

* 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 `<!DOCTYPE>` declaration is stripped before parsing and external entity/network loading is disabled) to prevent XXE-style attacks from a malicious export file.
* File uploads are validated against the request's own upload metadata via `is_uploaded_file()` and moved through the WordPress Filesystem API into a non-listable, non-executable private folder that is cleared automatically after each file is processed.

== External services ==

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.

== Installation ==

1. Install and activate LUX Content Migration on **both** the new site and the old site.
2. 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).
3. On the source site: go to LUX Content Migration → Export, choose what to export, and download the resulting .wpmpx file (it may be split into numbered parts).
4. On the destination site: go to LUX Content Migration → Import, upload that .wpmpx file (select all parts together if it was split), and import it category by category.

== Frequently Asked Questions ==

= Do I need to install this on the old site too? =

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.

= Will my images move too? =

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.

= Does this plugin have any limits on the number of posts, pages, or sites? =

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.

== Screenshots ==

1. Role selection on first activation.
2. Category-by-category import screen with live progress.

== Changelog ==

= 3.0.6 =
* Fix: `fwrite()` return values are now checked when writing the split .wpmpx export parts in `WPMP_Exporter::finalize_job()`. 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.
* Hardening: `$_POST['source_category']` / `$_POST['category_name']` are now passed through `sanitize_text_field()` before use in `WPMP_Import_Admin` (category-start, rollback, and preview handlers), consistent with the rest of the plugin's input handling.
* Clarity: the .wpmpx metadata-length header is now packed/unpacked with the explicit 32-bit big-endian `N` format code (two halves) instead of the 64-bit `J`/`P` 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.

= 3.0.5 =
* **Critical fix: with the paid add-on's full license active, real image/video rehosting silently did nothing** - 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: `WPMP_Importer::process_attachment_batch()` passes its `$global` state (the URL-rewrite map and the old-attachment-ID → new-attachment-ID map) into `apply_filters( 'lux_content_migration_process_attachment_batch', ... )` - but PHP/WordPress break by-reference passing across `apply_filters()`, 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 *before* 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).

= 3.0.4 =
* **Fixed a real bug:** the "내보내기 준비" (Export) screen had a second "📦 전체 미디어 백업" button since the very first release, but this plugin's own `ajax_export_start()` AJAX handler unconditionally rejected any request that button sent (`embed_files: 1`), 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, `lux_content_migration_embed_files_available`) 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.
* Replaced that removed button with a single "이미지/영상까지 포함해서 완전히 담기" (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 (`lux_content_migration_render_embed_files_option` action, `lux_content_migration_set_embed_files_preference` 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 `lux_content_migration_pro_addon_installed`/`lux_content_migration_pro_settings_url` filters from 3.0.2).
* 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.
* `render_screen_switcher()` now also shows a "자동 리디렉트 추천" (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.

= 3.0.3 =
* **Screen reorganization (user feedback):** the "설정 / 라이선스" (Settings / License) screen defaulted to a "Pro 애드온" tab (generic promotional copy, or - since 3.0.2 - a link to the add-on's own license screen) shown before the "사이트 역할" (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 설정" menu item once installed. Removed the tab switcher entirely: this screen (renamed "역할 설정" / 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 애드온 알아보기" links elsewhere in this plugin point here).

= 3.0.2 =
* Fixed the "Pro 애드온" tab on this plugin's own "설정 / 라이선스" (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, `lux_content_migration_pro_addon_installed` and `lux_content_migration_pro_settings_url` (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.

= 3.0.0 =
* 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.
* 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 (`lux_content_migration_is_pro`, `lux_content_migration_process_attachment_batch`, `lux_content_migration_embed_attachment_file`, `lux_content_migration_export_embed_item`, `lux_content_migration_post_imported`, `lux_content_migration_retry_failed_attachment`, `lux_content_migration_render_admin_notices`, `lux_content_migration_role_saved`, `lux_content_migration_deactivate`, `lux_content_migration_reset_all`, `lux_content_migration_pro_addon_installed`, `lux_content_migration_pro_settings_url`, `lux_content_migration_embed_files_available`, `lux_content_migration_render_embed_files_option`, `lux_content_migration_set_embed_files_preference`) - the same extensibility pattern used by other WordPress.org-hosted freemium plugins for their paid add-ons.
* Removed bundled translation `.po`/`.mo` files and the `load_plugin_textdomain()` call - translations for WordPress.org-hosted plugins are handled automatically via translate.wordpress.org since WordPress 4.6.
* 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-`sanitize_file_name()` bug, already fixed separately).

= 3.0.1 =
* Fixed a fatal error on "reset everything": `WPMP_Store::reset_all()` still called `WPMP_Importer::disable_background_mode()`, a method that had already been deleted from `WPMP_Importer` when background processing moved to the Pro add-on. Replaced with a `lux_content_migration_reset_all` 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.
* Fixed the export manifest always reporting `embed_files` as false: the value returned by the `lux_content_migration_export_embed_item` filter was computed per-batch but never written back onto the persisted export job, so `finalize_job()` could never see it. A Pro add-on embedding attachment bytes into the export file is now correctly reflected in the finished manifest.
* 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.
* Rewrote `uninstall.php`: it still dropped the `wp_redirects`/`wp_redirect_404_log` 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.

= 2.8.2 =
* Fixed two leftover code comments still referencing the old main file name (`wp-content-migration.php`) instead of the current one (`lux-content-migration.php`) - harmless (comments aren't executed), but corrected for accuracy.

= 2.8.1 =
* Fix (WordPress.org Plugin Check automated scan failures): replaced `move_uploaded_file()` (a forbidden direct filesystem function) with the WP_Filesystem API's `move()` method for handling uploaded .wpmpx files - the `is_uploaded_file()` 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).

= 2.7.3 =
* Fix: `Contributors` in readme.txt was set to `luxdigest` (the display name) instead of the actual WordPress.org login username (`luxsnap`) - required to match the account submitting this plugin for review.

= 2.7.2 =
* Updated "Tested up to" from 6.6 to 7.0 (current WordPress release) ahead of WordPress.org submission.

= 2.7.1 =
* Renamed the "팀" pricing tier to "에이전시(멀티사이트)" throughout, and corrected the license description that still said "슬롯 1개" (one slot) for a single license - it's actually 2 (source + target, a pair), matching the fix made earlier this cycle.

= 2.7.0 =
* Added a complete English translation (`languages/wp-content-migration-en_US.po`/`.mo`) 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 `load_plugin_textdomain()` based on the site's locale. Verified the compiled `.mo` file parses correctly with Python's own `gettext` module before shipping.

= 2.6.8 =
* Fix (WordPress.org submission blocker): the text domain was still `wp-media-porter` throughout the codebase (plugin header, `load_plugin_textdomain()`, and all 477 translation function calls) even though the plugin was renamed to `wp-content-migration` 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.
* Removed the bundled `wp-media-porter-*.mo/.po/.pot` 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 `languages/wp-content-migration.pot` 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 `wp i18n make-pot`). Translated `.po`/`.mo` files for specific languages (en_US, ja, etc.) are not included - see the note below.

= 2.6.7 =
* Reworded the "전체 미디어 백업" 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 "자동 리디렉트 추천"/"백그라운드 무인 처리" to "리디렉트 관리"/"백그라운드 프로세스" in this caption for a punchier, more marketing-appropriate phrasing (the fuller technical description on the Settings/License screen is unchanged).

= 2.6.6 =
* Reworded the caption under "전체 미디어 백업" to stop repeating the "미디어 주소 자동 적용" line already covered by the paragraph above it, and instead briefly point out the two other paid-only features this plugin has (자동 리디렉트 추천, 백그라운드 무인 처리) that aren't specific to this button but are worth knowing about while looking at a paid-tier option.

= 2.6.5 =
* Fix: "선택한 항목 내보내기" is usable on both free and paid tiers (unlike "전체 미디어 백업", which is paid-only) - the caption under it now shows both outcomes ("무료: 적용 불가" / "유료: 자동 적용, 원본 사이트가 가져오기 끝날 때까지 켜져 있어야 함") instead of a single line that only described one of them.

= 2.6.4 =
* Simplified the per-button captions under "선택한 항목 내보내기"/"전체 미디어 백업" to one line each ("새로운 미디어 주소 적용 불가" / "자동 적용") - the fuller explanation was already given in the paragraph just above, so repeating it under each button read as cluttered. Added a "🛒 정식 라이선스 구매하기" button directly under the locked PRO option so there's an immediate path to buy without needing to go to Settings first.

= 2.6.3 =
* Reworded the free-tier export copy ("이미지/영상은 주소만 포함됩니다") - 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 "전체 미디어 백업" copy is now explicit about the actual contrast: it automatically re-addresses images/video to the new site.

= 2.6.2 =
* 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 "🛒 정식 라이선스 구매하기" 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.

= 2.6.1 =
* Import screen now shows a progress bar (matching the export screen) instead of only raw counts, and fixes a real bug where the "처리 중입니다..." 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 "완료되었습니다" and the bar fills to 100% the moment the job's last poll reports done, instead of only silently revealing the finalize button.

= 2.6.0 =
* 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): `collect_needed_attachments()` 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 `src=`/`srcset=`/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.
* Changed `rewrite_urls()` 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.

= 2.5.2 =
* Fix (real, likely the main cause of "이미지가 재정의가 안 된다"): `rewrite_urls()` only replaced post content URLs that matched an attachment's original URL *exactly*. But WordPress normally inserts a resized variant into post content when you add an image via the editor (e.g. `photo-300x200.jpg` 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 `filename-WxHeight.ext` 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.

= 2.5.1 =
* Moved the split-part indicator from the end of the filename to the front: `NEWS-LUX-...-074327.wpmpx.part1of2` becomes `part1of2-NEWS-LUX-...-074327.wpmpx`. 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.

= 2.5.0 =
* Renamed the "카테고리별 가져오기"/"페이지 가져오기" section labels to "파일 가져오기" - 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.
* 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 "✅ 완료" 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.

= 2.4.1 =
* 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.

= 2.4.0 =
* Redesigned the import screen from a fixed two-section layout ("페이지 가져오기" + "카테고리별 가져오기", 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 "카테고리별 가져오기" table on a pages-only file.
* 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 `.wpmpx` built from "글 선택" or any export containing uncategorized posts had no way to import them. They now get their own "미분류 글 가져오기" section, importable as one flat batch like pages already were.
* Fix (also a real gap): files built with "미디어만 내보내기" (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 "미디어 가져오기" 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).
* 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.

= 2.3.0 =
* Fix (real cause of the source site never becoming "정식(full)" automatically): this client defaulted the source site's requested role to `export_only`, requiring a manual "정식 전환" 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 `full` for both site roles - entering a valid key activates both sites automatically, no separate step. A source site that already has an `export_only` success recorded from an earlier version still needs one manual "정식 전환" click to move off that recorded state, after which this no longer applies.
* Companion server-side fix required: `decoblocks-marketplace` 5.6.0 changes `SINGLE_MAX_ACTIVATIONS` from 1 to 2 to actually grant both slots on a single-tier purchase - this client-side default alone does nothing without that.

= 2.2.4 =
* Fix (real, confirmed cause of every export download failing): the download handler ran the requested filename through `sanitize_file_name()` 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 `name.wpmpx.part1of2` looks like to it) as a defense against extension-spoofing attacks - so the requested filename silently became `name.wpmpx_.part1of2`, 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.
* Clarified: the Redirect Manager lock screen and its diagnostic table now explicitly distinguish "라이선스 키가 유효함" (the key itself checks out) from "이 사이트가 정식(role=full)" (this specific site has been granted the full tier) - these are different things, and a site can legitimately show "유효함" while still being export_only, which isn't a bug.

= 2.2.3 =
* 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.

= 2.2.2 =
* Fix: clicking a real export download link (added in 2.2.1) could still fail. The download handler required a nonce (`check_admin_referer()`) - if it didn't match for any reason, WordPress shows its own generic "다시 시도해주세요" 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 `stream_part()` (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.

= 2.2.1 =
* Fix (real cause of "정식 전환을 눌러도 그대로다"): clicking "정식 전환" on the Redirect Manager screen redirected the result notice to the *Settings* 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 ("정식 전환에 실패했습니다 - 남은 활성화 자리가 없을 수 있습니다") - 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.
* 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 - "완료됐다는데 파일이 없다" 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.

= 2.2.0 =
* Fix (real root cause of "내보내기했는데 폴더에 파일이 없다" and "리디렉트가 라이선스 오류에 걸린다"): this hosting's persistent object cache doesn't reliably reflect option values immediately after `update_option()` - the same class of bug already fixed once for `get_site_role()`. It turned out 13 other option reads across the plugin had the identical exposure, including the two most consequential ones: `WPMP_License::get_key()`/`get_state()` (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 `WPMP_Exporter::get_manifest()` (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 `wpmp_get_option_fresh()` helper (cache-bypassing `get_option()`) and moved every one of this plugin's own option reads through it.
* Simplified: removed the "전체 미디어 백업" 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 "선택한 항목 내보내기" and "전체 미디어 백업" 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.

= 2.1.1 =
* Changed the licensing server domain from wpmediaporter.luxdigest.com to wpcm.luxdigest.com (`API_BASE` constant, Plugin URI). This is the address this plugin contacts to verify a license key - see "External services" above.

= 2.1.0 =
* 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.
* Fix: "이미지 주소 재적용" 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.

= 2.0.1 =
* Fix (likely the real cause of import failures): the upload handler used `wp_handle_upload()`, 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 (`.wpmpx`, `.wpmpx.part1of3`), so it could reject or mangle these files even with `test_type` disabled. Studying All-in-One WP Migration showed it deliberately does *not* use `wp_handle_upload()` for its own `.wpress` archives - it simply copies PHP's already-received temp file to its destination. Switched to the same approach: `is_uploaded_file()` verification, a sanitized filename, and `move_uploaded_file()` into the plugin's private, externally-blocked folder. Authenticity is still verified afterward by the parser's own magic-byte check.
* 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.

= 2.0.0 =
* 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 `[magic][8-byte metadata length][gzipped text metadata][raw binary section]`: 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.
* Removed the two-step export flow entirely - the export screen no longer keeps a "완료된 파일" panel with separate 다운로드 buttons and a 조각 정리 button. Clicking export now runs the job and downloads the resulting file(s) directly, then returns to a clean screen.
* Removed the now-unnecessary whole-file fallback parser and legacy base64 code paths.
* 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.

= 1.9.8 =
* Hardening: `parse_wpmpx()`'s streaming path now also checks `defined('ZLIB_ENCODING_GZIP')` before using it, not just `function_exists('inflate_init')` - 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.
* 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.

= 1.9.7 =
* Fix: the auto-download added in 1.9.5 created a hidden `<a download>` 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.

= 1.9.6 =
* Fix (the actual cause of every import - large or tiny - crashing with a blank "심각한 오류" screen, unaffected by any of the 1.9.2-1.9.5 fixes): the 1.9.5 rewrite of `parse_wpmpx()` left a leftover fragment of the *previous* version's code sitting directly in the class body, outside any function - a bare `return` statement outside a method is a PHP parse error, which fails to even compile the file. This happens at file-load time, before any code (including try/catch or the on-screen error banner added in 1.9.2) has a chance to run, and affects literally every request that touches this class regardless of file size - which is exactly why it looked identical for both small and large exports, and why none of the "diagnostic" additions ever showed anything. Removed the stray code. Also re-audited every other file touched this session with a script that specifically checks for statements sitting in a class body outside any method (a brace/paren counter alone cannot catch this class of bug, since both counts can still balance) - no other instances found.
* Changed decompression from `stream_filter_append('zlib.inflate', ...)` to PHP's explicit `inflate_init(ZLIB_ENCODING_GZIP)` / `inflate_add()` - the stream filter's gzip-header auto-detection behavior wasn't something I had actually verified, whereas the explicit ZLIB_ENCODING_GZIP constant removes that ambiguity entirely. Still fully streaming (64KB chunks).

= 1.9.5 =
* Fix (true root cause of the memory crash on import, not fully fixed in 1.9.4): `parse_wpmpx()` still read the entire uploaded file into memory and decompressed it all at once before the per-item processing loop that 1.9.4 fixed ever ran - for a full-media-backup file, this alone could exceed typical hosting memory limits before reaching the code that was supposed to help. PHP cannot report memory-exhaustion fatals to try/catch, which is why the error banner added in 1.9.2 never appeared. Rewrote to stream-decompress via `zlib.inflate` and process the file in 64KB chunks end to end - peak memory is now roughly constant regardless of file size.
* New: "글 선택" mode, alongside the existing "페이지 선택" - a searchable checklist to export specific individual posts rather than a whole category. Its referenced images/video are found and included automatically, same as the other modes.
* Improved: "선택한 항목 내보내기" and "전체 미디어 백업" no longer leave you to find and click a separate "다운로드" button after the job finishes - completed parts now download automatically.

= 1.9.4 =
* Fix: `WPMP_Media::process_attachment()` was introduced in 1.7.0 checking for a raw base64 `embedded_data` key on full-backup items. When 1.9.x's memory-safety fix (writing embedded files to disk immediately during parsing, see below) started replacing that key with `embedded_file_path`, this check was never updated to match - meaning every embedded image/video silently fell through to the normal remote-download path instead of using the already-decoded local copy. Fixed to check `embedded_file_path` first.
* Fix (real memory cause of "가져오기하면 이 웹사이트에 심각한 오류가 발생했습니다"): `WPMP_Parser::parse_wpmpx()` read the entire uploaded file into memory, decompressed the whole thing, and kept every item - including full base64-encoded image/video bytes for full-backup exports - in one PHP array that then got serialized whole into a single `wp_options` row via `update_option()`. For a media-heavy full backup, the same data existed several times over in memory simultaneously (raw file + decompressed string + parsed array + serialized option string), which could exceed typical hosting's PHP memory limit and crash with no error message. Embedded file data is now decoded and written straight to disk as each line is parsed, and only a short file path is kept in the array/option - the size stored in the database is now close to link-only mode regardless of backup mode.
* New: file uploads and category-import batches now catch PHP errors that are catchable (most logic bugs, though not memory-exhaustion fatals, which PHP itself cannot always report even to try/catch) and show the exact error message and location directly on screen, logged to `wp-content/uploads/wpmp-imports/error.log` - no need to edit `wp-config.php` or check hosting-panel error logs for these cases.

= 1.9.3 =
* Fix (the real cause of "전체 미디어 백업" failing with "이 라이선스 키로는 활성화할 수 없습니다" for legitimately licensed users): the feature was requesting a 'full' activation slot on the source site before running, exactly like turning on Redirect Manager does. But unlike Redirect Manager, this is a one-time export action, not an ongoing service - so requiring its own slot meant that on a single-site license (1 slot total, already used by the destination site), it could never succeed on the source site at all, regardless of how valid the license was. It no longer requests a slot - a license key that's already confirmed valid (export-only or full) is accepted as sufficient proof, since the destination site being activated already establishes the license is real.

= 1.9.2 =
* Diagnostic safety net: repeated reports of "버튼을 눌러도 아무 일도 안 일어난다" could not be reproduced from code review alone, which points to a silently-thrown JS error somewhere (a thrown error inside a jQuery click handler, or the `WPMP` config object failing to load, would both cause every button to appear completely dead with zero visible feedback). This screen now catches any JS error or unhandled promise rejection and shows it as an impossible-to-miss red banner at the top of the page, and explicitly checks on load whether the `WPMP` config object exists at all. If the same "nothing happens" symptom recurs, the banner text itself will show the real cause instead of requiring more guesswork.

= 1.9.1 =
* Fix: "전체 미디어 백업" clicked the folder-picker and the actual start request as one combined action, so if anything after the folder dialog failed (network hiccup, server error), the screen quietly reset and re-clicking just re-opened the OS folder picker with no visible explanation - it looked like an infinite "폴더 선택만 반복" loop. Folder selection is now a separate, explicit "📁 저장 폴더 선택" button whose result is remembered; the "시작" button never re-prompts for a folder and now surfaces the actual HTTP status/error text when a request fails instead of failing silently.
* Clarified: the "용량만 미리 확인해보기" button was easy to mistake for the actual start button - relabeled and given an explicit "실행 아님, 위쪽의 시작 버튼을 누르세요" note.

= 1.9.0 =
* Redesigned content selection: replaced the "글/페이지/미디어 checkboxes + separate category filter" combination (which required manually re-selecting media alongside categories, and silently broke image rehosting for any content if the media checkbox was unchecked) with four explicit modes - 전체 내보내기 (everything), 카테고리별 내보내기 (posts in chosen categories - referenced images/video are found and included automatically, no separate media step), 페이지 선택 (an actual page picker - individually chosen pages, not just "all pages"; their images/video are included automatically too), and 미디어만 내보내기 (media library only, filterable by date range and file type - image/video/audio/other).
* Fix: category/page-scoped exports now only include the specific attachments actually referenced in the selected content's HTML (via content scan + featured-image check at export time) - not the entire media library. "전체" mode still includes the whole library (nothing to filter).

= 1.8.0 =
* Renamed "완전 포함형" to "전체 미디어 백업" throughout the UI - clearer name for what it actually does.
* Fixed: the previous version gated this feature entirely behind Settings, and behind a strict is_full() check that a source site almost never satisfies on its own (it normally connects as the free export-only role until redirects are turned on) - so the option was effectively invisible/unreachable from the export screen for most licensed users. It's now a dedicated, equal-size second button right next to "선택한 항목 내보내기" (no need to leave the screen), available to anyone with a license key entered; clicking it now automatically requests the same free→full upgrade that turning on Redirect Manager does, since it's a paid feature.
* New: clear side-by-side guidance - "선택한 항목 내보내기" is described as the normal choice for migrating right away, while "전체 미디어 백업" is explained for when the move isn't immediate (or the source site will be retired soon), including that large media libraries will take longer to download because of the resulting part count.
* New (Chromium browsers - Chrome/Edge): clicking "전체 미디어 백업" prompts for a local folder via the File System Access API, then writes each completed part directly into it as they finish - no more clicking a separate download button per part. Falls back to the existing per-part download list on browsers without that API (Firefox/Safari) or if the folder prompt is cancelled.
* New: an on-demand "예상 용량 미리 확인" button in the help panel re-exposes the disk-space estimate (this had been tied to a toggle control removed in this rename/restructure).

= 1.7.0 =
* New (full license only): "완전 포함형으로 내보내기" - reads each attachment's file directly from this site's own disk (not a re-download, since export runs on the source site itself) and embeds it as base64 inside the .wpmpx file. Once downloaded, the source site can be deleted entirely and images/video on the destination site remain intact - solves the structural risk that the default (link-only) export requires the source site to stay online until the destination site finishes importing.
* New: a pre-flight disk-space check for the full-embed option. Hosting capacity varies per site (500MB/1GB/2GB/etc., never assumed) - `disk_free_space()` is queried live against the actual media library size (with a 2x safety margin for base64 + temporary compression overhead) before the job is allowed to start, so it fails fast with a clear message instead of filling the disk mid-export.
* New: explicit, always-visible warnings on the export screen for all three cases - free tier (images are link-only; don't remove the source site before the destination finishes importing), paid tier default (same requirement - source must stay alive until import completes), and full-embed mode (file size will grow substantially; requires an explicit checkbox acknowledgment before it can run).
* New: individual files that exceed the 100MB full-embed cap fall back to link-only for that file and are listed by name in the completion summary, rather than silently being dropped or crashing the batch.

= 1.6.0 =
* New: Export Prep now offers real selection - checkboxes for content type (posts/pages/media, each showing its actual count) and a category multi-select (post count shown per category) - instead of always exporting everything with no options. Selections are passed straight into the batch export job, so the size/progress numbers reflect exactly what was picked.
* New: the completed export now shows the total item count it actually processed, so you can cross-check it against the real post/page/media counts in wp-admin if a file size looks off - a small .wpmpx is normal and expected on its own, since it only contains text/metadata/image URLs (not the image files themselves, which are fetched during import), but this makes it possible to verify nothing was silently skipped rather than just guessing from file size.

= 1.5.3 =
* Fix (the actual "site role keeps resetting / keeps asking again" bug, not the menu-duplication one addressed in 1.5.2): `get_site_role()` now force-clears the option's object cache entry before every read, so a persistent object cache (Redis/Memcached, host-level caching) that fails to invalidate after `update_option()` can no longer serve a stale/empty value. `handle_save_role()` also now verifies, within the same request, that the value it just wrote actually reads back correctly - if not, it reports a save failure explicitly instead of silently redirecting as if it succeeded.
* New: the hidden diagnostics page (Settings → "진단 정보 보기") now shows the site role option's raw database value side by side with what `get_option()` returns after a forced cache-bypass - if those two ever differ, that's direct proof of a caching issue on that specific site, not a plugin logic bug.

= 1.5.2 =
* Changed: replaced the "register the first submenu on the same slug as its parent" trick (used since 1.1.1 to stop WordPress auto-duplicating a "LUX Content Migration" link at the top of the submenu) with the standard, documented approach - every submenu now gets its own distinct slug, and the auto-generated duplicate is removed explicitly via `remove_submenu_page()`. The old trick relied on how WordPress internally computes hook names for a submenu that shares its parent's slug, which isn't something the trick's correctness should have depended on to begin with - this version doesn't depend on that at all anymore.
* New: a hidden diagnostics page (Settings → "진단 정보 보기" link at the bottom) showing the site's saved role, the actual registered submenu slugs, and which hook the plugin's own scripts/styles are attached to - for troubleshooting menu-related reports without guesswork.

= 1.5.1 =
* Fix: the import screen's in-page section rail still labeled the upload step "XML 업로드" even though the actual card next to it is titled "LUX Content Migration 파일 업로드" and .wpmpx has been the primary format since 1.3.0 - relabeled to match.
* Simplified: Export Prep no longer bundles license-status and "switch to redirects" cards on the same page - it's now just the export action, nothing else (license status lives in Settings; Redirects is one click away via the top screen-switcher, which was already there). The export button is now the first thing on the page instead of being preceded by several paragraphs of explanation.
* Simplified: Settings/License is now two real tabs (라이선스 / 사이트 역할) that show one at a time, instead of a single scrolling page with both fully expanded plus a redundant "table of contents" rail pointing at sections that were already fully visible.
* Simplified: the site-role picker inside Settings now shows "현재 역할: OOO" by default with a "역할 바꾸기" disclosure instead of always displaying the full pick-one-of-two form - addressing that the same choice screen kept reappearing prominently even after it had already been set once.

= 1.5.0 =
* Rebuilt split-export generation (1.4.0's version) to be safe on any size site regardless of hosting resource limits: it previously gathered every post into one PHP array and gzip-compressed the whole thing in a single request, which could exceed memory_limit/max_execution_time on large sites. Now runs as small batches (25 posts per AJAX call, same pattern as the import side's background processing) that append to a plain temp file, then a separate finalize step gzip-compresses and splits that file in 64KB streamed chunks - memory use for the whole operation stays constant no matter how large the site is.
* New: a progress screen with a live progress bar, item count, and an explicit "이 창을 닫거나 다른 작업을 하지 마세요" notice while the export batches run, plus a rate-based estimated time remaining, addressing that a silent multi-minute wait reads as broken and invites people to disable/uninstall the plugin.
* Internal: .wpmpx's payload format changed from one big JSON array to JSON-Lines (one item per line) to support this streaming pipeline; bumped the file's internal format marker (WPMPX2). Existing WPMPX1 files exported by 1.3.0/1.4.0 are not affected by any live migration in this version - re-export with 1.5.0 if you still have one to import.

= 1.4.0 =
* New (core feature, was previously missing entirely): split export. Hosting servers commonly cap a single upload at 2-20MB, so a single .wpmpx file could be too large to re-upload on the destination site. "내보내기 준비" now splits the export into numbered parts (`site.wpmpx.part1of3`, `.part2of3`, ...) at a configurable size (default 19MB, adjustable 1-500MB behind an "고급" toggle for advanced users who've raised their server's upload limit). Each part gets its own download button. Small sites that fit under the chunk size still get a single plain file, unchanged from before.
* New: the "가져오기" upload field now accepts multiple files at once (`multiple`) - select every part together and the plugin reassembles them in order (by parsing the `.partNofM` suffix, validating no part is missing/duplicated/mismatched) before parsing, exactly as if it were the original single file. Legacy single .xml/.wpmpx uploads still work unchanged.
* Noted limitation (documented in-screen, not fully solved yet): selecting many parts at once still sends them as one combined upload request, so an unusually low `post_max_size` or `max_file_uploads` server setting can still block it even though each individual part is under the size limit - the export screen now explains this and suggests raising `post_max_size` or choosing a larger chunk size instead.

= 1.3.0 =
* Major: replaced the standard WordPress WXR (.xml) export with a proprietary LUX Content Migration file format (.wpmpx - magic-byte header + gzip-compressed JSON). Files exported by this plugin can no longer be opened by WordPress's native Importer or any other migration tool - only by another site running LUX Content Migration. The export screen's "지금 전체 내보내기" button now builds this file directly (querying the database itself) instead of delegating to wp-admin's generic Tools → Export screen. Uploading still accepts legacy standard .xml files for compatibility with files obtained from other sources, but everything this plugin itself produces is now exclusive to it.
* Fix: `WPMP_Media::build_safe_filename()` silently renamed any attachment with an unrecognized extension (including every video format - mp4, mov, webm, etc. - and several image formats) to `.jpg`, corrupting the file. Now checks the site's actually-allowed extensions (`get_allowed_mime_types()`) and, when the URL's extension is missing or unrecognized, inspects the downloaded file's real content via `finfo` to detect its true type (images, video, audio, PDF) instead of guessing from the URL alone or defaulting to `.jpg`.

= 1.2.0 =
* Redesigned: cross-page navigation (Import/Export Prep/Redirects/Settings) and in-page section navigation were mixed together in the same left rail, so the rail's item count and meaning changed inconsistently from screen to screen. Split into two clearly separate things: a top pill-style screen switcher for moving between whole screens, and the left rail exclusively for jumping to sections within the current screen.
* New: "내보내기 준비" no longer just links out to WordPress's generic Tools → Export screen and leaves you there. A new "지금 전체 내보내기" button triggers the export and downloads the .xml file directly from this plugin's own screen (still using WordPress's own well-tested export engine under the hood - just no more hand-off to a separate generic-looking screen). The native Tools → Export screen is now only mentioned as an optional link for partial/filtered exports.

= 1.1.3 =
* Fix (the real cause of "Import/Export screen has no styling and the feature rail is invisible"): 1.1.1's fix for the dead first menu item registered "가져오기"/"내보내기 준비" on the same slug as the parent menu to stop WordPress from auto-duplicating it - correct - but the CSS/JS enqueue check still guessed the resulting hook name as `toplevel_page_wp-media-porter`. That guess was wrong: when a submenu shares its parent's slug, WordPress actually names the hook using the parent's registered short name (`wp-media-porter_page_wp-media-porter` in our case), not the literal word "toplevel" - that's reserved for the top-level page's own untouched hook. The enqueue check never matched, so the Import and Export Prep screens silently loaded with zero CSS/JS (no card styling, no feature rail, plain unstyled HTML). Fixed by capturing the real hook name `add_submenu_page()` returns and matching against that directly instead of guessing the string.
* Fix: `wanted_role()` no longer gets permanently stuck requesting the paid "full" role after any prior full-role request. Previously, saving the site role only cleared the stuck preference if you resubmitted the role form; now the check runs automatically on every license verification (including the daily cron and the "지금 다시 확인" button) - no action needed beyond clicking "지금 다시 확인" once.

= 1.1.2 =
* Fixed a real bug behind "license won't verify on the export/source site even though the key is correct": once a site ever requested the paid "full" role (e.g. clicking "정식 전환" on the Redirect screen), that preference was stored permanently and kept being reused on every future check - even after switching the site role back to "이전 원본(source)", which should request the free "export_only" role instead. Saving the site role now clears that stored preference and re-checks the license immediately, so it correctly requests "export_only" again as a source site.
* Added a "지금 서버에 요청 중인 역할" (full vs. export_only) line to Settings → License so this kind of role/request mismatch is visible at a glance instead of only showing a generic "확인되지 않음".
* Improved: the first-run role picker now spells out the actual step-by-step procedure for each role (what to click, in what order) and explicitly warns that on the free tier, images are not rehosted - if the old site's post is deleted or its URL changes, the images will break on the new site (and, for the source role, that redirects also require a full license).

= 1.1.1 =
* Fix: the first submenu item under "LUX Content Migration" (auto-duplicated by WordPress core because our submenu slugs never matched the parent slug) did nothing when clicked - it called wp_safe_redirect() from inside a page callback, which silently fails because admin headers are already sent by that point. Import/Export Prep is now registered directly on the parent's own slug, so it's the actual screen with no dead redirect step.
* Fix: because of the slug mismatch above, the admin CSS/JS enqueue hook check (`wp-media-porter_page_wp-media-porter-import`) never matched the real hook WordPress generates when a submenu shares its parent's slug (`toplevel_page_wp-media-porter`) - Import and Export Prep were silently loading with zero plugin styling/scripts. Fixed the hook match.
* Improved: full visual redesign of every admin screen (Import, Export Prep, Redirect Manager, Settings) - layered card shadows, a persistent in-page "this screen's features" rail navigation (since WordPress's own left sidebar only supports two menu levels), wider spacious layout, refined typography/badges/buttons.

= 1.1.0 =
* Fixed: the source ("old site") role had no dedicated screen at all, even though first-run role selection promised a free "export prep" screen. Added LUX Content Migration → Export Prep, which walks through WordPress's built-in Tools → Export, shows this site's export-only/full license connection status, and links to Redirect Manager for when you're ready to switch to a full license.
* Developer/author metadata corrected to LUX Digest.

= 1.0.0 =
* Initial release: safe WXR import with media re-hosting, redirect manager with 404 log, and shared license/activation system between source and destination sites.
* Added, all free unless noted: dry-run preview, rollback/undo, comment migration, author matching, custom field/ACF relational ID reconnection, automatic redirect suggestion CSV export/import (full license), background/unattended WP-Cron processing (full license).

== Upgrade Notice ==

= 2.4.0 =
Import screen redesigned to adapt to what a file actually contains - fixes two real gaps where content simply couldn't be imported at all before: posts with no category, and files built from "미디어만 내보내기" with no posts/pages.

= 2.3.0 =
Source sites now request "정식(full)" 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 "정식 전환" click to move past that.

= 2.2.2 =
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.

= 2.2.1 =
Fixes "정식 전환" 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.

= 2.2.0 =
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's normal downloads folder.

= 2.1.1 =
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.

= 2.1.0 =
Renamed to LUX Content Migration. "이미지 주소 재적용" 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.

= 2.0.1 =
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.

= 2.0.0 =
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.

= 1.9.7 =
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't subject to that blocking.

= 1.9.6 =
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 "심각한 오류" screen.

= 1.9.5 =
Fixes the real memory-crash cause on import (true streaming decompression, not just post-processing), adds an individual "글 선택" post picker, and makes exports auto-download instead of requiring a second click.

= 1.9.4 =
Fixes the real cause of the blank "심각한 오류" 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.

= 1.9.3 =
Fixes "전체 미디어 백업" failing with a license-slot error on legitimately licensed single-site accounts - it no longer tries to consume a second 'full' activation slot for what is a one-time export action.

= 1.9.2 =
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.

= 1.9.1 =
Fixes "전체 미디어 백업" 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.

= 1.9.0 =
Export selection redesigned into four clear modes (전체/카테고리별/페이지 선택/미디어만) - 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.

= 1.8.0 =
"완전 포함형" renamed to "전체 미디어 백업" 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.

= 1.7.0 =
New full-license "완전 포함형" 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.

= 1.6.0 =
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.

= 1.5.3 =
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.

= 1.5.2 =
Menu registration switched from a same-slug trick to WordPress's standard documented method (each submenu has its own slug now), plus a hidden diagnostics page for troubleshooting menu issues without guesswork.

= 1.5.1 =
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 "current role + change" instead of always showing the full form, and a stray "XML 업로드" label corrected to match the actual .wpmpx format.

= 1.5.0 =
Export generation is now fully batch/streaming-based (safe for any site size, won't hit memory/time limits regardless of hosting), with a visible progress bar and "don't navigate away" notice while it runs. .wpmpx format marker bumped to v2 - re-export any file you haven't imported yet.

= 1.4.0 =
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.

= 1.3.0 =
Exports are now this plugin'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.

= 1.2.0 =
Navigation redesign (page-switcher vs. in-page section rail no longer mixed) and one-click export directly from the plugin's own screen instead of handing off to WordPress's generic Tools → Export screen.

= 1.1.3 =
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.

= 1.1.2 =
Fixes source-site license verification getting permanently stuck requesting the paid "full" role instead of the free "export_only" role after any prior full-role request - update if your export/source site's license shows "확인되지 않음" despite a correct key.

= 1.1.1 =
Fixes a dead first menu item and missing CSS/JS on the Import/Export screens (both introduced in 1.1.0's menu restructuring) - update if clicking the top menu item did nothing, or the screens looked unstyled.

= 1.1.0 =
Source-site "Export Prep" screen added (was missing since 1.0.0) - update if the old site's menu only showed Redirects with nothing about exporting.

= 1.0.0 =
Initial release.
