Title: Migravo WXR Media Migration
Author: QuiNguyen
Published: <strong>August 11, 2026</strong>
Last modified: August 12, 2026

---

Search plugins

![](https://ps.w.org/migravo-wxr-media-migration/assets/banner-772x250.png?rev=3641959)

![](https://ps.w.org/migravo-wxr-media-migration/assets/icon-256x256.png?rev=3641959)

# Migravo WXR Media Migration

 By [QuiNguyen](https://profiles.wordpress.org/dinhqui/)

[Download](https://downloads.wordpress.org/plugin/migravo-wxr-media-migration.1.2.0.zip)

 * [Details](https://wordpress.org/plugins/migravo-wxr-media-migration/#description)
 * [Reviews](https://wordpress.org/plugins/migravo-wxr-media-migration/#reviews)
 *  [Installation](https://wordpress.org/plugins/migravo-wxr-media-migration/#installation)
 * [Development](https://wordpress.org/plugins/migravo-wxr-media-migration/#developers)

 [Support](https://wordpress.org/support/plugin/migravo-wxr-media-migration/)

## Description

Import an export into a new WordPress site and two things break that nothing in 
the box will fix for you.

**ACF Relationship, Post Object and Gallery fields stop working.** They store post
IDs, and the new site assigns different IDs, so every one of those fields now points
at the wrong post or at nothing at all. Nothing reports it. The fields simply look
empty, or worse, quietly reference unrelated content — and on a site built around
related posts, professionals, case studies or products, that is most of the site.

**Every image still lives on the old server.** The core importer copies the references,
not the files. The content looks fine right up to the day the source site is switched
off.

Migravo fixes both, on a site that has already been imported or as part of importing
it.

#### Repairing relationships

Upload the export from the _source_ site on the ID Map tab. Migravo matches each
old post to its new one by slug, builds a map of old ID to new ID, then rewrites
every relationship, post-object and gallery field on the site in one pass.

It sweeps the whole database rather than one post type, so both sides of a two-way
relationship are repaired as long as both are present — the field on the Artist 
and the field on the Song are just two more fields to fix, and it does not matter
which order you imported them in. Import one post type now and the other later, 
then run the repair again: the map keeps what it already knows and grows as you 
import.

It only rewrites values it can account for. A flat list of numeric IDs is remapped;
an ACF Link field, a repeater row, a phone number that merely starts with digits,
and any ID with no entry in the map are all left exactly as they were. An ID whose
target has since been deleted is reported rather than written as a dead reference.

**And it tells you what it could not fix, as a verdict rather than a guess.** An
ID it has no mapping for is left alone — correct in itself, but the field still 
points somewhere wrong and the number still looks like a valid ID, so nothing errors.
Migravo checks each one against the field’s own definition: ACF records which post
types a Relationship or Post Object field may reference, so a `practice_areas` field
declared as accepting `service` while holding an ID that is a `trade_report` here
is broken as a matter of fact. Those are reported as broken and listed first, with
how many fields hold each ID and what it actually is. Values that match the type
the field accepts are marked as almost certainly already repaired, so re-running
the tool does not raise false alarms. Import the missing post type and run it again;
the map grows as you import, so order does not matter.

#### Re-hosting media

During import, Migravo finds every media URL in the post content and the featured
image, downloads each file from the source site into the Media Library, and rewrites
the content to point at the local copy. Files are deduplicated by source URL and
filename, so importing the same export twice does not produce a second copy of every
image.

#### What else the export carries, and what arrives with it

A WXR file says more about each post than its title and content, and a field that
is read by nothing is a field that goes missing without a word. These now come across:

 * **Page hierarchy.** `wp:post_parent` holds an ID from the source site, so it 
   means nothing here until it is translated — and an untranslated one is why imported
   trees arrive flat, every child at the top level. Order in the file does not matter:
   a child that arrives before its parent is placed when the parent turns up.
 * **Image alt text.** Written on the attachment item in the export, which is not
   a post that gets imported, so it was being left behind for every image at once.
   Alt text already on this site is never overwritten.
 * **Comment and ping status**, rather than falling back to whatever this site’s
   defaults happen to be.
 * **Post password.** A protected post used to arrive public, with nothing to say
   so.
 * **Sticky posts**, for `post` — the one type WordPress reads that list for.

Each is applied only where the export actually states it. An empty tag is a statement—“
no password”, “not sticky” — and is honoured on a re-import; a tag that is not in
the file at all says nothing, and nothing on this site is overwritten on the strength
of it.

**One limitation worth knowing.** Posts are tied back to the source by the ID the
export gives them, and that ID is not qualified by which site it came from. Import
two different sites into one destination and a post from each can claim the same
source ID; the hierarchy lookup then has two candidates and takes the most recent.
This is inherent in what a WXR file records, not something the plugin can detect,
and it applies equally to the relationship repair.

#### The rest of a real migration

 * **Resumable, non-blocking import** — preview what the file contains, then import
   with a live progress bar and 1–10 concurrent requests. Close the tab or press
   Stop and the import offers to continue where it left off. Posts lost to a server
   hiccup are retried automatically, and any that still fail are listed with a button
   to retry just those.
 * **Safe re-imports** — choose to skip, update in place, or delete and replace 
   posts that already exist. Matching accounts for the fact that WordPress rewrites
   slugs on insert, so re-running an import does not silently duplicate posts.
 * **Taxonomy mapping** — send a taxonomy from the export into a different taxonomy
   on this site, or skip it.
 * **Author mapping** — map each author in the export to an existing user, or create
   one.
 * **Field renaming** — map meta/ACF field keys when the source and destination 
   names differ.
 * **Featured image from a custom field** — for source sites that store it outside
   the standard thumbnail meta.
 * **Multisite aware** — choose which site in the network to import into, or to 
   build the ID map against.
 * **Built for large exports** — tested against a 52 MB export of 5,179 posts, which
   it parses in about 44 seconds using around 4 MB of memory.

#### Security

 * Every AJAX action requires a valid nonce and the `import` capability.
 * All database queries are parameterized via `$wpdb->prepare()`.
 * Media downloads are guarded against SSRF — loopback, private-network and link-
   local addresses (including the cloud metadata endpoint at 169.254.169.254) are
   rejected. Redirects are followed, and every hop is checked by the same rule as
   the first, so a public URL that redirects to an internal address is refused at
   the redirect rather than fetched. A source site on a private network can be allowed
   deliberately, per host, using the `wxmi_allow_private_media_host` filter — see
   the FAQ.
 * Restoring serialized meta values blocks PHP object injection (unserialize is 
   called with `allowed_classes => false`).
 * The temporary directory used during import is given a random, unguessable name,
   so the export and the per-post cache written inside it cannot be requested by
   anyone who does not already know the path. An .htaccess and an index.php are 
   written alongside as a second layer — they help on Apache, but they are not relied
   on, because nginx, LiteSpeed and IIS do not read .htaccess at all.

#### Why not just use the core WordPress Importer?

Use it — and then use this. The core importer creates the posts, which is the part
that already works. What it has no concept of is what became of everything that 
referenced those posts by ID, or of the files they pointed at:

 * It does not know that ACF Relationship, Post Object and Gallery fields hold post
   IDs, so it leaves every one of them pointing at whatever now happens to own that
   ID on the new site.
 * It copies image references, not images. The content depends on the old server
   staying online forever.
 * Re-running it creates another copy of everything, so a partial or interrupted
   import cannot simply be repeated.

#### Is this a full-site migration tool?

No, and deliberately not. If you can copy an entire site — database, uploads and
all — a tool built for that is the right choice and this is not it.

Migravo is for the cases where you cannot: you have a WXR export and a destination
that already exists, you are pulling a few post types across rather than cloning
a whole site, or you are on the other side of an import that already happened and
left broken relationships behind. The ID Map tab works on a site that was imported
months ago by something else entirely.

Two things in a WXR file are deliberately not imported, and are not planned:

 * **Comments.** Migravo is about posts, the fields that point between them and 
   the media they use. Comment threads, their authors and their moderation state
   are a separate body of data with their own spam and approval concerns, and the
   core WordPress Importer already handles them.
 * **Navigation menus.** A menu is a set of posts referring to other posts by ID,
   so importing one without also owning the pages it points at produces a menu of
   dead links. Rebuild menus on the destination once the content is in place.

Run the core importer for either of those, then use this for the parts it leaves
behind.

## Installation

 1. Upload the plugin folder to `/wp-content/plugins/`.
 2. Activate the plugin through the “Plugins” menu in WordPress.
 3. Go to **Tools  Migravo Migration**.

## FAQ

### My ACF Relationship fields are empty after importing — can this fix it?

Yes, and that is the main reason the plugin exists. Those fields store post IDs,
and the new site handed out different IDs, so the values that survived the import
now point at nothing — or at whatever unrelated post happens to hold that ID.

Open the ID Map tab and upload the export from the _source_ site. Migravo matches
every old post to its new one by slug, then rewrites every Relationship, Post Object
and Gallery field on the site to the correct new IDs — every post type at once, 
so both directions of a two-way relationship are fixed provided both sides are on
the site.

### Can I use it on a site that was already imported by something else?

Yes. The ID Map tab needs only the original export file and the site as it stands
now; it does not care what performed the import or how long ago. If a migration 
left broken relationships behind months ago, this repairs them without re-importing
anything.

### Which field types get remapped, and which are left alone?

Remapped: flat lists of post IDs, which is how ACF stores Relationship, Post Object,
Gallery and similar fields.

Left untouched: anything that is not a plain list of IDs. An ACF Link field (title/
url/target), repeater rows, a value that merely starts with digits such as a phone
number, and any ID with no entry in the map are all written back exactly as they
were. An ID whose target has since been deleted or trashed is counted and reported
rather than written as a dead reference.

### What XML format does this accept?

Standard WordPress eXtended RSS (WXR) — the same file produced by any WordPress 
site’s Tools  Export.

### Does it download media from the old site automatically?

Yes, when “Download Media” is checked. It scans the post content and featured image
for URLs on the source site’s domain, downloads each into the Media Library, and—
if “Update URLs in Content” is also checked — rewrites the content to point at the
new local URLs.

### What happens if a post already exists?

You choose per import: **skip** it, **update** its content and media in place, or**
replace** it (delete the existing post and re-import fresh). Matching is by title
+ slug + post type.

### No media was imported from my staging site — why?

Almost certainly because that site is on a private network. Staging and development
sites usually resolve to a private address (10.x, 172.16–31.x, 192.168.x), and the
plugin refuses to fetch from those by default: an import file comes from outside
your site, and blindly following the URLs inside it is how a server ends up reading
its own internal services.

If you know the source site and want to allow it, name that one host:

    ```
    add_filter( 'wxmi_allow_private_media_host', function ( $allow, $host ) {
        return 'staging.example.com' === $host ? true : $allow;
    }, 10, 2 );
    ```

Only the host you name is allowed. Loopback, link-local and every other private 
host stay blocked, and there is no setting in the admin screen to switch this on—
it takes deliberate code on your own site.

### Does it work on Multisite?

Yes — a site selector appears wherever it’s relevant (import, ID map, ACF field 
lookup) so you can target any site in the network without switching your admin session.

### What if a scheduled cleanup is needed?

An hourly cron job (`wxmi_cleanup_temp_files`) removes temporary uploaded XML/cache
files older than one hour, in case an import session is abandoned partway through.

## Reviews

There are no reviews for this plugin.

## Contributors & Developers

“Migravo WXR Media Migration” is open source software. The following people have
contributed to this plugin.

Contributors

 *   [ QuiNguyen ](https://profiles.wordpress.org/dinhqui/)

[Translate “Migravo WXR Media Migration” into your language.](https://translate.wordpress.org/projects/wp-plugins/migravo-wxr-media-migration)

### Interested in development?

[Browse the code](https://plugins.trac.wordpress.org/browser/migravo-wxr-media-migration/),
check out the [SVN repository](https://plugins.svn.wordpress.org/migravo-wxr-media-migration/),
or subscribe to the [development log](https://plugins.trac.wordpress.org/log/migravo-wxr-media-migration/)
by [RSS](https://plugins.trac.wordpress.org/log/migravo-wxr-media-migration/?limit=100&mode=stop_on_copy&format=rss).

## Changelog

#### 1.2.0

Five fields the export carries were read by nothing, so an import produced a post
that only
 partly resembled the source. Each was checked against WXR generated by
WordPress itself rather than a file written by hand, and against real exports, on
a first import and on a re-import.

 * Page hierarchy is rebuilt instead of flattened. `wp:post_parent` holds an ID 
   belonging to
    the source site and was not being read at all, so every tree arrived
   flat — each child at the top level with nothing to say it had ever had a parent.
   On one real 153-item export that is 59 relationships lost in silence. Order in
   the file does not matter: a child that arrives before its parent is placed when
   the parent turns up, which is not a hypothetical — 3 of those 59 appear before
   the parent they name. Verified with `get_post_ancestors()` and get_pages() on
   a three-level page tree, imported parent-first and child-first; before this, 
   both came out with no ancestors at all and no children under the root.
 * Loops are refused. WordPress does check for them, but its check begins “new post
   can’t
    cause a loop” and returns — so a post created against a chain that is 
   already looped is attached to it, and everything that walks ancestors then follows
   a line that never ends. Reproduced by looping two pages by hand and importing
   a third naming one of them.
 * Image alt text comes across. `_wp_attachment_image_alt` appeared nowhere in the
   plugin, so
    every image arrived blank and the accessibility work done on the 
   source site was lost in the move. It is written on the attachment item, which
   is not a post that gets imported, so it is collected during the pass that already
   reads those items. Alt text on this site is never overwritten — the export is
   authoritative about what the source said, not about what the destination has 
   since decided — and images already in the library pick it up without being fetched
   again.
 * Post password is imported. A protected post arrived public, with nothing to indicate
   that
    anything had been dropped.
 * Comment status, ping status and sticky are imported, instead of falling back 
   to this
    site’s defaults. Comment and ping are taken only when they read `open`
   or `closed`; comments_open() tests for the exact string, so any other value would
   behave as closed while claiming to be something else.
 * An empty tag and a missing tag are told apart, which only shows on a re-import.
   Empty is
    the export saying “no password”, “not sticky”, and is applied; a tag
   absent from the file says nothing, and nothing here is overwritten on the strength
   of it. Real exports exist that omit these fields entirely — two items from one
   were imported, given a password by hand, and re-imported, and the password was
   gone.
 * Importing an export twice under different titles no longer leaves the second 
   hierarchy in
    halves. Where two posts on this site both claim the same source
   ID, the most recent is taken, and a child that had to settle for an older copy
   is moved when its real parent arrives — scoped to the import in progress, so 
   the earlier tree is not pulled apart to build the new one.

#### 1.1.3

 * An existing image is now identified by where it sits, not by what it is called.
   
   Matching was on the bare filename, and WordPress only keeps filenames unique 
   inside one month folder — so a site with any history has /2024/01/logo.png alongside/
   2025/06/logo.png, and logo.png, banner.jpg and hero.jpg are exactly the names
   that repeat. Reproduced with two such images: asking for the 2025 one returned
   the 2024 one, and the right one was then never downloaded, because the import
   believed it already had it. The wrong picture, with nothing reported. Matching
   is now on the upload path — 2025/06/logo.png — compared for equality against 
   what WordPress records for each file. This is the case the plugin is written 
   for, a destination that already holds media, and not an edge case.
 * A match found that way is no longer written back as if it were fact. The old 
   code
    stamped its guess onto the attachment, so the next lookup accepted it without
   re-examining and a wrong match could never correct itself. That record is now
   written in one place only, after a file has genuinely been downloaded from that
   URL.
 * When media download is switched off, the filename is still used, but only after
   the path
    has been tried and found nothing. In that mode the user has said the
   files are already on the site, so a missed match means no image at all rather
   than a re-download — an imperfect match beats a missing one there, while a right
   answer still wins wherever both are available.
 * Removed the Screenshots section from this file. It described two screenshots 
   that were
    not shipped.
 * Post content containing “]]>” is no longer mangled. A CDATA section cannot hold
   that
    sequence, so an exporter meeting one has to break out and back in — WordPress
   writes “]]]]><![CDATA[>” — and an XML parser rejoins the halves. Reading the 
   file with a regular expression did not, so those six characters were stored as
   though the author had typed them. Any post quoting XML, a regular expression 
   or a code sample was affected, silently. Checked against a real XML parser on
   all 5,179 items of a real export: 36,253 fields compared, no differences.
 * Values held outside CDATA now have their XML escapes decoded, so “Tom &amp; Jerry”
   
   reads as “Tom & Jerry”. Nothing in the import path reads such a field today, 
   because WordPress wraps everything carrying text in CDATA; this is so that the
   next field that does read one does not inherit the problem.
 * Importing with media download switched off now reports what it did. That route
   sets a
    featured image from a file already in the library, and it was the one
   media path that said nothing at all – a post that should have had an image and
   did not get one looked exactly like a post that never wanted one. Images attached
   this way are counted as already in the library, since nothing is transferred,
   and a post whose image this site does not have is now listed. Measured against
   the database on the same run: what the screen reports and what was actually attached
   now agree.
 * The plugin no longer loads itself on front-end requests. Every hook it registers
   is an
    admin screen, an admin-ajax action or the cleanup cron, none of which 
   can fire for a visitor, yet all eleven were being registered on every page view.
   This is tidiness rather than speed – the work avoided measures at 0.0092 ms and
   no query – but eleven fewer entries in the hook table is worth having.
 * Every message the plugin shows can now be translated. Twenty-four of them — everything
   
   from “No file uploaded” to “Import cache not found” — were returned as bare English
   strings that never passed through WordPress’s translation layer, so a translated
   site showed them in English no matter what. Confirmed by marking every string
   that goes through that layer and watching the responses: none of them did before,
   all of them do now.

#### 1.1.2

Security:

 * Files written during an import are no longer readable by anyone who asks. They
   were
    placed in a fixed uploads/wxmi-temp/ protected by an .htaccess containing“
   deny from all” — which is Apache configuration. nginx, LiteSpeed and IIS do not
   read .htaccess at all, so on any of them there was no protection, and the accompanying
   index.php only prevents a directory listing, not a request for a file whose name
   is already known. The names were known: the uploaded export is wxmi__.xml and
   each parsed post is items_/.txt, so on a normal site the first one is items_1/
   0.txt — nothing to guess. Confirmed on nginx: that URL returned the file’s contents
   without logging in, as did the export. The directory now carries a random 128-
   bit component in its name, which is the only defence that does not depend on 
   the web server; the .htaccess and index.php are still written as a second layer.
   Upgrading removes the old directory.

Housekeeping:

 * Two names that did not use the plugin’s `wxmi` prefix now do: the filter
    migravo_allow_private_media_host
   is now `wxmi_allow_private_media_host`, and the main class is `WXMI_Media_Migration`.
   Nothing outside the plugin can have been relying on either — this is its first
   release under this name.
 * The ID map option is no longer rewritten once per imported post. It was read,
   
   re-serialised and written back for every post, to add one entry: on one site 
   that is 4,881 entries at 74.8 KB with autoload off, so roughly 45 seconds and
   189 MB of writes across a 5,179-post import. The `_wxmi_old_post_id` meta written
   beside it already recorded the same thing per post, and the map is assembled 
   from both, so nothing is lost — verified by importing the same export with the
   option emptied and confirming relationships still remapped to the same ids.

#### 1.1.1

Every item below was reproduced before it was fixed, and the same test was run again

afterwards. Two of them were found only because the plugin was pointed at real exports
rather than a test site.

Security:

 * A media URL that redirected was followed wherever it led. 1.1.0 blocked the cloud
   
   metadata address 169.254.169.254 when the import file named it directly, then
   allowed redirects to be followed — and the check that catches that address ran
   only on the URL from the file, never on a redirect target. What guarded the hops
   was WordPress’s own wp_http_validate_url(), which rejects loopback and private
   ranges but allows link-local. So a public URL returning a 302 to the metadata
   endpoint was fetched: measured, it produced a connection error rather than a 
   refusal, meaning the request left the machine. On a host where that address answers,
   the response — instance credentials — would have been stored in the media library.
   Every hop is now checked by the same rule as the first. Following redirects at
   all is what made this reachable, so it is worth saying plainly that 1.0.0, which
   refused them outright, was not exposed to this.

Reliability:

 * An import running longer than an hour was destroyed by the plugin’s own cleanup,
   in two
    independent ways, either of which was enough. The transients holding 
   the cache location and the attachment and author maps expired after an hour, 
   and the hourly cron deleted any cache directory older than an hour — and nothing
   had written to that directory since the upload, so an import three hours in looked
   exactly as abandoned as one given up on at the start. Both were reproduced in
   isolation. The cache directory is now touched on every post imported, so “abandoned”
   is measured from the last request instead of from the upload; the transients 
   last a day; and the cron reaps at twelve hours.
 * A failed write to the item cache is now reported. The result of that write was
   discarded,
    so when it failed — a full disk, a permissions change — the upload
   still answered success and still listed every post it had found, because that
   list comes from parsing the XML and not from what reached disk. The run only 
   came apart later, at import, blaming the export: “Content, title, and excerpt
   are empty.”

Interface:

 * The ACF field mapping row appeared for some sites and not others, with nothing
   to
    explain it. Field keys were matched against a hexadecimal pattern, which 
   only fits the keys ACF generates for itself in the admin UI. A field declared
   in PHP or in hand-written JSON normally carries a readable key — field_practice_areas—
   and every one of those failed the test, leaving the list empty and the row hidden.
   Across the 1,974 distinct field keys on one machine the old pattern matched 1,229
   of them, and 15% of field groups were entirely unmatched, meaning importing that
   post type offered nothing to map at all.
 * “Media downloaded” now says what actually happened. It was one number that answered
   
   neither question it was asked. It missed the featured-image-field route entirely—
   that branch reported neither successes nor failures, so an import driven by that
   setting finished reporting 0 whether every image had arrived or none had; measured
   on a 47-item export, 47 images downloaded and the screen said 0. And “downloaded”
   was never what it counted: files already in the library counted too, so a second
   import of the same export reported a large figure while transferring nothing.
   It now reads “Media attached: 47 (47 uploaded, 0 already in library)”, and failures
   from that route are listed with the field they came from.
 * A featured-image URL is kept when its download fails. It used to be discarded
   either
    way, so a failure left no thumbnail and no record of what the image should
   have been.

#### 1.1.0

Tested against a real 52 MB / 5,179-post WXR export. Every item below is a problem
that
 export exposed.

Reliability:

 * Large files no longer exhaust memory. Parsing an upload made five passes over
   the whole
    document, holding it and a match array of every item at the same time—
   roughly 8x the file size at peak, enough for a host to kill the process before
   PHP could report anything. It now streams the file one item at a time: the 52
   MB export peaks at 4 MB and parses in about 44 seconds.
 * Failed posts are retried instead of being lost. A brief server hiccup used to
   lose every
    post whose request was in flight — 790 posts in one run — while the
   screen still said “Import Complete!”. Transport failures are now retried up to
   three times with a growing delay, which recovered all but 11 of those posts on
   the same data.
 * Posts that still fail are reported and can be retried. The import summary lists
   them and
    offers a retry button; the parsed data is kept on the server so retrying
   does not mean uploading again.
 * Stopping an import no longer discards it. Pressing Stop used to make resuming
   impossible
    — while closing the browser did not — so stopping properly was penalised.
   Reopen the page and the import offers to continue where it left off.

Data integrity:

 * Relationship repair now reports what it could not fix, and says which of those
   are
    definitely broken. An ID with no mapping was left alone and never mentioned,
   so a field could still point at the wrong post with nothing to indicate it — 
   the number looks like a valid ID, so nothing errors. Each one is now checked 
   against the field’s own definition: ACF records which post types a Relationship
   or Post Object field accepts, so a value of another type is wrong as a matter
   of fact and is reported as broken, while a value of an accepted type is marked
   as almost certainly already repaired. On one real site this identified 1,240 
   broken references across 964 fields that had previously gone unmentioned.
 * Where a post came from is now recorded on the post itself, not only in one option.
   The
    map can be rebuilt from the database, so it survives clearing it and no 
   longer depends on still having the export file.
 * Removed a “sync two-way relationships” step that wrote back more than it should.
   After
    repairing a relationship it also wrote the reverse link onto each post
   the field pointed at, storing it under the same field name. That name belongs
   to the forward field, which is usually restricted to the post type on the other
   side, so the reverse values it wrote sit in a field that does not accept them—
   the same thing this plugin’s own check reports as “points at the wrong kind of
   post”. It did this whether or not the field asked for a two-way relationship,
   which ACF has its own setting for, and it wrote no field-key row beside the value,
   so the result depended on some field group happening to apply to that post. Nothing
   replaces it: repairing relationships sweeps every post type in one pass, so both
   sides are fixed whenever both are present. Values an earlier version wrote are
   left exactly as they are — they are stored under a real field’s name and cannot
   be told apart from relationships you set yourself, so removing them is a decision
   only you can make.
 * Re-importing the same file no longer creates duplicate posts. Slugs containing
   
   characters WordPress strips (for example a leading “$”) were stored differently
   from what the file said, so “Skip” could not recognise those posts and made a
   second copy of each one on every import. The example export contained 112 such
   slugs.
 * Relationship remapping and the ID map now find those posts too. They were previously
   
   reported as missing, and their relationships were left pointing at the old site’s
   IDs.
 * Remapping relationships no longer scans the whole postmeta table in one query.
   It works
    in pages, so the operation stays within limits on large sites.

Interface:

 * Updated admin scripts now reach the browser. The script URL never changed between
   
   releases, so browsers kept running the cached older copy and fixes appeared to
   have no effect.
 * The results panel no longer announces “Import Complete!” for an import that was
   stopped
    or that lost posts.

Media download:

 * Media now downloads from source sites that redirect. Redirects were refused outright,
   
   so an http-to-https redirect — near universal — failed every download with an
   error the user could do nothing about. Following them is no less safe: the internal-
   address check is re-applied to each redirect, so a public URL still cannot be
   used to reach an internal one.
 * Importing no longer stalls waiting for shared images. When one image was already
   being
    fetched, the import slept up to ten seconds for it — and since a single
   image is often reused across many posts, several imports could be asleep at once
   and exhaust the server’s PHP workers, taking the whole run down. The wait is 
   now about a second.
 * Cloud metadata addresses are blocked. WordPress’s own URL check rejects loopback
   and
    private ranges but allows 169.254.169.254, the address AWS and Azure serve
   instance credentials on, so an import file could have had that response stored
   in the media library. Link-local and reserved addresses are now rejected too.
 * AVIF images are imported. The patterns that scan post content for media listed
   every
    other web image format but not AVIF, so an AVIF image was never even found,
   let alone downloaded – its URL was left pointing at the source site with nothing
   reported.
 * A source site on a private network can be allowed on purpose, one host at a time,
   with
    the new `wxmi_allow_private_media_host` filter — which is what makes migrating
   from a staging site possible without weakening the default for everyone else.

Housekeeping:

 * Deactivating the plugin now removes its temporary files. Previously it only cancelled
   
   the hourly cleanup task, so anything left over from an abandoned or failed import—
   the uploaded file plus one cache file per item — stayed in the uploads folder
   with nothing left to reclaim it.
 * Deleting the plugin now removes what it stored under its own name: the accumulated
   post
    ID map, the record it keeps on each imported post, the source URL noted
   against each downloaded attachment, its transients, and the temporary folder —
   around 4,600 rows on one real site. On Multisite this runs for every site. Imported
   posts, media and field values are yours and stay; the plugin only cleans up its
   own bookkeeping.

#### 1.0.0

 * Initial release.

## Meta

 *  Version **1.2.0**
 *  Last updated **9 hours ago**
 *  Active installations **Fewer than 10**
 *  WordPress version ** 5.0 or higher **
 *  Tested up to **7.0.3**
 *  PHP version ** 7.4 or higher **
 * Tags
 * [acf](https://wordpress.org/plugins/tags/acf/)[import](https://wordpress.org/plugins/tags/import/)
   [media](https://wordpress.org/plugins/tags/media/)[migration](https://wordpress.org/plugins/tags/migration/)
   [WXR](https://wordpress.org/plugins/tags/wxr/)
 *  [Advanced View](https://wordpress.org/plugins/migravo-wxr-media-migration/advanced/)

## Ratings

No reviews have been submitted yet.

[Your review](https://wordpress.org/support/plugin/migravo-wxr-media-migration/reviews/#new-post)

[See all reviews](https://wordpress.org/support/plugin/migravo-wxr-media-migration/reviews/)

## Contributors

 *   [ QuiNguyen ](https://profiles.wordpress.org/dinhqui/)

## Support

Got something to say? Need help?

 [View support forum](https://wordpress.org/support/plugin/migravo-wxr-media-migration/)