LUX Content Migration

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.

FAQ

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.

Reviews

There are no reviews for this plugin.

Contributors & Developers

“LUX Content Migration” is open source software. The following people have contributed to this plugin.

Contributors

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 …