# Mishka CMS Migration v0.0.1 - Table of Contents > A migration extension for Mishka CMS that moves a WordPress site into it: posts, pages, media, redirects, settings and Elementor design tokens, built with Elixir and Phoenix LiveView. ## Pages - [README](readme.md) - [License](license.md) ## Modules - [MishkaCmsMigration](MishkaCmsMigration.md): The migration extension's Ash domain. The extension moves a site from another CMS into a Mishka CMS site. No source ships yet: WordPress is planned as the first, and Joomla and others come later as plugins that reuse the same mapping, dry run, import job and report. - [MishkaCmsMigration.Activity](MishkaCmsMigration.Activity.md): The runs' log (`MishkaCmsMigration.MigrationLog`) as a source of the core's activity log, so a run's steps show where the CMS shows what happened, each in the sentence the bell says (`MishkaCmsMigration.Resources.Notifiers.Steps.sentence/2`). - [MishkaCmsMigration.Application](MishkaCmsMigration.Application.md): The migration extension's OTP application. It supervises every mishka_installer plugin the extension ships (`plugins/0`). A plugin registers on the event it listens on once it is supervised here. - [MishkaCmsMigration.Events](MishkaCmsMigration.Events.md): The three `mishka_installer` events an import fires, through `MishkaInstaller.Event.Hook.call/3`. Each carries the run it belongs to, and `meta: %{actor: actor, site_id: site_id}` for a plugin that takes `call/2`. A dry run fires none of them: it writes nothing in the CMS and asks no plugin. - [MishkaCmsMigration.MigrationItem](MishkaCmsMigration.MigrationItem.md): What the migration remembers about one item of a source on a site: what the last dry run would do with it (`plan`, `plan_reason`), the CMS record the last import wrote (`target_resource`, `target_id`), when that record last changed as the import left it (`target_updated_at`), what the source said about the item then (`title`, `source_slug`, `source_modified_at`, `content_hash`, and the fields the CMS has no home for, `extra`), the SHA-256 of the file an import downloaded for a media item (`file_hash`, which finds a second attachment of the same bytes), and what the last import that met it did (`outcome`, `reason`, `run_id`). `outcome` is `nil` for an item no import has met yet, so a run's counts (`MishkaCmsMigration.MigrationRun`'s `created_count` and the rest) count what its import did alone. The item's content and its other fields, such as a post's status and author, are not kept: every run reads them from the source again. - [MishkaCmsMigration.MigrationLog](MishkaCmsMigration.MigrationLog.md): One entry of a run's log: a step of its life (`step`), who took it (`user_id`, none for a job that failed on its own or that Oban discarded), and what it said: the host the run reads from, a finished step's counts and a failed step's reason. Nothing else of the run is kept, so an entry never holds its connection or its secret. - [MishkaCmsMigration.MigrationRun](MishkaCmsMigration.MigrationRun.md): One move of a site's content from another CMS: which source it reads (`source`, a `MishkaCmsMigration.Source` id), which host it reads from (`source_host`, so a site may take content from several sources, each run showing its own), how it connects (`connection`, and the secret sealed in `connection_secret`), what the admin chose (`options`) and how far it got. - [MishkaCmsMigration.MigrationRun.Options](MishkaCmsMigration.MigrationRun.Options.md) - [MishkaCmsMigration.Provision](MishkaCmsMigration.Provision.md): What setting the migration up on a site gives it: the permission names its runs are gated by, `admin:dashboard:migration` and `admin:api:migration`, so a site's admin can be granted them. The names are shared by every site, so the first site's install creates them and every later one finds them; the system writes them, whoever asked, through `MishkaCmsCore.Install.Bootstrap.ensure_permissions/1`, as `MishkaCmsCore.Runtime.ExtensionProvision` does for every extension. The extension ships no components, pages or snippets. - [MishkaCmsMigration.Repo](MishkaCmsMigration.Repo.md): The Postgres repo the extension's resources write through when it runs on its own. Inside the CMS the resources write through the CMS's repo, which `config :mishka_cms_core, repo: …` names. - [MishkaCmsMigration.Report](MishkaCmsMigration.Report.md): What a dry run or an import did with each item, saved on the run (`MishkaCmsMigration.MigrationRun`'s `report`) and handed to `migration:import:finished`. - [MishkaCmsMigration.Resources.Calculations.Ancestors](MishkaCmsMigration.Resources.Calculations.Ancestors.md): The items above each item at the source, for the item's `:ancestors` calculation: its parent's item (`source_parent`, of its kind and source host on its site), that one's parent, and so on, the top one first, as the Items page draws an item's place (Where it sits) and the Map view places an item under its parents. `nil` for an item with no parent. - [MishkaCmsMigration.Resources.Calculations.Decisions](MishkaCmsMigration.Resources.Calculations.Decisions.md): What waits for a decision now, and what an admin decided, among the items of each run's source host, for the run's `:decisions` calculation: `%{waiting: waiting, decided: decided}`. - [MishkaCmsMigration.Resources.Calculations.RecordPaths](MishkaCmsMigration.Resources.Calculations.RecordPaths.md): Where the CMS record of each item is, for the item's `:record_paths` calculation: the answer of `MishkaCmsMigration.MigrationRun.record_paths/2` for its kind and the record it was written to, with the record's `name` (its name, or its title), or `nil` for an item with no record, a kind with no pages, and a record that is gone or that the reader may not read. - [MishkaCmsMigration.Resources.Changes.CancelJobs](MishkaCmsMigration.Resources.Changes.CancelJobs.md): Cancels a run's jobs that are not done — waiting, scheduled, waiting to be tried again or running — with `Oban.cancel_all_jobs/1`, in the same transaction as the action, once the run is written. A job AshOban queued for one of the run's triggers would find the run out of the trigger's `where` when it ran and stop there; cancelling it now leaves nothing waiting in the queue for a run that is over. - [MishkaCmsMigration.Resources.Changes.CheckConnection](MishkaCmsMigration.Resources.Changes.CheckConnection.md): Connects to a new run's source before the run is saved (`MishkaCmsMigration.Source.connect/4`, with the run's `connection` and its sealed secret opened), so a run is only kept for a source it can reach; a refusal is also told on telemetry. What the source says of itself (`c:MishkaCmsMigration.Source.extra/1`) becomes the run's `extra`. - [MishkaCmsMigration.Resources.Changes.Choices](MishkaCmsMigration.Resources.Changes.Choices.md): Folds what the New import window's steps ask apart from the run's Advanced options into the run's `options`: the `bring` argument (the switches of what to bring, such as `"bring_posts"` and `"comments"`) and the `language` argument (the one language to put every item of an unknown or unpublished language in, blank to keep the source's languages), as the run's `"one_language"`. - [MishkaCmsMigration.Resources.Changes.Decided](MishkaCmsMigration.Resources.Changes.Decided.md): Keeps an admin's decision on the conflict an item waits on (`MishkaCmsMigration.MigrationItem.conflict/1`): the choice is put in the item's `decisions` under that conflict and brings the item back, so every later dry run and import follows it; `"leave_out"` leaves the item out instead. - [MishkaCmsMigration.Resources.Changes.EnqueueJob](MishkaCmsMigration.Resources.Changes.EnqueueJob.md): Queues the job of a run's AshOban trigger (`:trigger`) with `AshOban.run_trigger/3`, in the same transaction as the action, once the run is written, as the actor who asked. - [MishkaCmsMigration.Resources.Changes.PickedItems](MishkaCmsMigration.Resources.Changes.PickedItems.md): Turns the `items` argument of a run's `:dry_run_items` or `:import_items`, the ids of the items an admin picked on the Items page (`MishkaCmsMigration.MigrationItem`), into what the run's job is handed (`MishkaCmsMigration.Resources.Changes.EnqueueJob`): their source ids by kind, and the status the run goes back to once the job is done, the one it is in now. - [MishkaCmsMigration.Resources.Changes.Reason](MishkaCmsMigration.Resources.Changes.Reason.md): Keeps why a run failed: the action's `error` argument in words (`MishkaCmsMigration.Report.reason/1`) as the run's `reason`. The error is what AshOban hands its trigger's `on_error` action after the last attempt, or what Oban says of a job it discarded. - [MishkaCmsMigration.Resources.Changes.SourceFile](MishkaCmsMigration.Resources.Changes.SourceFile.md): The file a run of a source that reads one (`c:MishkaCmsMigration.Source.file?/0`) is handed, kept from the run's creation until it ends (see "The source's file" in `MishkaCmsMigration.MigrationRun`). - [MishkaCmsMigration.Resources.Notifiers.Steps](MishkaCmsMigration.Resources.Notifiers.Steps.md): What a run's steps tell the admins, an Ash notifier on `MishkaCmsMigration.MigrationRun`, run once a step is saved. - [MishkaCmsMigration.Resources.Validations.Connectable](MishkaCmsMigration.Resources.Validations.Connectable.md): Holds a step that queues a job, Run again (`:start_dry_run`) and the jobs of picked items (`:dry_run_items`, `:import_items`), on a run that still holds what its source connects with (`MishkaCmsMigration.MigrationRun.connectable?/1`): its sealed password, or the file it was handed. A run an earlier release ended cleared both, and its job could only fail on the source's refusal, so it is refused before anything is queued, in words: "This run no longer holds its WordPress password. Start a New import.", or its file, for a source that reads one. - [MishkaCmsMigration.Resources.Validations.MappedRecord](MishkaCmsMigration.Resources.Validations.MappedRecord.md): Holds an item's `mapped_id` (`MishkaCmsMigration.MigrationItem`'s `:choose`) to a record the item may be mapped to: one of the CMS resource its kind is written to (`MishkaCmsMigration.Target`), on the item's site, that the admin may read. `nil`, importing the item as its own, is always taken. - [MishkaCmsMigration.Resources.Validations.SiteIdle](MishkaCmsMigration.Resources.Validations.SiteIdle.md): Holds an admin's choice on an item (`MishkaCmsMigration.MigrationItem`'s `:leave_out`, `:bring_back`, `:choose`, `:decide_keep` and `:decide_import`, and their master actions) while one of the item's site's runs is dry running or importing: the job reads the choices as it walks, so one made halfway would hold for part of the run alone. - [MishkaCmsMigration.Resources.Validations.SourceInstalled](MishkaCmsMigration.Resources.Validations.SourceInstalled.md): Refuses a run whose `source` names no installed source plugin (`MishkaCmsMigration.Source.fetch/1`), on its `source` field, so a run is never created for a source no job could read. - [MishkaCmsMigration.Resources.Validations.Status](MishkaCmsMigration.Resources.Validations.Status.md): Holds a run's move to the statuses it may come from: the status the run is in before the action, not the one the action sets. - [MishkaCmsMigration.Source](MishkaCmsMigration.Source.md): A CMS a site is moved from: one module that is a `mishka_installer` plugin on the `"migration:sources"` event and implements the callbacks below. WordPress is the first (`MishkaCmsMigration.Sources.WordPress`); Joomla and others come the same way and reuse the mapping, the dry run, the import job and the report. - [MishkaCmsMigration.Source.Item](MishkaCmsMigration.Source.Item.md): One item of a source, as every source hands it over: the normalized model the dry run decides on and a target writes from. A source's `c:MishkaCmsMigration.Source.normalize/3` returns the map `build/1` turns into this struct; the walk puts `:kind` in, so a source never names it. - [MishkaCmsMigration.Source.Item.Content](MishkaCmsMigration.Source.Item.Content.md) - [MishkaCmsMigration.Sources.WordPress](MishkaCmsMigration.Sources.WordPress.md): The WordPress source (`"wordpress"`): a site read over its REST API, signed in to with a user name and an application password (WordPress 5.6+, Users ▸ Profile ▸ Application Passwords). WordPress offers application passwords over HTTPS only, or on a site whose `WP_ENVIRONMENT_TYPE` is `local`. - [MishkaCmsMigration.Sources.WordPress.Content](MishkaCmsMigration.Sources.WordPress.Content.md): A WordPress post's or page's `content.raw` as its item's content pieces (`MishkaCmsMigration.Source.Item`): `%{html: ...}` for what is converted here, and `%{name: ..., attrs: ..., raw: ...}` for a block or shortcode left to `migration:content:convert` (`MishkaCmsMigration.Events`), in the order they come. `call/2` reads the content with LazyHTML, an HTML5 parser, so its markup is never edited as text; only a shortcode, which is text, is read with a pattern. - [MishkaCmsMigration.Sources.WordPress.Elementor](MishkaCmsMigration.Sources.WordPress.Elementor.md): A WordPress page or post laid out with Elementor as its item's content pieces (`MishkaCmsMigration.Source.Item`), and the widgets in it the CMS has no equivalent for. - [MishkaCmsMigration.Sources.WordPress.ElementorKit](MishkaCmsMigration.Sources.WordPress.ElementorKit.md): Elementor's Site Settings, the kit a site's Elementor pages are drawn with: its global colours and fonts, which a widget names by id (`__globals__`), and the theme tokens of the CMS they become. - [MishkaCmsMigration.Sources.WordPress.Export](MishkaCmsMigration.Sources.WordPress.Export.md): A WordPress export file (WXR, what Tools ▸ Export makes) read as it streams by (`MishkaCmsMigration.Sources.WordPressFile`): `fold/3` hands each child of its ``, one at a time, to a function, and never holds more of the file than the child it is reading. - [MishkaCmsMigration.Sources.WordPress.Navigation](MishkaCmsMigration.Sources.WordPress.Navigation.md): A block theme's menus as the rows of a menu (`MishkaCmsMigration.Sources.WordPress.menus/4`). - [MishkaCmsMigration.Sources.WordPressFile](MishkaCmsMigration.Sources.WordPressFile.md): 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). - [MishkaCmsMigration.Target](MishkaCmsMigration.Target.md): Where one kind of item goes in the CMS: one module that is a `mishka_installer` plugin on the `"migration:targets"` event and names an existing CMS resource and the create and update actions that write it. The extension adds no write path of its own: every record is written by that action, as the admin who pressed Import and under the run's site, so the resource's policies, validations and plugins decide as they do for anything else. - [MishkaCmsMigration.Targets.BlogBlogger](MishkaCmsMigration.Targets.BlogBlogger.md): The `"user"` items of a source, its authors, written as the Blog's bloggers (`MishkaBlog.Blogger`), each the profile of a user of the run's site, through the `:create` and `:update` an admin's own blogger form runs, on the run's site and as the admin who pressed Import: the admin needs the Blog's `admin:dashboard:blog:post`, and the Blog's own rules decide the rest, such as one slug and one profile per user on a site, and that a profile names a user of its own site. - [MishkaCmsMigration.Targets.BlogCollection](MishkaCmsMigration.Targets.BlogCollection.md): The `"category"` items of a source written as the Blog's categories (`MishkaBlog.Collection`), through the `:create` and `:update` an admin's own category form runs, on the run's site and as the admin who pressed Import: the admin needs the Blog's `admin:dashboard:blog:post`, and the Blog's own rules decide the rest, such as one slug per site and how a slug is written (its `slugify`). - [MishkaCmsMigration.Targets.BlogComment](MishkaCmsMigration.Targets.BlogComment.md): The `"comment"` items of a source written as the Blog's comments (`MishkaBlog.Comment`), through the `:create` and `:update` an admin's own comment form runs, on the run's site and as the admin who pressed Import: the admin needs the Blog's `admin:dashboard:blog:comment` to write them and `admin:dashboard:blog:post` to find the posts and bloggers they belong to, and the Blog's own rules decide the rest, such as how a comment's text is cleaned (it is plain text). The Blog's `:create` keeps the author it is given (`author_id`), or a guest's name, instead of making the acting admin the author. - [MishkaCmsMigration.Targets.BlogPost](MishkaCmsMigration.Targets.BlogPost.md): The `"post"` items of a source written as the Blog's posts (`MishkaBlog.BlogPost`), through the `:create` and `:update` an admin's own post form runs, on the run's site and as the admin who pressed Import: the admin needs the Blog's `admin:dashboard:blog:post`, and the Blog's own rules decide the rest, such as how a slug is written (its `slugify`), how its content is sanitized and its code blocks highlighted. - [MishkaCmsMigration.Targets.BlogTag](MishkaCmsMigration.Targets.BlogTag.md): The `"tag"` items of a source written as the Blog's tags (`MishkaBlog.Tag`), through the `:create` and `:update` an admin's own tag form runs, on the run's site and as the admin who pressed Import: the admin needs the Blog's `admin:dashboard:blog:post`, and the Blog's own rules decide the rest, such as one slug per site and how a slug is written (its `slugify`). - [MishkaCmsMigration.Targets.CmsLayout](MishkaCmsMigration.Targets.CmsLayout.md): The `"menu"` items of a source: every menu of the site, classic and block (`MishkaCmsMigration.Sources.WordPress.Navigation`), written where an admin says it goes. A menu becomes the links of the site's layout, the core's own navigation, through the `:update` an admin's layout form runs (`MishkaCmsCore.Runtime.Layout`), on the run's site and as the admin who pressed Import: the admin needs the core's `admin:dashboard:runtime:layout`, and the core's own rules decide the rest, such as that the layout's template must compile. - [MishkaCmsMigration.Targets.CmsMedia](MishkaCmsMigration.Targets.CmsMedia.md): The `"media"` items of a source, its files, written to the CMS's own media library (`MishkaCmsCore.Runtime.Media`), on the run's site and as the admin who pressed Import: a file is stored and its record made by the core's `:upload`, the action the media chooser of an admin's forms runs, and written again by the `:update` an admin's own media form runs. The admin needs the core's `admin:dashboard:runtime:media`, and the core's own rules decide the rest: which types it keeps and how big, that a file's first bytes are the type its name says (`MishkaCmsCore.Runtime.FileSignature`), where the file is stored, and that a name is unique within a site. - [MishkaCmsMigration.Targets.CmsPage](MishkaCmsMigration.Targets.CmsPage.md): The `"page"` items of a source written as the CMS's own pages (`MishkaCmsCore.Runtime.Page`), through the `:create` and `:update` an admin's own page form runs, on the run's site and as the admin who pressed Import: the admin needs the core's `admin:dashboard:runtime:page`, and the core's own rules decide the rest, such as that a path is unique within a site and that a page's template must compile. - [MishkaCmsMigration.Targets.CmsSetting](MishkaCmsMigration.Targets.CmsSetting.md): The `"setting"` items of a source, the site's own name and pictures, written to the run's site's settings (`MishkaCmsCore.Runtime.Setting`) through the `:set` the Settings page's save runs (`MishkaCmsCore.Settings.put_as/4`), as the admin who pressed Import: the admin needs what writing the setting takes (`MishkaCmsCore.Settings.write_permission/1`, `admin:dashboard:setting` for these), and the core's own rules decide the rest. - [MishkaCmsMigration.Targets.CmsStylesheet](MishkaCmsMigration.Targets.CmsStylesheet.md): The `"stylesheet"` items of a source, such as Elementor's global colours and fonts as theme tokens (`MishkaCmsMigration.Sources.WordPress.ElementorKit`), written as a stylesheet of the run's site (`MishkaCmsCore.Runtime.Stylesheet`) through the `:create` and `:update` the Stylesheets page runs, as the admin who pressed Import: the admin needs `admin:dashboard:runtime:stylesheet` (or the code permission), and the core's own rules decide the rest. A stylesheet Tailwind will not build is refused (`MishkaCmsCore.Runtime.Resources.Changes.StylesheetCompiles`), and each write builds the site's CSS again. - [MishkaCmsMigration.Targets.Helpers.BySlug](MishkaCmsMigration.Targets.Helpers.BySlug.md): The `existing/2` (`MishkaCmsMigration.Target`) of a target whose resource keeps one record per slug on a site, such as the Blog's tags and categories: an item whose slug the run's site already holds a record with, in any language, as the Blog keeps one slug per site, waits for an admin's decision as `"slug_taken"` (`MishkaCmsMigration.MigrationItem`'s Decide…). Connect to the existing record connects the item to it (`"keep"`), and Import as new with another slug (`"import"`) writes it as its own, with the slug `wanted/2` makes and the report saying so (`note/2`, `"another_slug"`). - [MishkaCmsMigration.Targets.Helpers.ContentChecks](MishkaCmsMigration.Targets.Helpers.ContentChecks.md): What a post or a page may wait on before it is written, for the targets that write it (`MishkaCmsMigration.Targets.CmsPage`, `MishkaCmsMigration.Targets.BlogPost`), asked in this order, the first that waits answering: its language, which the site may not publish in or the source may not say (`MishkaCmsMigration.Targets.Helpers.Editions`), the widgets of a page builder the CMS has no equivalent for (`MishkaCmsMigration.Targets.Helpers.Widgets`), its scripts and inline event handlers (`MishkaCmsMigration.Targets.Helpers.Scripts`), and the video players the site does not allow yet (`MishkaCmsMigration.Targets.Helpers.Frames`). An admin decides each in turn. - [MishkaCmsMigration.Targets.Helpers.Editions](MishkaCmsMigration.Targets.Helpers.Editions.md): An item's language and the other editions of it, for the targets whose resource keeps one record per language, joined by `language_group_id` (`MishkaCmsCore.LanguageEditions`): the core's pages (`MishkaCmsMigration.Targets.CmsPage`) and the Blog's posts, categories and tags (`MishkaCmsMigration.Targets.BlogPost`, `MishkaCmsMigration.Targets.BlogCollection`, `MishkaCmsMigration.Targets.BlogTag`). A source names them in the item's `"language"` and `"translations"` (`%{"en" => "121"}`, the source id of each other edition by language), as the WordPress source reads them from Polylang or WPML (`MishkaCmsMigration.Sources.WordPress`, "Languages"). - [MishkaCmsMigration.Targets.Helpers.Frames](MishkaCmsMigration.Targets.Helpers.Frames.md): The video players a post's or a page's content frames, allowed on the run's site, for the targets that write the content (`MishkaCmsMigration.Targets.BlogPost`, `MishkaCmsMigration.Targets.CmsPage`). The CMS's Content-Security-Policy lets a page frame only the origins of the site's `security.frame_origins` setting (Sites pages may embed), so an imported YouTube or Vimeo player would show nothing without its origin there. - [MishkaCmsMigration.Targets.Helpers.LanguageUrls](MishkaCmsMigration.Targets.Helpers.LanguageUrls.md): Whether the links an import writes may not match the site's addresses, for the targets that write links (`MishkaCmsMigration.Targets.CmsLayout`). - [MishkaCmsMigration.Targets.Helpers.Read](MishkaCmsMigration.Targets.Helpers.Read.md): The records a query finds for a target's `existing/2` (`MishkaCmsMigration.Target`), read as the admin running the job under the run's site and the CMS's own policies. - [MishkaCmsMigration.Targets.Helpers.Scripts](MishkaCmsMigration.Targets.Helpers.Scripts.md): The scripts and inline event handlers a post's or a page's content holds, for the targets that write the content (`MishkaCmsMigration.Targets.CmsPage`, `MishkaCmsMigration.Targets.BlogPost`): each `