# `MishkaCmsMigration.Sources.WordPressFile`
[🔗](https://github.com/mishka-group/mishka_cms_migration/blob/v0.0.1/lib/mishka_cms_migration/sources/wordpress_file.ex#L5)

The WordPress export file source (`"wordpress_file"`): a site read from the file WordPress makes
under Tools ▸ Export (WXR), for a site whose REST API cannot be reached or signed in to. It gives
the same items as the WordPress source (`MishkaCmsMigration.Sources.WordPress`), so a site moved
from its export lands in the CMS as it does from its REST API, but for what an export does not
carry (below), and a run of either finds what a run of the other wrote (both read from the same
host).

It reads a file (`file?/0`): the New import form asks for the export, an `.xml` file, and no
connection keys (`form_fields/0` is empty), and the run keeps the file until it ends
(`MishkaCmsMigration.MigrationRun`'s "The source's file"); its path is the connection's `"file"`.
The file is streamed (`MishkaCmsMigration.Sources.WordPress.Export`), never read whole, and one
that declares a document type, entities or anything it would fetch is refused.

`host/1` is the host of the export's `<link>`, the site's home, which is also where its files are
downloaded from (`MishkaCmsMigration.Targets.CmsMedia`). `connect/2` reads the whole file once,
refusing with one of these reasons:

  * `:no_file` — the connection names no file, or none is there;
  * `:unreadable_file` — the file is not a WordPress export: not XML, not an `<rss>` channel
    with a `wp:wxr_version` and a `<link>`, broken off, or one that declares a document type.

While it reads, it keeps what the items point at: the authors by login, the categories and tags
by slug, how many published posts each term has, and the id, slug and address of each post and
page, the address, alt text and sizes of each file, and the content of each reusable block. That
is how it answers the reads the WordPress source's `normalize/3` makes with its connection's
`read` (the blocks and gallery images of a post's content, the posts, pages and files its links
point at), from the file instead of the site. `extra/1` keeps the site's name, tagline and home.

## What it reads

`kinds/0` is the WordPress source's, in its order. `list/3` reads the file again for each page of
100 rows of a kind, from the top, and stops at the row after the page's last; the cursor is the
next page's number. Each row is the shape the REST API answers for that kind, built from the
export, and `normalize/3` is the WordPress source's own, so its items carry the same attrs, extra
and content, converted the same way:

  * setting — the site's name and tagline, the channel's `title` and `description`, read once
    when the file is opened;
  * stylesheet — Elementor's global colours and fonts, read once when the file is opened from
    the settings (`_elementor_page_settings`) of its kit, the `elementor_library` item whose
    `_elementor_template_type` is `kit`, as the REST API's `/elementor/v1/globals` answers them
    (`MishkaCmsMigration.Sources.WordPress.ElementorKit.from_settings/1`). An export does not say
    which kit is active, so of two the one made last is read; an export with none has no row;
  * user — each `wp:author`: its id, login (`username`), display name (`name`), first and last
    name, email, and a `slug` made of the login the way the CMS slugs a name;
  * category and tag — each `wp:category` and `wp:tag`: its id, name, slug, description, how
    many published posts carry it (`count`) and, for a category, its parent's id;
  * media — each `attachment` item: its title, slug, address (`source_url`, the
    `wp:attachment_url`), type (`mime_type`, from its file name), alt text, caption, description,
    the post it was uploaded to, its dates, and its `media_details` (`_wp_attachment_metadata`,
    its size in bytes and the address of each size WordPress made of it);
  * page and post — each `page` and `post` item, but the trash: title, content, excerpt, slug,
    status, address (`link`), author's id, dates, featured file (`_thumbnail_id`), template, its
    other meta under `meta`, and a page's parent, menu order and `permalink_template` (its
    parent's address, or the home, with `%pagename%/`), a post's categories and tags by id,
    whether it is sticky and its format;
  * comment — each `wp:comment` of an item that is not in the trash, approved or held for
    moderation, with its post, parent, author, text, status, type and dates;
  * post_redirect — each post again, its id, title, status, address and modified time, its old
    address being the path of its `<link>`;
  * menu — each `nav_menu` term (`wp:term`, which an export may list twice) with its
    `nav_menu_item` items, whose own fields are their meta (`_menu_item_type`,
    `_menu_item_object`, `_menu_item_object_id`, `_menu_item_menu_item_parent`,
    `_menu_item_url`, `_menu_item_target`), read in one pass
    (`MishkaCmsMigration.Sources.WordPress.menus/4`). An item with no label of its own takes the
    title of the page or post, or the name of the category or tag, it points at, and its address
    too; one whose page, post or term the export does not carry is invalid, as WordPress counts
    it. Each `wp_navigation` post is a block menu too, as the REST API answers it.

## Languages

An export of a site that runs Polylang or WPML carries the language of each entry and term, and
which entries and terms are editions of one another, and a page, a post, a category and a tag row
carries them as Polylang Pro's REST API does, `lang` and `translations` (every edition's id by
language, the row's own among them), which the WordPress source reads into the item
(`MishkaCmsMigration.Sources.WordPress`, "Languages"):

  * Polylang keeps them as its own taxonomies, which an export carries as terms (`wp:term`): an
    entry names its `language` term (its slug is the code) and its `post_translations` term, and
    each `post_translations` and `term_translations` term describes its group as a serialized
    map of code to id. A term's language is its code in the `term_translations` group that names
    it; a term in none says no language;
  * WPML, with its WPML Export and Import, keeps them in an entry's meta and a term's meta
    (`_wpml_import_language_code`, the code, and `_wpml_import_translation_group`, a value every
    edition of one entry shares; https://wpml.org/documentation/translating-your-contents/multilingual-content-import/).
    These are not kept in `meta`.

`extra/1` keeps which plugin the export came from as `"languages"` (`"polylang"` or `"wpml"`),
as the WordPress source does for a site.

## Where it differs from the REST API

An export carries less than the REST API answers, and some of it otherwise:

  * the site's logo and icon are options, which an export does not carry, so only its name and
    tagline are read;
  * an author has no bio, website, avatar, nickname or roles, so a blogger made from one has
    none, and its slug is made of its login rather than WordPress's own `user_nicename`;
  * a term has no `link`, so a menu item of a category or a tag has WordPress's plain address of
    it (`/?cat=<id>`, `/?tag=<slug>`), which WordPress answers whatever its permalinks;
  * a menu's theme locations are the theme's, which an export does not carry, so the main menu
    is the first made;
  * a draft, pending or scheduled entry WordPress kept no GMT date for has no `date_gmt` (the
    export gives only the site's own time, not its timezone), and no entry has a `generated_slug`;
  * `meta` holds every meta field of the entry the export carries (custom fields among them,
    such as `subtitle`), where the REST API answers only those a plugin registered;
  * an export of posts alone (Tools ▸ Export ▸ Posts) carries the authors, files and comments of
    its posts but no categories, tags or pages, so its posts come without their categories and
    tags; an export of all content carries both.

`MishkaCmsMigration.Source.Item.hash/1` covers `extra`, so an item read from an export and later
from the REST API, or the other way round, is updated once.

# `error_return`

```elixir
@type error_return() ::
  {:error, [%{action: atom(), field: atom(), message: String.t()}]}
```

# `okey_return`

```elixir
@type okey_return() :: {:ok, struct() | map() | module() | [any()]}
```

# `child_spec`

Returns a specification to start this module under a supervisor.

See `Supervisor`.

# `config`

```elixir
@spec config() :: keyword()
```

# `config`

```elixir
@spec config(atom()) :: any()
```

# `enable`

```elixir
@spec enable() :: okey_return() | error_return()
```

# `get`

```elixir
@spec get() :: keyword()
```

# `health_check`

```elixir
@spec health_check() :: :ok | {:degraded, term()} | {:error, term()}
```

Optional. See `c:MishkaInstaller.Event.Hook.health_check/0`. Override to report health.

# `on_dependency_error`

```elixir
@spec on_dependency_error(term()) :: term()
```

Optional. See `c:MishkaInstaller.Event.Hook.on_dependency_error/1`. Override to react.

# `register`

```elixir
@spec register() :: okey_return() | error_return()
```

# `restart`

```elixir
@spec restart() :: okey_return() | error_return()
```

# `start`

```elixir
@spec start() :: okey_return() | error_return()
```

# `start_link`

# `stop`

```elixir
@spec stop() :: okey_return() | error_return()
```

# `unregister`

```elixir
@spec unregister() :: okey_return() | error_return()
```

---

*Consult [api-reference.md](api-reference.md) for complete listing*
