MishkaCmsMigration.Sources.WordPress.Export (Mishka CMS Migration v0.0.1)

Copy Markdown View Source

A WordPress export file (WXR, what Tools ▸ Export makes) read as it streams by (MishkaCmsMigration.Sources.WordPressFile): fold/3 hands each child of its <channel>, one at a time, to a function, and never holds more of the file than the child it is reading.

A child is an element/0: its own text, its attributes and its children by name, each a list in the order the file gives them. text/1, text/2, all/2 and attribute/2 read it. A <channel>'s children are the site's own fields (title, link, wp:wxr_version, …), its authors (wp:author), categories (wp:category), tags (wp:tag) and other terms (wp:term), and its items (item): every post, page, file, reusable block and menu item, each with its category references, its wp:postmeta and its wp:comments.

The file is read with :xmerl_sax_parser from the disk in blocks. An export never declares a document type, so a file that does is refused before anything in it is read: no external entity, DTD or other address it names is ever fetched or opened, and no entity is expanded, so neither a file that names a file of the server or an address (XXE) nor one that nests entities to grow without end (the billion laughs) gets anywhere. The parser is told the same (disallow_entities, external_entities: :none). A file that is not XML, whose root is not an <rss> with a <channel>, or that breaks off is refused too; each is {:error, :unreadable_file}, and a file that is not there {:error, :no_file}.

php/1 reads the PHP-serialized values WordPress keeps in some meta, such as a file's sizes in _wp_attachment_metadata, without running anything: arrays, strings, numbers, booleans and null; an object, or anything else, reads as nil.

Summary

Types

One element of the export: its text, its attributes and its children by name.

What fold/3's function answers for a child: {:cont, acc} to read on, {:halt, acc} to stop reading the file there.

Functions

The element's children named name, in order.

The element's attribute name, or nil.

Hands each child of the export's <channel> at path to fun with its qualified name, such as "item" or "wp:author", in the order of the file, from acc, and answers the last acc.

A PHP-serialized value as Elixir terms: an array whose keys are 0 to n - 1 in order is a list, any other a map with string keys. nil for an object, or for a value that is not one whole serialized value.

The element's own text, "" for none.

The text of the element's first child named name, or nil without one.

Types

element()

@type element() :: %{
  text: String.t(),
  attributes: %{optional(String.t()) => String.t()},
  children: %{optional(String.t()) => [element()]}
}

One element of the export: its text, its attributes and its children by name.

step(acc)

@type step(acc) :: {:cont, acc} | {:halt, acc}

What fold/3's function answers for a child: {:cont, acc} to read on, {:halt, acc} to stop reading the file there.

Functions

all(map, name)

@spec all(element(), String.t()) :: [element()]

The element's children named name, in order.

attribute(map, name)

@spec attribute(element(), String.t()) :: String.t() | nil

The element's attribute name, or nil.

fold(path, acc, fun)

@spec fold(Path.t(), acc, (String.t(), element(), acc -> step(acc))) ::
  {:ok, acc} | {:error, :no_file | :unreadable_file}
when acc: term()

Hands each child of the export's <channel> at path to fun with its qualified name, such as "item" or "wp:author", in the order of the file, from acc, and answers the last acc.

php(value)

@spec php(String.t() | nil) :: term()

A PHP-serialized value as Elixir terms: an array whose keys are 0 to n - 1 in order is a list, any other a map with string keys. nil for an object, or for a value that is not one whole serialized value.

text(map)

@spec text(element()) :: String.t()

The element's own text, "" for none.

text(element, name)

@spec text(element(), String.t()) :: String.t() | nil

The text of the element's first child named name, or nil without one.