202 changes: 1 addition & 201 deletions docs/gh-pages/src/configuration.md
Original file line number Diff line number Diff line change
@@ -1,201 +1 @@
<!-- toc -->

# Usage

This chapter is about the basic setup of the plugin.

You can use this plugin **out of the box** to export/import

- content (posts/pages/nav_menu and media assets)

- FSE content (Full Site Editing assets like blocks, templates, template parts, reusable blocks and theme settings)

For everything else (third-party plugin settings for example) you need to provide custom export/import profile(s).

## Configuration

Impex import and export can be customized by providing **Profiles**.

An **Export profile** defines the Wordpress data to export.

An **Import profile** declares how and which exported data should be consumed.

## Custom profile configuration

**Profiles** are configured and registered using a custom Impex [Wordpress action](https://developer.wordpress.org/plugins/hooks/actions/) :

```php
\add_action('cm4all_wp_impex_register_profiles', function () {
// your Impex profile registration goes here
...
});
```

Using a [Wordpress action](https://developer.wordpress.org/plugins/hooks/actions/) for Impex profile registration guarantees that the action callback is only executed if the Impex plugin is activated.

> Implementing a Impex profile in your plugin does not make Impex a required dependency for your plugin since the code is only executed if Impex is avalable and active.
### Export

Let's say you want to export the core Wordpress contents (pages/posts/attachments and stuff) but also

- [Ninja Forms Contact Form](https://wordpress.org/plugins/ninja-forms/) contents and settings

- [Ultimate Maps by Supsystic](https://wordpress.org/plugins/ultimate-maps-by-supsystic/) contents and settings

- [Complianz GDPR](https://wordpress.org/plugins/complianz-gdpr/) contents and settings

- some options of your own plugin

To do so you need to create a **Export profile**.

Fortunately Impex provides some low level building blocks called **ExportProvider** to make our Impex Profile declaration piece of cake :

```php
\add_action('cm4all_wp_impex_register_profiles', function () {
// ensure admin plugin functions are available
require_once(ABSPATH . 'wp-admin/includes/plugin.php');

// register a new export profile
$profile = Impex::getInstance()->Export->addProfile('impex-export-profile-example');
// give the profile a senseful description
$profile->setDescription('Exports posts/pages/media-assets and plugin data of [cm4all-wordpress,complianz-gdpr,ninja-forms,ultimate-maps-by-supsystic]');

// export pages/posts/comments/block patterns/templates/template parts/reusable blocks
$profile->addTask(
'wordpress content',
cm4all\wp\impex\ContentExporter::PROVIDER_NAME
);

// export media
$profile->addTask(
'wordpress attachments (uploads)',
cm4all\wp\impex\AttachmentsExporter::PROVIDER_NAME
);

// export ninja-forms related tables/options if active
$plugin_ninjaforms_disabled = !is_plugin_active("ninja-forms/ninja-forms.php");
$profile->addTask(
"ninja-forms db tables (nf3_*)",
cm4all\wp\impex\DbTablesExporter::PROVIDER_NAME,
[cm4all\wp\impex\DbTablesExporter::OPTION_SELECTOR => 'nf3_*',]
)->disabled = $plugin_ninjaforms_disabled;
$profile->addTask(
'ninja-forms wp_options',
cm4all\wp\impex\WpOptionsExporter::PROVIDER_NAME,
[cm4all\wp\impex\WpOptionsExporter::OPTION_SELECTOR => ['ninja_*', 'nf_*', 'wp_nf_*', 'widget_ninja_*']]
)->disabled = $plugin_ninjaforms_disabled;

// export ultimate_maps related tables/options
$plugin_ultimatemaps_disabled = !is_plugin_active("ultimate-maps-by-supsystic/ums.php");
$profile->addTask(
"ultimate_maps db tables (ums_*)",
cm4all\wp\impex\DbTablesExporter::PROVIDER_NAME,
[cm4all\wp\impex\DbTablesExporter::OPTION_SELECTOR => 'ums_*',]
)->disabled = $plugin_ultimatemaps_disabled;
$profile->addTask(
'ultimate_maps wp_options',
cm4all\wp\impex\WpOptionsExporter::PROVIDER_NAME,
[cm4all\wp\impex\WpOptionsExporter::OPTION_SELECTOR => ['ums_*', 'wp_ums_*',]]
)->disabled = $plugin_ultimatemaps_disabled;

// export complianz related tables/options
$plugin_complianz_disabled = !is_plugin_active("complianz-gdpr/complianz-gpdr.php");
$profile->addTask(
"complianz-gdpr db tables",
cm4all\wp\impex\DbTablesExporter::PROVIDER_NAME,
[DbTablesExporter::OPTION_SELECTOR => 'cmplz_*',]
)->disabled = $plugin_complianz_disabled;
$profile->addTask(
'complianz-gdpr wp_options',
cm4all\wp\impex\WpOptionsExporter::PROVIDER_NAME,
[cm4all\wp\impex\WpOptionsExporter::OPTION_SELECTOR => ['cmplz_*', 'complianz_*']]
)->disabled = $plugin_complianz_disabled;

// export our own plugin uses wp_options starting with `foo-` or `bar*`
$profile->addTask(
'custom plugin options',
cm4all\wp\impex\WpOptionsExporter::PROVIDER_NAME,
[cm4all\wp\impex\WpOptionsExporter::OPTION_SELECTOR => ['foo-*','bar-*']]
)->disabled = !is_plugin_active("cm4all-wordpress/plugin.php");
});
```

That's it !

Now you can trigger the export using this Impex export configuration in the Impex screen at WP dashboard (or even using the [Impex CLI](./impex-cli.md))

### Import

Thanks to Impex architecture you normally don't need to define a custom import configuration.

Impex sports a generic `all` import provider and profile importing **anything** exported using the Impex building blocks.

> As long as you use the **Exporter** provided by Impex in your custom Export profile, you don't need to define an matching custom import profile.
But sometimes there exist some etch cases when you need to execude PHP code after the import to get everything working.

In our export profile example above we implemented support for [Ninja Forms Contact Form](https://wordpress.org/plugins/ninja-forms/). Unfortunately the forms will be in maintenance mode after importing them.

To fix this we need to execute some PHP code (`WPN_Helper::set_forms_maintenance_mode(0)` from the [Ninja Forms Contact Form Plugin](https://wordpress.org/plugins/ninja-forms/)) after the import.

Impex provides **Events** for exactly that purpose :

```php
\add_action('cm4all_wp_impex_register_profiles', function () {
// ensure admin plugin functions are available
require_once(ABSPATH . 'wp-admin/includes/plugin.php');

// get the 'all' profile
$profile = Impex::getInstance()->Import->getProfile('all');

// attach a listener callback for the `EVENT_IMPORT_END` event
$profile->events(ImpexImport::EVENT_IMPORT_END)->addListener(
'reset ninja forms mainentance mode',
fn () => method_exists('WPN_Helper', 'set_forms_maintenance_mode') && WPN_Helper::set_forms_maintenance_mode(0)
);
});
```

Tada - thats it !

There ist just one caveat ... what if the Impex 'all' profile gets disabled by someone else ?
To work around this we can also introduce a custom import profile utilizing the 'all' import provider:

```php
\add_action('cm4all_wp_impex_register_profiles', function () {
// ensure admin plugin functions are available
require_once(ABSPATH . 'wp-admin/includes/plugin.php');

// create a new import profile
$profile = Impex::getInstance()->Import->addProfile('impex-import-profile-example');
$profile->setDescription('Import everything example with event listener');

// reuse the 'all' import provider registered by the 'all' import profile
$profile->addTask('main', Impex::getInstance()->Import->getProvider('all')->name);

// attach a listener callback for the `EVENT_IMPORT_END` event
$profile->events(ImpexImport::EVENT_IMPORT_END)->addListener(
'reset ninja forms mainentance mode',
fn () => method_exists('WPN_Helper', 'set_forms_maintenance_mode') && WPN_Helper::set_forms_maintenance_mode(0)
);
});
```

### (API)

**THIS IS JUST DUMNMY CONTENT**

An example diagram.

```mermaid
graph TD;
A-->B;
A-->C;
B-->D;
C-->D;
```

- there is

- more to come :-)
# Configuration
30 changes: 15 additions & 15 deletions docs/gh-pages/src/impex-cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@

# CLI

Impex provides a commandline tool to interact with the Impex plugin remotely using Wordpress HTTP REST API.
Impex provides a commandline tool to interact with the Impex plugin remotely using WordPress HTTP REST API.

Using this tool you can import and export data from and to a remote wordpress installation.

> impex-cli works also fine at _most_ managed wordpress installations since it does'nt need direct Wordpress access like [wp-cli](https://wp-cli.org/).
> impex-cli works also fine at _most_ managed wordpress installations since it does'nt need direct WordPress access like [wp-cli](https://wp-cli.org/).
# Prerequisities

Expand All @@ -33,14 +33,14 @@ Using the official PHP 7.4 Docker image :

```sh
docker run -it --network host --rm -v "$PWD":/usr/src/myapp -w /usr/src/myapp php:7.4-cli php \
impex-cli-php7.4.0.php export-profile list -username=admin -password=password -rest-url=http://localhost:8888/wp-json
impex-cli-php7.4.0.php export-profile list -username=<adminuser> -password=<password> -rest-url=http://localhost:8888/wp-json
```

Alternatively using the PHP 8.0 image:

```sh
docker run -it --network host --rm -v "$PWD":/usr/src/myapp -w /usr/src/myapp php:8.0-cli php \
impex-cli.php export-profile list -username=admin -password=password -rest-url=http://localhost:8888/wp-json
impex-cli.php export-profile list -username=<adminuser> -password=<password> -rest-url=http://localhost:8888/wp-json
```

## Syntax
Expand Down Expand Up @@ -73,8 +73,8 @@ Example:

```sh
impex-cli.php export-profile \
-username=admin \
-password='password' \
-username=<adminuser> \
-password='<password>' \
-rest-url=http://example.com/wp-json
```

Expand All @@ -90,7 +90,7 @@ Example:
impex-cli.php export-profile \
-H="X-foo=bar" \
-H="X-myrealm=cheers" \
-username=admin -password='password' \
-username=<adminuser> -password='<password>' \
-rest-url=http://example.com/wp-json
```

Expand All @@ -107,7 +107,7 @@ Example:
```sh
impex-cli.php export-profile \
-verbose \
-username=admin -password='password' \
-username=<adminuser> -password='<password>' \
-rest-url=http://example.com/wp-json
```

Expand All @@ -120,7 +120,7 @@ Example:
```sh
impex-cli.php export-profile \
-CURLOPT_VERBOSE \
-username=admin -password='password' \
-username=<adminuser> -password='<password>' \
-rest-url=http://example.com/wp-json
```

Expand Down Expand Up @@ -148,7 +148,7 @@ A Impex export results in a directory structure containing

- JSON Files for structured data

Wordpress content will be stored in JSON files. This gives you also the option to tranform the content locally before re-importing them somewehere else.
WordPress content will be stored in JSON files. This gives you also the option to tranform the content locally before re-importing them somewehere else.

- Blobs for attachments/media

Expand All @@ -158,7 +158,7 @@ Example:

```sh
impex-cli.php export \
-username=admin -password='password' \
-username=<adminuser> -password='<password>' \
-rest-url=http://localhost:8888/wp-json \
-overwrite \
-profile=base \
Expand All @@ -175,7 +175,7 @@ export-cm4all-wordpress-created-
│   ├── slice-0001-logo-fabrics.png
│   ├── slice-0002-johny-goerend-ou-GkKJm3fc-unsplash.jpg
│   ├── slice-0002.json
│   ├── slice-0003.json
│   └── slice-0003.json
...
├── chunk-0006
│   ├── slice-0000.json
Expand Down Expand Up @@ -231,7 +231,7 @@ Example usage:
```sh
impex-cli.php import \
-username=admin -password='password' \
-username=<adminuser> -password='<password>' \
-rest-url=http://localhost:8888/wp-json \
-profile=all \
~/tmp/my-export
Expand Down Expand Up @@ -263,7 +263,7 @@ Example usage:

```sh
impex-cli.php export-profile list \
-username=admin -password='password' \
-username=<adminuser> -password='<password>' \
-rest-url=http://localhost:8888/wp-json
```

Expand Down Expand Up @@ -296,7 +296,7 @@ Example usage:

```sh
impex-cli.php import-profile list \
-username=admin -password='password' \
-username=<adminuser> -password='<password>' \
-rest-url=http://localhost:8888/wp-json
```

Expand Down
10 changes: 10 additions & 0 deletions docs/gh-pages/src/jsonschema/slice-attachment-example.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"$schema": "slice-attachment.json",
"version": "1.0.0",
"type": "php",
"tag": "attachment",
"meta": {
"entity": "attachment"
},
"data": "https://www.example.com/attachment.jpg"
}
46 changes: 46 additions & 0 deletions docs/gh-pages/src/jsonschema/slice-attachment.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
{
"$schema": "http://json-schema.org/draft-07/schema",
"title": "Impex Content Slice",
"description": "a Impex slice containing content",
"type": "object",
"properties": {
"$schema": {
"type": "string",
"description": "The JSON schema for this slice"
},
"version": {
"title": "Impex Provider content version",
"description": "Version will be used by Impex to know what data format to expect",
"const": "1.0.0"
},
"type": {
"title": "Impex slice type",
"description": "Value will be used by Impex to know the content type of the slice",
"const": "php"
},
"tag": {
"title": "Impex slice tag",
"description": "The Impex slice tag contains information about the responsible Impex provider for this slice",
"const": "attachment"
},
"meta": {
"title": "Metadata for the slice",
"description": "Metadata is a JSON object used to store additional information about the slice",
"const": {
"entity": "attachment"
}
},
"data": {
"format": "uri-template",
"type": "string",
"title": "Data portion of this Impex slice",
"description": "Attachment slice data is expected to be a URI to the attachment file.\nFor media it has to be the URI to the image.\nURI can be absolute or relative.",
"examples": [
"./media/image.jpg",
"https://www.example.com/attachment.jpg"
]
}
},
"additionalProperties": false,
"required": ["version", "type", "tag", "meta", "data"]
}
23 changes: 23 additions & 0 deletions docs/gh-pages/src/jsonschema/slice-content-example.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"$schema": "slice-content.json",
"version": "1.0.0",
"type": "php",
"tag": "content-exporter",
"meta": {
"entity": "content-exporter"
},
"data": {
"posts": [
{
"wp:post_id": 1,
"title": "Home",
"wp:post_content": "<!-- wp:paragraph -->\n<p>Hello from first imported post !</p>\n<!-- /wp:paragraph -->"
},
{
"wp:post_id": 2,
"title": "About",
"wp:post_content": "<!-- wp:paragraph -->\n<p>Hello from \"second imported post\" !</p>\n<!-- /wp:paragraph -->"
}
]
}
}
119 changes: 119 additions & 0 deletions docs/gh-pages/src/jsonschema/slice-content.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
{
"$schema": "http://json-schema.org/draft-07/schema",
"title": "Impex Content Slice",
"description": "a Impex slice containing content",
"type": "object",
"properties": {
"$schema": {
"type": "string",
"description": "The JSON schema for this slice"
},
"version": {
"title": "Impex Provider content version",
"description": "Version will be used by Impex to know what data format to expect",
"const": "1.0.0"
},
"type": {
"title": "Impex slice type",
"description": "Value will be used by Impex to know the content type of the slice",
"const": "php"
},
"tag": {
"title": "Impex slice tag",
"description": "The Impex slice tag contains information about the responsible Impex provider for this slice",
"const": "content-exporter"
},
"meta": {
"title": "Metadata for the slice",
"description": "Metadata is a JSON object used to store additional information about the slice",
"const": {
"entity": "content-exporter"
}
},
"data": {
"type": "object",
"properties": {
"posts": {
"type": "array",
"items": {
"$ref": "#/definitions/posts-item"
},
"minItems": 1,
"$comment": "@TODO: unique ids are not yet supported by jsonschema",
"uniqueItems": true
}
},
"title": "Data portion of this Impex slice",
"description": "data contains the real Impex data.",
"required": ["posts"]
}
},
"additionalProperties": false,
"required": ["version", "type", "tag", "meta", "data"],
"definitions": {
"posts-item": {
"title": "WordPress posts stored in this this Impex slice",
"type": "object",
"properties": {
"wp:post_id": {
"type": "integer",
"minimum": 1,
"title": "WordPress post_id",
"description": "The unique WordPress post id of the post"
},
"title": {
"type": "string",
"minLength": 1,
"title": "WordPress post title",
"description": "The title of the WordPress post as it is stored in the database"
},
"wp:post_content": {
"type": "string",
"minLength": 1,
"title": "WordPress post content",
"description": "The content of the WordPress post",
"examples": [
"<!-- wp:paragraph -->\n<p>Hello from first imported post !</p>\n<!-- /wp:paragraph -->",
"<!-- wp:paragraph -->\n<p>Hello world</p>\n<!-- /wp:paragraph -->\n\n<!-- wp:html -->\n<p>A bit of custom html utilizing the Gutenberg html block</p>\n<ul>\n <li>hi</li>\n <li>ho</li>\n <li>howdy</li>\n</ul><!-- /wp:html -->"
]
},
"wp:post_type": {
"title": "WordPress post type",
"description": "The type of the WordPress post.\nCcontent related post types are 'post' and 'page'.\nIf not declared, type 'post' will be assumed.",
"type": "string",
"enum": [
"post",
"page",
"nav_menu_item",
"wp_template",
"wp_template_part",
"wp_block",
"wp_global_styles"
],
"default": "page"
},
"wp:status": {
"type": "string",
"title": "WordPress post status",
"description": "The WordPress post status (https://wordpress.org/support/article/post-status/)",
"enum": ["publish", "future", "draft", "pending", "private"],
"default": "draft"
},
"wp:post_name": {
"type": "string",
"title": "WordPress post slug",
"description": "Used to generate the permalink. If not given, the sanitized post title will be used instead.",
"minLength": 1
},
"wp:post_parent": {
"type": "integer",
"minimum": 1,
"title": "WordPress post parent id",
"description": "The WordPress post id of the parent post.\n If not given, the post will be created as a top level post."
}
},
"required": ["wp:post_id", "title", "wp:post_content"],
"additionalProperties": false
}
}
}
223 changes: 223 additions & 0 deletions docs/gh-pages/src/migrating-content.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
<!-- toc -->

# Migrating content

Migrating existing content into WordPress is a very common task.

Impex provides tooling support for migrating your content to WordPress.

## Preparation

Impex imports data from a directory containing JSON files grouped in `chunk-\*` sub directories.

```
my-exported-website
├── chunk-0001
│   ├── slice-0001.json
│   ├── slice-0002.json
│   ├── slice-0003.json
│   ├── slice-0004.json
│   └── slice-0005.json
├── chunk-0002
│ ├── slice-0001.json
│ ├── slice-0001-wes-walker-unsplash.jpg
│ ├── slice-0002-greysen-johnson-unsplash.jpg
│ ├── slice-0002.json
│ ├── slice-0003-james-wheeler-unsplash.jpg
│ └── slice-0003.json
...
```

> Why that _chunk-\*_ sub directory structure ?
>
> Organizing thousands of content documents and hundreds of images/videos in a single directory slows down file managers like Windows Explorer. That's the one and only reason for `chunk-\*` sub directories.
Both _chunk-\*_ sub directories and the JSON files are suffixed by a 4 digit number.

Impex imports slice files ordered by name. So the slices in sub directory `chunk-0001` will be imported first, then the slices in `chunk-0002` and so on.

Same rule for `slice-*.json` files within the same `chunk-\*` sub directory : `slice-0001.json` will be imported before `slice-0002.json` and so on.

> Know that import order is important. If you import content referencing images/videos in the wrong order, you will get broken links. Impex will rewrite/fix media links in the content if you **import content as first and media afterwards.**
Have a look at this [sample Impex export](https://github.com/IONOS-WordPress/cm4all-wp-impex/tree/develop/impex-cli/tests/fixtures/simple-import) provided by the Impex plugin to get a clue about a minimal working impex export containing content and referencing images.

## Data files

`slice-*.json` files are JSON files containing data.

The real data is stored in the `data` property.

The data might be anything expressed in textual form. Beside the data itself, each `slice-*.json` file contains some json data describing the data so that Impex knows how to import.

An minimal slice file transporting a single Wordpress post looks like this:

```json
{
"version": "1.0.0",
"type": "php",
"tag": "content-exporter",
"meta": {
"entity": "content-exporter"
},
"data": {
"posts": [
{
"wp:post_id": 1,
"wp:post_content": "<!-- wp:paragraph -->\n<p>Hello from first imported post !</p>\n<!-- /wp:paragraph -->",
"title": "Hello first post!"
}
]
}
}
```

As you can see the real content is located in the `data` property.

Everything except the `data` property are used for versioning and content identification.

### Content (aka WordPress posts/pages)

Content slice files wrap regular WordPress _posts_ and _pages_.

Content slices may also transport further content like comments, custom fields, terms, taxonomies, categories, FSE templates/template-parts, global styles and so on. But that's another story.

> To get a clue about the power of content slices by exporting a FSE enabled WordPress instance and inspecting the resulting `slice-_.json` files.
Below is a [JSONschema](https://json-schema.org/) describing the content slice file format.

Download [JSONschema](https://json-schema.org/) definition for content slices : [slice-content.json](https://github.com/IONOS-WordPress/cm4all-wp-impex/tree/develop/docs/gh-pages/src/jsonschema/slice-content.json)

```json
{{#include ./jsonschema/slice-content.json}}
```

A content slice may contain any number of WordPress posts/pages/etc.

> When generating a content slice file, it's best to embed only a single page/post per `slice-_.json` file
Each content document is identified by a unique `wp:post_id` property.

The `title` property is used as the title.

`wp:post_content` transports the content.

_See the Content slice [JSONSchema definition](https://github.com/IONOS-WordPress/cm4all-wp-impex/tree/develop/docs/gh-pages/src/jsonschema/slice-content.json) for all supported properties._

Since WordPress expects [block-annotated HTML](https://wordpress.com/support/wordpress-editor/blocks/) you need to transform your HTML content into [block-annotated HTML](https://wordpress.com/support/wordpress-editor/blocks/).

There are 2 options to do that :

- **The gold solution** : annotate almost every HTML tag with the matching [Gutenberg block](https://wordpress.com/support/wordpress-editor/blocks/custom-html-block/).

```html
<!-- wp:paragraph -->
<p>A bit of custom html utilizing the Gutenberg html block</p>
<!-- /wp:paragraph -->

<!-- wp:list -->
<ul>
<li>hi</li>
<li>ho</li>
<li>howdy</li>
</ul>
<!-- /wp:list -->

<!-- wp:image -->
<figure class="wp-block-image">
<img src="./greysen-johnson-unsplash.jpg" />
<figcaption>Fly fishing</figcaption>
</figure>
<!-- /wp:image -->
```

- the quick and dirty solution : wrap the whole html content into a WordPress [Custom HTML block](https://wordpress.com/support/wordpress-editor/blocks/custom-html-block/) :

```html
<!-- wp:html -->
<p>A bit of custom html utilizing the Gutenberg html block</p>
<ul>
<li>hi</li>
<li>ho</li>
<li>howdy</li>
</ul>
<figure>
<img src="./greysen-johnson-unsplash.jpg" />
<figcaption>Fly fishing</figcaption>
</figure>
<!-- /wp:html -->
```

_Why is this solution dirty ?_

_=> If you open up a page/post containing a - the quick and dirty solution : wrap the whole html content into a WordPress [Custom HTML block](https://wordpress.com/support/wordpress-editor/blocks/custom-html-block/) in the Gutenberg editor, you will see just the HTML content but its not rendered_. So the quick and dirty solution is actually a no-go from a designers perspective.

The HTML content must be encoded as JSON string in the slice file. See [this example content slice](https://github.com/IONOS-WordPress/cm4all-wp-impex/blob/develop/impex-cli/tests/fixtures/simple-import/chunk-0001/slice-0005.json).

See [Attachments (like Pictures and Videos)](#attachments-like-pictures-and-videos) for importing referenced media files.

### Attachments (like Pictures and Videos)

Attachments a binary files like images/videos or anything else stored in the WordPress `uploads` directory.

Such binary data is handled a bit differently than textual - because it cannot be easily embedded into a JSON file.

Below is a [JSONschema](https://json-schema.org/) describing the attachment slice file format.

Download [JSONschema](https://json-schema.org/) definition for media files : [slice-attachment.json](https://github.com/IONOS-WordPress/cm4all-wp-impex/tree/develop/docs/gh-pages/src/jsonschema/slice-content.json)

```json
{{#include ./jsonschema/slice-attachment.json}}
```

Let's say you have a reference to an image in your content :

```html
<img src="./greysen-johnson-unsplash.jpg" />
```

So you need to import the image into your WordPress instance. To do so, you need to

- create a `slice-*json` file (let's name it `slice-0002.json`) declaring the attachment :

```json
{
"version": "1.0.0",
"type": "resource",
"tag": "attachment",
"meta": {
"entity": "attachment"
},
"data": "./greysen-johnson-unsplash.jpg"
}
```

As you can see, there is actually only the `data` property referencing the image. Rest of the slice file is just boilerplate code.

- provide the image in the same chunk directory as it's slice json file and **prefixed** with the slice json file name (`slice-0002.json`) :

```
slice-0002-greysen-johnson-unsplash.jpg
```

Thats it ! If you import the slice file using Impex, the image will appear in the WordPress `uploads` directory and in the WordPress media page. If you referenced the image in your content, it will also appear in your imported pages/posts.

> Remember: Content slices referencing media files should **ALWAYS** be imported **before** the attachment slices.
>
> This can be achieved by naming content slicing with a lower number than the media slices or - much simpler - keeping the content slices in a lower numbered `chunk-*` directory than the attachments.
>
> See [simple-import](https://github.com/IONOS-WordPress/cm4all-wp-impex/tree/develop/impex-cli/tests/fixtures/simple-import) example for a full featured generated import at the [Impex Github page](https://github.com/IONOS-WordPress/cm4all-wp-impex).

### Other data

Although Impex provides a simple way to import content and media, you may also want to import more advanced data like database tables or settings into WordPress.

Impex can import

- relational data like database tables

- key/value based settings

into WordPress.

@TODO: Add JSONSchema / examples for other data.
139 changes: 84 additions & 55 deletions impex-cli/impex-cli.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,71 +36,98 @@ function rmdir_r($dir)
}
}

function _die($options, $message, ...$args)
class DieException extends \RuntimeException
{
fprintf(STDERR, $message, ...array_map(fn ($_) => is_array($_) || is_object($_) ? json_encode($_) : $_, $args));
exit(1);
function __construct($message, $code = 0, \Exception $previous = null)
{
parent::__construct($message, $code, $previous);
}
}

function main($argv)
function _die($options, $message, ...$args)
{
$argc = count($argv);
$message = sprintf($message, ...array_map(fn ($_) => is_array($_) || is_object($_) ? json_encode($_) : $_, $args));

$operation = 'help';
$options = ['header' => []];
$arguments = [];
throw new DieException($message, 1);
}

switch ($argc) {
case 1:
break;
default:
switch ($argv[1]) {
case 'import':
case 'export':
case 'export-profile':
case 'import-profile':
$operation = $argv[1];
_parseCommandlineArguments(array_slice($argv, 2), $options, $arguments);

[$result, $status, $error] = _curl(
$options,
"",
null,
fn ($curl) => curl_setopt($curl, \CURLOPT_URL, $options['rest-url'])
);


// ensure we can connect to the wordpress rest api
if ($error) {
_die(
function main($argv)
{
try {
$argc = count($argv);

$operation = 'help';
$options = ['header' => []];
$arguments = [];

switch ($argc) {
case 1:
break;
default:
switch ($argv[1]) {
case 'import':
case 'export':
case 'export-profile':
case 'import-profile':
$operation = $argv[1];
_parseCommandlineArguments(array_slice($argv, 2), $options, $arguments);

[$result, $status, $error] = _curl(
$options,
"Could not connect to wordpress rest endpoint(='%s'): %s\nEnsure param '-rest-url' is correct.\nCheck wordpress rest api is enabled.\n",
$options['rest-url'],
$error
"",
null,
fn ($curl) => curl_setopt($curl, \CURLOPT_URL, $options['rest-url'])
);
} else {
// ensure impex plugin rest namespace is available
if (!in_array('cm4all-wp-impex/v1', $result['namespaces'])) {


// ensure we can connect to the wordpress rest api
if ($error) {
_die(
$options,
"Wordpress plugin cm4all-wp-impex seems not to be installed in the wordpress instance - expected rest endpoint(='cm4all-wp-impex/v1') is not available : Available rest endpoints %s'\n",
json_encode($result['namespaces'])
"Could not connect to wordpress rest endpoint(='%s'): %s\nEnsure param '-rest-url' is correct.\nCheck wordpress rest api is enabled.\n",
$options['rest-url'],
$error
);
} else {
if (is_string($result) && $status === 301) {
_die(
$options,
"Wordpress JSON Rest API Endpoint is probably misconfigured - request returned(http status='%s') : %s'\n",
$status,
'HTTP/1.1 301 Moved Permanently'
);
} else {
// ensure impex plugin rest namespace is available
if (!in_array('cm4all-wp-impex/v1', $result['namespaces'])) {
_die(
$options,
"Wordpress plugin cm4all-wp-impex seems not to be installed in the wordpress instance - expected rest endpoint(='cm4all-wp-impex/v1') is not available : Available rest endpoints %s'\n",
json_encode($result['namespaces'])
);
}
}
}
}

break;
case 'help':
case '--help':
break;
default:
$arguments[] = sprintf("Invalid option(s): %s", join(' ', array_slice($argv, 1)));
}
};
break;
case 'help':
case '--help':
break;
default:
_die($options, "Invalid operation: %s", join(' ', array_slice($argv, 1)));
}
};

[$json, $status] = call_user_func(__NAMESPACE__ . '\\' . str_replace(['-'], ['_'], $operation), $options, ...$arguments);
if ($status === 200) {
echo json_encode($json, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) . PHP_EOL;
[$json, $status] = call_user_func(__NAMESPACE__ . '\\' . str_replace(['-'], ['_'], $operation), $options, ...$arguments);
if ($status === 200) {
echo json_encode($json, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) . PHP_EOL;
}
} catch (DieException $ex) {
if (php_sapi_name() === "cli" && !str_ends_with($_SERVER['argv'][0], 'phpunit')) {
fprintf(STDERR, $ex->getMessage());
exit(1);
} else {
throw $ex;
}
}
}

Expand Down Expand Up @@ -142,8 +169,7 @@ function _parseCommandlineArguments($argv, &$options, &$arguments)
if (isset($options['username']) && isset($options['password'])) {
foreach ($options['header'] as $header) {
if (preg_match('/^authorization: Basic$/', $header, $matches)) {
fprintf(STDERR, "Can't use both username and password options and Authorization header\n");
exit(1);
_die($options, "Can't use both username and password options and Authorization header\n");
}
}

Expand Down Expand Up @@ -250,7 +276,7 @@ function _curl($options, string $endpoint, $method = null, $callback = __NAMESPA
if (((string)$http_status)[0] !== '2') {
_log(
$options,
"accessing '%s' returned http statuscode(=%s) : %s",
"accessing '%s' returned http status code(=%s) : %s",
curl_getinfo($curl, CURLINFO_EFFECTIVE_URL),
$http_status,
curl_error($curl),
Expand Down Expand Up @@ -590,7 +616,10 @@ function _saveSlicesChunk($options, $export_directory, $response, $chunk)
]);
*/

main($argv);
// if we are running in cli mode and not as phpunit test then run main
if (php_sapi_name() === "cli" && !str_ends_with($_SERVER['argv'][0], 'phpunit')) {
main($argv);
}

/*
main([
Expand Down
1 change: 1 addition & 0 deletions impex-cli/tests/.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
*
91 changes: 91 additions & 0 deletions impex-cli/tests/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# PHPUnit Docker Container.
FROM alpine:latest as builder
LABEL mantainer="Lars Gersmann <lars.gersmann@cm4all.com>"

WORKDIR /tmp

RUN apk --no-cache add \
bash \
ca-certificates \
curl \
git \
php8 \
php8-bcmath \
php8-ctype \
php8-curl \
php8-dom \
php8-exif \
php8-fileinfo \
php8-json \
php8-mbstring \
php8-pecl-mcrypt \
php8-mysqli \
php8-opcache \
php8-openssl \
php8-pcntl \
php8-pdo \
php8-pdo_mysql \
php8-pdo_pgsql \
php8-pdo_sqlite \
php8-phar \
php8-session \
php8-simplexml \
php8-soap \
php8-tokenizer \
php8-xdebug \
php8-xml \
php8-xmlreader \
php8-xmlwriter \
php8-zip \
php8-zlib \
unzip

# composer expects command "php" available in path
RUN ln -s /usr/bin/php8 /usr/bin/php

# install composer
RUN wget https://getcomposer.org/installer -O composer-setup.php && php composer-setup.php --install-dir=/usr/bin --filename=composer && rm composer-setup.php

# install phpunit
RUN composer require "phpunit/phpunit:^9.5" --prefer-source --no-interaction
# RUN composer require "phpunit/php-invoker" --prefer-source --no-interaction
RUN ln -s /tmp/vendor/bin/phpunit /usr/local/bin/phpunit

# Enable X-Debug
RUN sed -i 's/\;z/z/g' /etc/php8/conf.d/*xdebug.ini
# RUN php -m | grep -i xdebug

# pear support
RUN wget https://pear.php.net/go-pear.phar -O go-pear.phar && echo 'Y' | php go-pear.phar && rm go-pear.phar
# add your pear packages to be installed using --build-arg in the docker run command
ARG PEAR_PACKAGES=""
ONBUILD RUN \
{ \
[ "${PEAR_PACKAGES}" != "" ]; \
} || exit 0 && pear install ${PEAR_PACKAGES}

# see https://phpunit.readthedocs.io/en/9.5/installation.html#recommended-php-configuration
RUN echo $'memory_limit=-1\n\
error_reporting=-1\n\
log_errors_max_len=0\n\
zend.assertions=1\n\
assert.exception=1\n\
xdebug.show_exception_trace=0\n' >> /etc/php8/php.ini

#####################################################################################################

FROM alpine:latest
# simple trick to get rid of intermediate layers
# https://stackoverflow.com/questions/56117261/how-to-merge-dockers-layers-of-image-and-slim-down-the-image-file
COPY --from=builder / /

VOLUME ["/workdir"]
WORKDIR /workdir/tests

# add a entrypoint script to enable CTRL-C abortion in terminal
# (see https://stackoverflow.com/a/57526365/1554103)
# RUN echo "#!/bin/bash" > /usr/local/bin/docker-entrypoint.sh && \
# echo '$@' >> /usr/local/bin/docker-entrypoint.sh && \
# chmod a+x /usr/local/bin/docker-entrypoint.sh

# ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"]
25 changes: 25 additions & 0 deletions impex-cli/tests/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# About

a tiny (alpine based) docker image for phpunit testing.

## Features

- php 8

- including xdebug support

- phpunit 9.5

# Development

build : `DOCKER_BUILDKIT=1 docker build -t cm4all-wp-impex/impex-cli-phpunit .`

- (optional) you can add `--build-arg PEAR_PACKAGES="<space-separated-list-of-pear-packages>"` to auto install pear packages into the image

jump into : `docker run -ti --rm cm4all-wp-impex/impex-cli-phpunit bash`

php.ini : `/etc/php8/php.ini`

# Usage

`docker run --add-host=host.docker.internal:host-gateway -it -v $(pwd)/..:/workdir --rm cm4all-wp-impex/impex-cli-phpunit phpunit .`
49 changes: 49 additions & 0 deletions impex-cli/tests/abstract-impex-cli-testcase.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<?php

namespace cm4all\wp\impex\tests\phpunit;

use PHPUnit\Framework\TestCase;

require_once __DIR__ . '/../impex-cli.php';

function impex_cli($operation, ...$args): array
{
$retval = [
'stderr' => '',
'stdout' => '',
'exit_code' => 0,
];

ob_start();

try {
\cm4all\wp\impex\cli\main([
realpath(__DIR__ . '/../impex-cli/impex-cli.php'),
$operation,
'-rest-url=http://host.docker.internal:8889/wp-json',
'-username=admin',
'-password=password',
...$args,
]);
} catch (\cm4all\wp\impex\cli\DieException $ex) {
$retval['stderr'] = $ex->getMessage();
$retval['exit_code'] = $ex->getCode();
} finally {
$retval['stdout'] = ob_get_clean();
}

return $retval;
}

abstract class AbstractImpexCLITestCase extends TestCase
{
function setUp(): void
{
parent::setUp();
}

function tearDown(): void
{
parent::tearDown();
}
}
Empty file.
17 changes: 17 additions & 0 deletions impex-cli/tests/fixtures/simple-import/chunk-0001/slice-0001.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"version": "1.0.0",
"type": "php",
"tag": "content-exporter",
"meta": {
"entity": "content-exporter"
},
"data": {
"posts": [
{
"wp:post_id": 1,
"wp:post_content": "<!-- wp:paragraph -->\n<p>Hello from first imported post !</p>\n<!-- /wp:paragraph -->",
"title": "Hello first post!"
}
]
}
}
20 changes: 20 additions & 0 deletions impex-cli/tests/fixtures/simple-import/chunk-0001/slice-0002.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"version": "1.0.0",
"type": "php",
"tag": "content-exporter",
"meta": {
"entity": "content-exporter"
},
"data": {
"posts": [
{
"wp:status": "publish",
"wp:post_type": "page",
"wp:post_name": "hello-world",
"wp:post_id": 2,
"wp:post_content": "<!-- wp:paragraph -->\n<p>Hello from import !</p>\n<!-- /wp:paragraph -->",
"title": "Hello world!"
}
]
}
}
20 changes: 20 additions & 0 deletions impex-cli/tests/fixtures/simple-import/chunk-0001/slice-0003.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"version": "1.0.0",
"type": "php",
"tag": "content-exporter",
"meta": {
"entity": "content-exporter"
},
"data": {
"posts": [
{
"wp:status": "publish",
"wp:post_type": "page",
"wp:post_name": "page-with-image",
"wp:post_id": 3,
"wp:post_content": "<!-- wp:paragraph -->\n<p>Hello world</p>\n<!-- /wp:paragraph -->\n<!-- wp:image --><figure class=\"wp-block-image\"><img src=\"./wes-walker-unsplash.jpg\"/></figure><!-- /wp:image -->",
"title": "A page with an image block"
}
]
}
}
20 changes: 20 additions & 0 deletions impex-cli/tests/fixtures/simple-import/chunk-0001/slice-0004.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"version": "1.0.0",
"type": "php",
"tag": "content-exporter",
"meta": {
"entity": "content-exporter"
},
"data": {
"posts": [
{
"wp:status": "publish",
"wp:post_type": "page",
"wp:post_name": "page-with-gallery-multiple-images",
"wp:post_id": 4,
"wp:post_content": "<!-- wp:paragraph -->\n<p>Hello world</p>\n<!-- /wp:paragraph -->\n<!-- wp:gallery -->\n<figure class=\"wp-block-gallery has-nested-images columns-default is-cropped\">\n <!-- wp:image -->\n <figure class=\"wp-block-image\"><img src=\"./james-wheeler-unsplash.jpg\"/></figure>\n <!-- /wp:image -->\n\n <!-- wp:image -->\n <figure class=\"wp-block-image\"><img src=\"./greysen-johnson-unsplash.jpg\"/></figure>\n <!-- /wp:image -->\n\n <!-- wp:image -->\n <figure class=\"wp-block-image\"><img src=\"./wes-walker-unsplash.jpg\"/></figure>\n <!-- /wp:image -->\n</figure>\n<!-- /wp:gallery -->",
"title": "A page with gallery block."
}
]
}
}
20 changes: 20 additions & 0 deletions impex-cli/tests/fixtures/simple-import/chunk-0001/slice-0005.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"version": "1.0.0",
"type": "php",
"tag": "content-exporter",
"meta": {
"entity": "content-exporter"
},
"data": {
"posts": [
{
"wp:status": "publish",
"wp:post_type": "page",
"wp:post_name": "page-with-custom-html-including-media",
"wp:post_id": 5,
"wp:post_content": "<!-- wp:paragraph -->\n<p>Hello world</p>\n<!-- /wp:paragraph -->\n\n<!-- wp:html -->\n<p>A bit of custom html utilizing the Gutenberg html block</p>\n<ul>\n <li>hi</li>\n <li>ho</li>\n <li>howdy</li>\n</ul>\n<figure class=\"wp-block-image\">\n<img src=\"http://localhost:8889/wp-content/uploads/2022/03/greysen-johnson-unsplash.jpg\" alt=\"\"> <figcaption>Fly fishing</figcaption>\n</figure>\n<!-- /wp:html -->",
"title": "A page using Gutenberg HTML block."
}
]
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"version": "1.0.0",
"type": "resource",
"tag": "attachment",
"meta": {
"entity": "attachment"
},
"data": "./wes-walker-unsplash.jpg"
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"version": "1.0.0",
"type": "resource",
"tag": "attachment",
"meta": {
"entity": "attachment"
},
"data": "./greysen-johnson-unsplash.jpg"
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"version": "1.0.0",
"type": "resource",
"tag": "attachment",
"meta": {
"entity": "attachment"
},
"data": "./james-wheeler-unsplash.jpg"
}
21 changes: 21 additions & 0 deletions impex-cli/tests/phpunit.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<phpunit
backupGlobals="false"
colors="true"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
cacheResult="false"
>
<testsuites>
<testsuite name="impex-cli">
<directory
phpVersion="8.0.0"
phpVersionOperator=">="
prefix="test-"
suffix=".php"
>
./
</directory>
</testsuite>
</testsuites>
</phpunit>
25 changes: 25 additions & 0 deletions impex-cli/tests/test-import-profile.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php

use cm4all\wp\impex\tests\phpunit\AbstractImpexCLITestCase;

use function cm4all\wp\impex\tests\phpunit\impex_cli;

require_once __DIR__ . "/abstract-impex-cli-testcase.php";

final class ImportProfileTest extends AbstractImpexCLITestCase
{
function testImportProfiles()
{
// curl -v 'http://host.docker.internal:8889/wp-json/cm4all-wp-impex/v1/export/profile' -H 'accept: application/json' -H 'authorization: Basic YWRtaW46cGFzc3dvcmQ='

$result = impex_cli(
'import-profile',
// '-CURLOPT_VERBOSE',
// '-verbose',
// __DIR__ . '/fixtures/impex-cli/simple-snapshot',
);

$this->assertEquals('', $result['stderr'], 'stderr should be empty');
$this->assertEquals(0, $result['exit_code'], 'should succeed');
}
}
43 changes: 43 additions & 0 deletions impex-cli/tests/test-import.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?php

use cm4all\wp\impex\tests\phpunit\AbstractImpexCLITestCase;

use function cm4all\wp\impex\tests\phpunit\impex_cli;

require_once __DIR__ . "/abstract-impex-cli-testcase.php";
final class ImportTest extends AbstractImpexCLITestCase
{
function testInvalidOptions()
{
$result = impex_cli(
'-overwrite',
'/tmp/test-export.zip',
);

$this->assertEquals(1, $result['exit_code'], 'should fail because of misplaced options/arguments');
}

function testImportEmptySnapshotDirectory()
{
$result = impex_cli(
'import',
'-overwrite',
'-profile=all',
__DIR__ . '/fixtures/empty-snapshot',
);

$this->assertEquals(0, $result['exit_code'], 'should succeed');
}

function testSimpleImport()
{
$result = impex_cli(
'import',
'-overwrite',
'-profile=all',
__DIR__ . '/fixtures/simple-import',
);

$this->assertEquals(0, $result['exit_code'], 'should succeed');
}
}
4 changes: 2 additions & 2 deletions package-lock.json
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "cm4all-wp-impex",
"version": "1.2.0",
"version": "1.2.1",
"scripts": {
"dev": "make dev",
"build": "make",
Expand Down
2 changes: 1 addition & 1 deletion plugins/cm4all-wp-impex-example/plugin.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* Plugin Name: cm4all-wp-impex-example
* Plugin URI: http://dev.intern.cm-ag/trinity/research/cm4all-wp-impex
* Description: Example Plugin contributing additional Importer/Exporter facilities to Impex Plugin
* Version: 1.2.0
* Version: 1.2.1
* Tags: import, export, migration
* Requires PHP: 8.0
* Requires at least: 5.7
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ public function get_item_slices($request)
global $wpdb;

$rows = $wpdb->get_results(
$wpdb->prepare("SELECT * from {$wpdb->prefix}" . ImpexExport::DB_CHUNKS_TABLENAME . ' WHERE export_id=%s ORDER BY position LIMIT %d OFFSET %d', $id, $per_page, $page * $per_page + $offset)
$wpdb->prepare("SELECT * from {$wpdb->prefix}" . Impex::DB_SNAPSHOTS_TABLENAME . ' WHERE snapshot_id=%s ORDER BY position LIMIT %d OFFSET %d', $id, $per_page, $page * $per_page + $offset)
);
$data = [];
foreach ($rows as $row) {
Expand All @@ -128,7 +128,7 @@ public function get_item_slices($request)
}

$total = absint($wpdb->get_var(
$wpdb->prepare("SELECT COUNT(*) from {$wpdb->prefix}" . ImpexExport::DB_CHUNKS_TABLENAME . ' WHERE export_id=%s', $id)
$wpdb->prepare("SELECT COUNT(*) from {$wpdb->prefix}" . Impex::DB_SNAPSHOTS_TABLENAME . ' WHERE snapshot_id=%s', $id)
));
$total_pages = ceil($total / $per_page);

Expand Down
82 changes: 6 additions & 76 deletions plugins/cm4all-wp-impex/inc/class-impex-export.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,21 +15,10 @@

abstract class ImpexExport extends ImpexPart
{
const DB_CHUNKS_TABLENAME = 'impex_export_chunks';
const WP_OPTION_EXPORTS = 'impex_exports';
const WP_FILTER_SLICE_SERIALIZE = 'impex_export_filter_serialize';
const WP_FILTER_SLICE_DESERIALIZE = 'impex_export_filter_deserialize';

protected string $_db_chunks_tablename;

public function __construct()
{
parent::__construct();

global $wpdb;
$this->_db_chunks_tablename = $wpdb->prefix . self::DB_CHUNKS_TABLENAME;
}

protected function _createProvider(string $name, callable $cb): ImpexExportProvider
{
return new class($name, $cb) extends ImpexExportProvider
Expand All @@ -52,65 +41,6 @@ public function __construct($name, $context)
};
}

public function __install(string|bool $installed_version): bool
{
global $wpdb;

if ($installed_version === false) {
// plugin was newly installed
$charset_collate = $wpdb->get_charset_collate();

$sql = "CREATE TABLE {$this->_db_chunks_tablename} (
id mediumint(9) NOT NULL AUTO_INCREMENT,
export_id CHAR(36) NOT NULL,
position mediumint(9) NOT NULL,
slice json NOT NULL,
PRIMARY KEY (id)
) $charset_collate;";

require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
\dbDelta($sql);
} else if ($installed_version !== Impex::VERSION) {
// new plugin version is now installed, try to upgrade
/*
$sql = "CREATE TABLE {$this->_db_chunks_tablename} (
id mediumint(9) NOT NULL AUTO_INCREMENT,
time datetime DEFAULT '0000-00-00 00:00:00' NOT NULL,
name tinytext NOT NULL,
text text NOT NULL,
url varchar(100) DEFAULT '' NOT NULL,
PRIMARY KEY (id)
);";
require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
dbDelta($sql);
*/
}

return $this->__install_data($installed_version);
}

protected function __install_data(string|bool $installed_version): bool
{
/*
global $wpdb;
$welcome_name = 'Mr. WordPress';
$welcome_text = 'Congratulations, you just completed the installation!';
$wpdb->insert(
$this->_db_chunks_tablename,
[
'time' => current_time('mysql'),
'name' => $welcome_name,
'text' => $welcome_text,
]
);
*/

return true;
}

function extract(ImpexExportTransformationContext $transformationContext): \Generator
{
foreach ($transformationContext->profile->getTasks() as $task) {
Expand Down Expand Up @@ -152,7 +82,7 @@ function save(ImpexExportProfile $profile, array $options = [], string $name =
$this->_db_chunks_tablename,
[
'position' => $position,
'export_id' => $transformationContext->id,
'snapshot_id' => $transformationContext->id,
'slice' => $json,
]
);
Expand All @@ -171,11 +101,11 @@ function save(ImpexExportProfile $profile, array $options = [], string $name =
return $transformationContext;
}

function update(string $export_id, array $data): array|bool
function update(string $snapshot_id, array $data): array|bool
{
$exports = \get_option(self::WP_OPTION_EXPORTS, []);
foreach ($exports as &$export) {
if ($export['id'] === $export_id) {
if ($export['id'] === $snapshot_id) {
foreach ($data as $key => $value) {
// prevent updating 'id', 'options', 'profile', 'user', 'created'
if (!in_array($key, ['id', 'options', 'profile', 'user', 'created'])) {
Expand All @@ -196,12 +126,12 @@ function update(string $export_id, array $data): array|bool
return false;
}

function remove(string $export_id): bool|array
function remove(string $snapshot_id): bool|array
{
/* @var array<array> */
$exports = \get_option(self::WP_OPTION_EXPORTS, []);
foreach ($exports as $index => $export) {
if ($export['id'] === $export_id) {
if ($export['id'] === $snapshot_id) {
$transformationContext = ImpexExportTransformationContext::fromJson($export);

global $wpdb;
Expand All @@ -210,7 +140,7 @@ function remove(string $export_id): bool|array
\WP_Filesystem();

// remove matching export table rows
$rowsDeleted = $wpdb->delete($this->_db_chunks_tablename, ['export_id' => $transformationContext->id,]);
$rowsDeleted = $wpdb->delete($this->_db_chunks_tablename, ['snapshot_id' => $transformationContext->id,]);
if ($rowsDeleted === false) {
throw new ImpexExportRuntimeException(sprintf('failed to delete jsonized slices from database : %s', $wpdb->last_error));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -160,9 +160,9 @@ public function upsert_item_slice($request)
global $wpdb;

/** @var string */
$import_id = $request->get_param('id');
$snapshot_id = $request->get_param('id');
foreach (\get_option(ImpexImport::WP_OPTION_IMPORTS, []) as $import) {
if ($import['id'] === $import_id) {
if ($import['id'] === $snapshot_id) {
$transformationContext = ImpexImportTransformationContext::fromJson($import);

$position = $request->get_param('position');
Expand All @@ -175,7 +175,7 @@ public function upsert_item_slice($request)

$slice = \apply_filters(self::WP_FILTER_IMPORT_REST_SLICE_UPLOAD, $slice, $transformationContext, $request);

$success = Impex::getInstance()->Import->_upsert_slice($import_id, $position, $slice);
$success = Impex::getInstance()->Import->_upsert_slice($snapshot_id, $position, $slice);

if ($success === false) {
throw new ImpexImportRuntimeException(sprintf('failed to insert/update jsonized slice(=%s) to database : %s', $slice, $wpdb->last_error));
Expand All @@ -192,14 +192,14 @@ public function upsert_item_slice($request)
public function consume($request)
{
//$profile = Impex::getInstance()->Import->getProfile($request->get_param('profile'));
$import_id = $request->get_param('id');
$snapshot_id = $request->get_param('id');
//$options = $request->get_json_params();

$limit = $request->get_param('limit');
$offset = $request->get_param('offset');

foreach (\get_option(ImpexImport::WP_OPTION_IMPORTS, []) as $import) {
if ($import['id'] === $import_id) {
if ($import['id'] === $snapshot_id) {
$transformationContext = ImpexImportTransformationContext::fromJson($import);

$notConsumedSlices = Impex::getInstance()->Import->consume($transformationContext, $limit, $offset);
Expand Down
98 changes: 14 additions & 84 deletions plugins/cm4all-wp-impex/inc/class-impex-import.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,21 +15,10 @@
require_once __DIR__ . '/class-impex-import-runtime-exception.php';
abstract class ImpexImport extends ImpexPart
{
const DB_CHUNKS_TABLENAME = 'impex_import_chunks';
const WP_OPTION_IMPORTS = 'impex_imports';

const EVENT_IMPORT_END = 'cm4all_wp_import_end';

protected string $_db_chunks_tablename;

public function __construct()
{
parent::__construct();

global $wpdb;
$this->_db_chunks_tablename = $wpdb->prefix . self::DB_CHUNKS_TABLENAME;
}

protected function _createProvider(string $name, callable $cb): ImpexImportProvider
{
return new class($name, $cb) extends ImpexImportProvider
Expand All @@ -52,66 +41,7 @@ public function __construct($name, $context)
};
}

public function __install(string|bool $installed_version): bool
{
global $wpdb;

if ($installed_version === false) {
// plugin was newly installed
$charset_collate = $wpdb->get_charset_collate();

$sql = "CREATE TABLE {$this->_db_chunks_tablename} (
id mediumint(9) NOT NULL AUTO_INCREMENT,
import_id CHAR(36) NOT NULL,
position mediumint(9) NOT NULL,
slice json NOT NULL,
PRIMARY KEY (id)
) $charset_collate;";

require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
\dbDelta($sql);
} else if ($installed_version !== Impex::VERSION) {
// new plugin version is now installed, try to upgrade
/*
$sql = "CREATE TABLE {$this->_db_chunks_tablename} (
id mediumint(9) NOT NULL AUTO_INCREMENT,
time datetime DEFAULT '0000-00-00 00:00:00' NOT NULL,
name tinytext NOT NULL,
text text NOT NULL,
url varchar(100) DEFAULT '' NOT NULL,
PRIMARY KEY (id)
);";
require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
dbDelta($sql);
*/
}

return $this->__install_data($installed_version);
}

protected function __install_data(string|bool $installed_version): bool
{
/*
global $wpdb;
$welcome_name = 'Mr. WordPress';
$welcome_text = 'Congratulations, you just completed the installation!';
$wpdb->insert(
$this->_db_chunks_tablename,
[
'time' => current_time('mysql'),
'name' => $welcome_name,
'text' => $welcome_text,
]
);
*/

return true;
}

function _upsert_slice(string $import_id, int $position, array $slice): bool
function _upsert_slice(string $snapshot_id, int $position, array $slice): bool
{
$json = json_encode($slice);
if ($json === false) {
Expand All @@ -123,12 +53,12 @@ function _upsert_slice(string $import_id, int $position, array $slice): bool

$data = [
'position' => $position,
'import_id' => $import_id,
'snapshot_id' => $snapshot_id,
'slice' => $json,
];

$existing_id = $wpdb->get_var(
$wpdb->prepare("SELECT DISTINCT id from {$this->_db_chunks_tablename} WHERE import_id=%s and position=%d", $import_id, $position)
$wpdb->prepare("SELECT DISTINCT id from {$this->_db_chunks_tablename} WHERE snapshot_id=%s and position=%d", $snapshot_id, $position)
);

if ($existing_id !== null) {
Expand Down Expand Up @@ -162,12 +92,12 @@ function create(ImpexImportProfile $profile, array $options = [], string $name =
/**
* @return Generator|array[]
*/
function get_slices(string $import_id, int $limit = PHP_INT_MAX, int $offset = 0): \Generator
function get_slices(string $snapshot_id, int $limit = PHP_INT_MAX, int $offset = 0): \Generator
{
global $wpdb;

$rows = $wpdb->get_results(
$wpdb->prepare("SELECT * from {$this->_db_chunks_tablename} WHERE import_id=%s ORDER BY position LIMIT %d OFFSET %d", $import_id, $limit, $offset)
$wpdb->prepare("SELECT * from {$this->_db_chunks_tablename} WHERE snapshot_id=%s ORDER BY position LIMIT %d OFFSET %d", $snapshot_id, $limit, $offset)
);
foreach ($rows as $row) {
yield json_decode($row->slice, JSON_OBJECT_AS_ARRAY);
Expand All @@ -181,7 +111,7 @@ function get_slices(string $import_id, int $limit = PHP_INT_MAX, int $offset = 0
*/
function consume(ImpexImportTransformationContext $transformationContext, int $limit = PHP_INT_MAX, int $offset = 0): array
{
$uncomsumed_slices = [];
$unconsumed_slices = [];

$options = $transformationContext->options;
$profile = $transformationContext->profile;
Expand All @@ -204,24 +134,24 @@ function consume(ImpexImportTransformationContext $transformationContext, int $l
}

if (!$consumed) {
$uncomsumed_slices[] = $slice;
$unconsumed_slices[] = $slice;
}
}

$profile->events(self::EVENT_IMPORT_END)($transformationContext, [
'uncomsumed_slices' => &$uncomsumed_slices,
'unconsumed_slices' => &$unconsumed_slices,
'limit' => $limit,
'offset' => $offset,
]);

return $uncomsumed_slices;
return $unconsumed_slices;
}

function update(string $import_id, array $data): array|bool
function update(string $snapshot_id, array $data): array|bool
{
$imports = \get_option(self::WP_OPTION_IMPORTS, []);
foreach ($imports as &$import) {
if ($import['id'] === $import_id) {
if ($import['id'] === $snapshot_id) {
foreach ($data as $key => $value) {
// prevent updating 'id', 'options', 'profile', 'user', 'created'
if (!in_array($key, ['id', 'options', 'profile', 'user', 'created'])) {
Expand All @@ -242,11 +172,11 @@ function update(string $import_id, array $data): array|bool
return false;
}

function remove(string $import_id): bool|array
function remove(string $snapshot_id): bool|array
{
$imports = \get_option(self::WP_OPTION_IMPORTS, []);
foreach ($imports as $index => $import) {
if ($import['id'] === $import_id) {
if ($import['id'] === $snapshot_id) {
$transformationContext = ImpexImportTransformationContext::fromJson($import);

global $wpdb;
Expand All @@ -255,7 +185,7 @@ function remove(string $import_id): bool|array
\WP_Filesystem();

// remove matching export table rows
$rowsDeleted = $wpdb->delete($this->_db_chunks_tablename, ['import_id' => $import_id,]);
$rowsDeleted = $wpdb->delete($this->_db_chunks_tablename, ['snapshot_id' => $snapshot_id,]);
if ($rowsDeleted === false) {
throw new ImpexExportRuntimeException(sprintf('failed to delete jsonized slices from database : %s', $wpdb->last_error));
}
Expand Down
4 changes: 1 addition & 3 deletions plugins/cm4all-wp-impex/inc/class-impex-part.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ abstract class ImpexPart

protected /* ImpexSet */ $_profiles;

public function __construct()
public function __construct(protected string $_db_chunks_tablename)
{
$this->_providers = new class implements \IteratorAggregate
{
Expand Down Expand Up @@ -98,8 +98,6 @@ public function removeProfile(string $name): ImpexProfile|null
return $this->_profiles->remove($name);
}

public abstract function __install(string|bool $installed_version): bool;

/*
make it not serializable
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

require_once __DIR__ . '/class-impex-part.php';
require_once __DIR__ . '/class-impex-runtime-exception.php';

require_once ABSPATH . 'wp-admin/includes/file.php';
/**
* ImpexTransformationContext is a superset of ImpexImportTransformationContext and ImpexExportTransformationContext
*
Expand Down Expand Up @@ -69,7 +69,7 @@ protected function __construct(
$this->_name = $name;
}
$this->_description = $description ?? '';
$this->_uploads_subpath ??= 'impex/' . ($this->_isExportPart ? 'export/' : 'import/') . $this->_id;
$this->_uploads_subpath ??= 'impex/snapshots/' . $this->_id;

$this->_options = $options ?? [];

Expand Down
66 changes: 63 additions & 3 deletions plugins/cm4all-wp-impex/inc/class-impex.php
Original file line number Diff line number Diff line change
Expand Up @@ -57,13 +57,17 @@ class Impex
protected $_export = null;
protected $_import = null;

const DB_SNAPSHOTS_TABLENAME = 'impex_snapshots';

protected function __construct()
{
$this->_export = new class extends ImpexExport
global $wpdb;
$_db_chunks_tablename = $wpdb->prefix . self::DB_SNAPSHOTS_TABLENAME;
$this->_export = new class($_db_chunks_tablename) extends ImpexExport
{
};

$this->_import = new class extends ImpexImport
$this->_import = new class($_db_chunks_tablename) extends ImpexImport
{
};
}
Expand All @@ -82,7 +86,42 @@ public function __install(): bool
/* @var string */
$installed_version = \get_option('impex_version');

$successful = $this->Export->__install($installed_version) && $this->Import->__install($installed_version);
global $wpdb;

if ($installed_version === false) {
// plugin was newly installed
$charset_collate = $wpdb->get_charset_collate();

$_db_chunks_tablename = "{$wpdb->prefix}" . Impex::DB_SNAPSHOTS_TABLENAME;

$sql = "CREATE TABLE {$_db_chunks_tablename} (
id mediumint(9) NOT NULL AUTO_INCREMENT,
snapshot_id CHAR(36) NOT NULL,
position mediumint(9) NOT NULL,
slice json NOT NULL,
PRIMARY KEY (id)
) $charset_collate;";

require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
\dbDelta($sql);
} else if ($installed_version !== Impex::VERSION) {
// new plugin version is now installed, try to upgrade
/*
$sql = "CREATE TABLE {$this->_db_chunks_tablename} (
id mediumint(9) NOT NULL AUTO_INCREMENT,
time datetime DEFAULT '0000-00-00 00:00:00' NOT NULL,
name tinytext NOT NULL,
text text NOT NULL,
url varchar(100) DEFAULT '' NOT NULL,
PRIMARY KEY (id)
);";
require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
dbDelta($sql);
*/
}

$successful = $this->__install_data($installed_version);

if ($installed_version === false) {
\add_option('impex_version', Impex::VERSION);
Expand All @@ -92,6 +131,27 @@ public function __install(): bool

return $successful;
}

protected function __install_data(string|bool $installed_version): bool
{
/*
global $wpdb;
$welcome_name = 'Mr. WordPress';
$welcome_text = 'Congratulations, you just completed the installation!';
$wpdb->insert(
$this->_db_chunks_tablename,
[
'time' => current_time('mysql'),
'name' => $welcome_name,
'text' => $welcome_text,
]
);
*/

return true;
}
}

Impex::getInstance();
22 changes: 20 additions & 2 deletions plugins/cm4all-wp-impex/inc/impex-import-extension-attachment.php
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,12 @@ function __invoke()
}

$post = $this->slice[Impex::SLICE_META]['data'];
$url = $post['guid'];
$url = $post['guid'] ?? null;
if ($url === null) {
$path = $this->slice[Impex::SLICE_DATA];
$fileExtension = pathinfo($path, PATHINFO_EXTENSION);
$url = './' . \sanitize_title($post['post_title'] ?? pathinfo($path, PATHINFO_FILENAME)) . '.' . $fileExtension;
}

/*
code more or less duplicated from wordpress-importer function process_attachment
Expand All @@ -112,6 +117,19 @@ function __invoke()
$post['guid'] = $upload['url'];
unset($post['ID']);

// if post_mime_type is not set the media will not appear correctly resized within media uploader
if (!isset($post['post_mime_type'])) {
$post_mime_type = \wp_get_image_mime($upload['file']);

if (!$post_mime_type) {
$post_mime_type = mime_content_type(basename($upload['file']));
}

if (is_string($post_mime_type)) {
$post['post_mime_type'] = $post_mime_type;
}
}

// as per wp-admin/includes/upload.php
$post_id = \wp_insert_attachment($post, $upload['file']);

Expand Down Expand Up @@ -152,7 +170,7 @@ function fetch_remote_file($url, $post, $slice)
// Extract the file name from the URL.
$file_name = basename(parse_url($url, PHP_URL_PATH));

$uploads = wp_upload_dir($post['post_date']);
$uploads = wp_upload_dir($post['post_date'] ?? null);
if (!($uploads && false === $uploads['error'])) {
return new \WP_Error('upload_dir_error', $uploads['error']);
}
Expand Down
12 changes: 10 additions & 2 deletions plugins/cm4all-wp-impex/inc/impex-import-extension-content.php
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ function _import_posts(array $options, array $slice, array $author_mapping, arra
// @TODO: should we do it (adapted from wp importer) ? it makes actually no sense since the given data is different ...
$post = \apply_filters('wp_import_post_data_raw', $post);

if (!\post_type_exists($post[ContentExporter::SLICE_DATA_POSTS_TYPE])) {
if (!\post_type_exists($post[ContentExporter::SLICE_DATA_POSTS_TYPE] ?? 'post')) {
throw new ImpexImportRuntimeException(
"Failed to create post(title='{$post['post_title']}, post_type={$post['post_title']}') : post_type does not exist."
);
Expand Down Expand Up @@ -181,6 +181,7 @@ function _import_posts(array $options, array $slice, array $author_mapping, arra
$author = \sanitize_user($post[ContentExporter::SLICE_DATA_POSTS_CREATOR], true);
$author = $author_mapping[$author] ?? (int) get_current_user_id();

// see defaults here : https://developer.wordpress.org/reference/functions/wp_insert_post/
$postdata = [
'import_id' => $post[ContentExporter::SLICE_DATA_POSTS_ID],
'post_author' => $author,
Expand All @@ -200,6 +201,13 @@ function _import_posts(array $options, array $slice, array $author_mapping, arra
'post_password' => $post[ContentExporter::SLICE_DATA_POSTS_PASSWORD],
];

// filter out undefined properties
foreach ($postdata as $key => $value) {
if ($value === null) {
unset($postdata[$key]);
}
}

$original_post_id = $post[ContentExporter::SLICE_DATA_POSTS_ID];
$postdata = \apply_filters('wp_import_post_data_processed', $postdata, $post);

Expand Down Expand Up @@ -312,7 +320,7 @@ function _import_posts(array $options, array $slice, array $author_mapping, arra

foreach ($newcomments as $key => $comment) {
// if this is a new post we can skip the comment_exists() check
if (!$post_exists || !comment_exists($comment['comment_author'], $comment['comment_date'])) {
if (!$post_exists || !\comment_exists($comment['comment_author'], $comment['comment_date'])) {
if (isset($inserted_comments[$comment['comment_parent']])) {
$comment['comment_parent'] = $inserted_comments[$comment['comment_parent']];
}
Expand Down
2 changes: 1 addition & 1 deletion plugins/cm4all-wp-impex/plugin.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* Plugin Name: cm4all-wp-impex
* Plugin URI: https://github.com/IONOS-WordPress/cm4all-wp-impex
* Description: Impex contributes extendable Import / Export functionality to WordPress
* Version: 1.2.0
* Version: 1.2.1
* Tags: import, export, migration
* Requires PHP: 8.0
* Requires at least: 5.7
Expand Down
2 changes: 1 addition & 1 deletion plugins/cm4all-wp-impex/src/components/screen.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export default function () {
{!isFileystemApiAvailable && (
<components.Modal
title="Ouch - your browser does not support the File System Access API :-("
onRequestClose={() => {}}
isDismissible={false}
>
<p>
Impex Import / Export requires a browser implementing the{" "}
Expand Down
3 changes: 3 additions & 0 deletions plugins/cm4all-wp-impex/src/wp.impex.dashboard.scss
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ button > svg {
}

.components-modal__screen-overlay {
// do not span over the wp admin menu on the left
margin-left: 160px;

&.blocking {
cursor: wait;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
{
"tag": "content-exporter",
"version": "1.0.0",
"meta": {
"entity": "content-exporter",
"options": []
},
"data": {
"wp:bloginfo_title": "cm4all-wp-impex",
"wp:bloginfo_url": "http://localhost:8888",
"wp:bloginfo_description": "Just another WordPress site",
"wp:bloginfo_language": "en-US",
"wp:bloginfo_rss_url": "http://localhost:8888",
"authors": [
{
"wp:author_id": 0,
"wp:author_login": null,
"wp:author_email": null,
"wp:author_display_name": null,
"wp:author_first_name": null,
"wp:author_last_name": null
},
{
"wp:author_id": 1,
"wp:author_login": "admin",
"wp:author_email": "wordpress@example.com",
"wp:author_display_name": "admin",
"wp:author_first_name": "",
"wp:author_last_name": ""
}
],
"taxonomies": [],
"categories": {
"1": {
"wp:term_id": 1,
"wp:term_slug": "uncategorized",
"wp:term_parent": "",
"wp:term_name": "Uncategorized",
"wp:term_description": "",
"wp:term_meta": []
}
},
"tags": [],
"terms": [
{
"wp:term_id": 1,
"wp:term_taxonomy": "category",
"wp:term_slug": "uncategorized",
"wp:term_parent": "",
"wp:term_name": "Uncategorized",
"wp:term_description": "",
"wp:term_meta": []
},
{
"wp:term_id": 3,
"wp:term_taxonomy": "wp_theme",
"wp:term_slug": "trinity-core",
"wp:term_parent": "",
"wp:term_name": "trinity-core",
"wp:term_description": "",
"wp:term_meta": []
}
],
"posts": [
{
"title": "Header Vorlage",
"link": null,
"pubDate": false,
"dc:creator": "",
"guid": null,
"guid_isPermaLink": false,
"description": null,
"wp:post_content": "<!-- wp:cover {\"url\":\"/wp-content/themes/trinity-core/reusable-blocks/images/content_0002_david-gavi-Ijx8OxvKrgM-unsplash.jpg\",\"customOverlayColor\":\"rgba(0,0, 0, 1)\",\"focalPoint\":{\"x\":\"0.54\",\"y\":\"0.85\"},\"minHeight\":600,\"minHeightUnit\":\"px\",\"align\":\"full\",\"cm4allBlockId\":\"4accb986-dd24-4029-bfb3-ee9bfca557a5\"} -->\n<div class=\"wp-block-cover alignfull has-background-dim\" style=\"min-height:600px\"><img class=\"wp-block-cover__image-background \" alt=\"\" src=\"/wp-content/themes/trinity-core/reusable-blocks/images/content_0002_david-gavi-Ijx8OxvKrgM-unsplash.jpg\" style=\"object-position:54% 85%\" data-object-fit=\"cover\" data-object-position=\"54% 85%\" /><div class=\"wp-block-cover__inner-container\"><!-- wp:group {\"align\":\"wide\",\"className\":\"is-style-cm4all-theme-blocku002du002dshrink-center\",\"cm4allBlockId\":\"f9f61c5f-5133-40cf-b663-c9a9c30de4e2\"} -->\n<div class=\"wp-block-group alignwide is-style-cm4all-theme-block--shrink-center\"><!-- wp:heading {\"textAlign\":\"center\",\"level\":1,\"style\":{\"color\":{\"text\":\"rgba(255,255, 255, 1)\"}},\"className\":\"\",\"cm4allBlockId\":\"b6b44337-ebaa-4058-8d6c-cdbc7d11ab28\"} -->\n<h1 class=\"has-text-align-center has-text-color\" style=\"color:rgba(255,255, 255, 1)\">Willkommen bei<br>[Unternehmen]</h1>\n<!-- /wp:heading -->\n\n<!-- wp:paragraph {\"align\":\"center\",\"style\":{\"color\":{\"text\":\"rgba(255,255, 255, 1)\"}},\"className\":\"\",\"cm4allBlockId\":\"fd614a2c-0500-4e6c-847c-10855869f1f0\"} -->\n<p class=\"has-text-align-center has-text-color\" style=\"color:rgba(255,255, 255, 1)\">Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus.</p>\n<!-- /wp:paragraph -->\n\n<!-- wp:buttons {\"contentJustification\":\"center\",\"className\":\"\",\"cm4allBlockId\":\"e4b5b644-a252-440a-99c7-919d72037ad3\"} -->\n<div class=\"wp-block-buttons is-content-justification-center\"><!-- wp:button {\"className\":\"is-style-outline\",\"cm4allBlockId\":\"bd5bfa00-38a4-4901-b0c0-e5bd61bac962\"} -->\n<div class=\"wp-block-button is-style-outline\"><a class=\"wp-block-button__link\">Jetzt kontaktieren</a></div>\n<!-- /wp:button --></div>\n<!-- /wp:buttons --></div>\n<!-- /wp:group --></div></div>\n<!-- /wp:cover -->",
"wp:post_excerpt": "",
"wp:post_id": 4,
"wp:post_date": "2022-03-08 08:06:47",
"wp:post_date_gmt": "2022-03-08 08:06:47",
"wp:post_modified_gmt": "2022-03-08 08:06:47",
"wp:comment_status": "closed",
"wp:ping_status": "closed",
"wp:post_name": "header-vorlage",
"wp:status": "publish",
"wp:post_parent": 0,
"wp:menu_order": 0,
"wp:post_type": "wp_block",
"wp:post_password": "",
"wp:is_sticky": 0,
"wp:attachment_url": null,
"taxonomy_terms": [],
"meta": [],
"comments": []
},
{
"title": "Footer Vorlage",
"link": null,
"pubDate": false,
"dc:creator": "",
"guid": null,
"guid_isPermaLink": false,
"description": null,
"wp:post_content": "<!-- wp:group {\"align\":\"full\",\"style\":{\"spacing\":{\"padding\":{\"top\":\"80px\",\"bottom\":\"80px\"}}},\"backgroundColor\":\"trinity-theme-secondary-contrast-variant\",\"layout\":{\"wideSize\":\"px\"},\"cm4allBlockId\":\"2fdfc9fd-cceb-42cc-8cca-2a59fbbb682c\"} -->\n<div class=\"wp-block-group alignfull has-trinity-theme-secondary-contrast-variant-background-color has-background\" style=\"padding-top:80px;padding-bottom:80px\"><!-- wp:columns {\"align\":\"wide\",\"cm4allBlockId\":\"82bde859-29a4-4d42-85db-0e289580a623\"} -->\n<div class=\"wp-block-columns alignwide\"><!-- wp:column {\"cm4allBlockId\":\"89db2977-3e48-4428-bc36-76f732bd0b5b\"} -->\n<div class=\"wp-block-column\"><!-- wp:group {\"style\":{\"spacing\":{\"padding\":{\"bottom\":\"20px\"}}},\"cm4allBlockId\":\"0eb1eaa6-0f66-4ba0-8b3c-78d81a3f0ca1\"} -->\n<div class=\"wp-block-group\" style=\"padding-bottom:20px\"><!-- wp:heading {\"level\":3,\"style\":{\"typography\":{\"fontSize\":\"53px\"},\"color\":{\"text\":\"rgba(255,255, 255, 1)\"}},\"className\":\"\",\"cm4allBlockId\":\"2e5f3d2c-3a9e-4d50-bd45-13065fd6ba38\"} -->\n<h3 class=\"has-text-color\" style=\"color:rgba(255,255, 255, 1);font-size:53px\">Wir freuen<br>uns auf Sie! </h3>\n<!-- /wp:heading --></div>\n<!-- /wp:group --></div>\n<!-- /wp:column -->\n\n<!-- wp:column {\"cm4allBlockId\":\"e91bf9e1-5c80-402e-a2b5-2d3960f06565\"} -->\n<div class=\"wp-block-column\"><!-- wp:group {\"style\":{\"spacing\":{\"padding\":{\"bottom\":\"20px\"}}},\"cm4allBlockId\":\"0976f56a-d803-4b6f-9b26-778e3e4c7263\"} -->\n<div class=\"wp-block-group\" style=\"padding-bottom:20px\"><!-- wp:heading {\"level\":4,\"style\":{\"color\":{\"text\":\"rgba(255,255, 255, 1)\"}},\"className\":\"\",\"cm4allBlockId\":\"0f22ba45-55fd-4d7b-ae02-136ad33eddb4\"} -->\n<h4 class=\"has-text-color\" style=\"color:rgba(255,255, 255, 1)\">IHR Unternehmen</h4>\n<!-- /wp:heading -->\n\n<!-- wp:paragraph {\"style\":{\"color\":{\"text\":\"rgba(255,255, 255, 1)\"}},\"className\":\"\",\"cm4allBlockId\":\"b5936153-094a-494d-97fa-af6fefa3a6b5\"} -->\n<p class=\"has-text-color\" style=\"color:rgba(255,255, 255, 1)\">Inhaber: Max Mustermann<br>[Musterstraße 654]<br>[12345 Musterstadt]</p>\n<!-- /wp:paragraph --></div>\n<!-- /wp:group --></div>\n<!-- /wp:column -->\n\n<!-- wp:column {\"cm4allBlockId\":\"ee0ae707-f0cf-45aa-8b60-f381f918635e\"} -->\n<div class=\"wp-block-column\"><!-- wp:group {\"style\":{\"spacing\":{\"padding\":{\"bottom\":\"20px\"}}},\"cm4allBlockId\":\"363034b5-1348-4c42-b902-21a151520056\"} -->\n<div class=\"wp-block-group\" style=\"padding-bottom:20px\"><!-- wp:heading {\"level\":4,\"style\":{\"color\":{\"text\":\"rgba(255,255, 255, 1)\"}},\"className\":\"\",\"cm4allBlockId\":\"8bc1d89a-58fb-4791-a488-76df05f93532\"} -->\n<h4 class=\"has-text-color\" style=\"color:rgba(255,255, 255, 1)\">Kontakt</h4>\n<!-- /wp:heading -->\n\n<!-- wp:paragraph {\"style\":{\"color\":{\"text\":\"rgba(255,255, 255, 1)\"}},\"className\":\"\",\"cm4allBlockId\":\"c3279e95-3f56-42dd-824d-738fd32521ae\"} -->\n<p class=\"has-text-color\" style=\"color:rgba(255,255, 255, 1)\">Tel.: [+49 (0) 123 45 67 89]<br>Fax: [+49 (0) 123 45 67 10]<br>E-Mail: [info@musterfirma.de]</p>\n<!-- /wp:paragraph --></div>\n<!-- /wp:group --></div>\n<!-- /wp:column -->\n\n<!-- wp:column {\"cm4allBlockId\":\"b55a91ae-0aff-47c1-a164-d371236d29e7\"} -->\n<div class=\"wp-block-column\"><!-- wp:group {\"style\":{\"spacing\":{\"padding\":{\"bottom\":\"20px\"}}},\"cm4allBlockId\":\"79f52c29-43c2-4bf9-b54e-633fe92a4af6\"} -->\n<div class=\"wp-block-group\" style=\"padding-bottom:20px\"><!-- wp:heading {\"level\":4,\"style\":{\"color\":{\"text\":\"rgba(255,255, 255, 1)\"}},\"className\":\"\",\"cm4allBlockId\":\"f6dce74e-6cdd-4e99-ac7e-7e97bd2c12d5\"} -->\n<h4 class=\"has-text-color\" style=\"color:rgba(255,255, 255, 1)\">Weitere Links</h4>\n<!-- /wp:heading -->\n\n<!-- wp:paragraph {\"style\":{\"color\":{\"text\":\"rgba(255,255, 255, 1)\"}},\"className\":\"\",\"cm4allBlockId\":\"26ce4fbf-3aae-461c-9aa1-d4ef23aa46dd\"} -->\n<p class=\"has-text-color\" style=\"color:rgba(255,255, 255, 1)\">&gt; <a href=\"#\">Impressum</a><br>&gt; <a href=\"#\">Datenschutz</a></p>\n<!-- /wp:paragraph --></div>\n<!-- /wp:group --></div>\n<!-- /wp:column --></div>\n<!-- /wp:columns --></div>\n<!-- /wp:group -->",
"wp:post_excerpt": "",
"wp:post_id": 5,
"wp:post_date": "2022-03-08 08:06:47",
"wp:post_date_gmt": "2022-03-08 08:06:47",
"wp:post_modified_gmt": "2022-03-08 08:06:47",
"wp:comment_status": "closed",
"wp:ping_status": "closed",
"wp:post_name": "footer-vorlage",
"wp:status": "publish",
"wp:post_parent": 0,
"wp:menu_order": 0,
"wp:post_type": "wp_block",
"wp:post_password": "",
"wp:is_sticky": 0,
"wp:attachment_url": null,
"taxonomy_terms": [],
"meta": [],
"comments": []
},
{
"title": "my-page",
"link": null,
"pubDate": false,
"dc:creator": "admin",
"guid": null,
"guid_isPermaLink": false,
"description": null,
"wp:post_content": "<!-- wp:paragraph {\"className\":\"\",\"cm4allBlockId\":\"38c1404f-05ac-4a8a-8ef5-d04499b0c5ae\"} -->\n<p>This is just a </p>\n<!-- /wp:paragraph -->\n\n<!-- wp:paragraph {\"className\":\"\",\"cm4allBlockId\":\"9c46ca06-bd0a-4567-9ba9-2c0b50873622\"} -->\n<p>multiline paragraph</p>\n<!-- /wp:paragraph -->\n\n<!-- wp:image {\"id\":160,\"sizeSlug\":\"full\",\"linkDestination\":\"none\",\"cm4allBlockId\":\"6c7fe1df-e801-4ed4-952f-ac82561a1f7a\"} -->\n<figure class=\"wp-block-image size-full\"><img src=\"http://localhost:8888/wp-content/uploads/2022/03/angeln.jpg\" alt=\"\" class=\"wp-image-160\"/><figcaption>Fishing is a nice hobby !</figcaption></figure>\n<!-- /wp:image -->",
"wp:post_excerpt": "",
"wp:post_id": 161,
"wp:post_date": "2022-03-08 11:29:46",
"wp:post_date_gmt": "2022-03-08 11:29:46",
"wp:post_modified_gmt": "2022-03-08 11:29:47",
"wp:comment_status": "closed",
"wp:ping_status": "closed",
"wp:post_name": "my-page",
"wp:status": "publish",
"wp:post_parent": 0,
"wp:menu_order": 0,
"wp:post_type": "page",
"wp:post_password": "",
"wp:is_sticky": 0,
"wp:attachment_url": null,
"taxonomy_terms": [],
"meta": [
{
"wp:term_meta_key": "_edit_lock",
"wp:term_meta_value": "1646738987:1"
},
{
"wp:term_meta_key": "_edit_last",
"wp:term_meta_value": "1"
},
{
"wp:term_meta_key": "cmplz_hide_cookiebanner",
"wp:term_meta_value": ""
}
],
"comments": []
},
{
"title": "Custom Styles",
"link": null,
"pubDate": false,
"dc:creator": "admin",
"guid": null,
"guid_isPermaLink": false,
"description": null,
"wp:post_content": "{\"version\": 2, \"isGlobalStylesUserThemeJSON\": true }",
"wp:post_excerpt": "",
"wp:post_id": 162,
"wp:post_date": "2022-03-08 11:28:31",
"wp:post_date_gmt": "2022-03-08 11:28:31",
"wp:post_modified_gmt": "2022-03-08 11:28:31",
"wp:comment_status": "closed",
"wp:ping_status": "closed",
"wp:post_name": "wp-global-styles-trinity-core",
"wp:status": "publish",
"wp:post_parent": 0,
"wp:menu_order": 0,
"wp:post_type": "wp_global_styles",
"wp:post_password": "",
"wp:is_sticky": 0,
"wp:attachment_url": null,
"taxonomy_terms": [
{
"wp:term_taxonomy": "wp_theme",
"wp:term_slug": "trinity-core",
"wp:term_name": "trinity-core"
}
],
"meta": [],
"comments": []
}
]
},
"type": "php",
"date": "2022-03-08 11:29:55 +0000"
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
{
"tag": "attachment",
"version": "1.0.0",
"type": "resource",
"meta": {
"name": "angeln",
"entity": "attachment",
"options": [],
"data": {
"ID": 160,
"post_author": "1",
"post_date": "2022-03-08 11:28:26",
"post_date_gmt": "2022-03-08 11:28:26",
"post_content": "",
"post_title": "angeln",
"post_excerpt": "",
"post_status": "inherit",
"comment_status": "open",
"ping_status": "closed",
"post_password": "",
"post_name": "angeln",
"to_ping": "",
"pinged": "",
"post_modified": "2022-03-08 11:28:26",
"post_modified_gmt": "2022-03-08 11:28:26",
"post_content_filtered": "",
"post_parent": 0,
"guid": "http://localhost:8888/wp-content/uploads/2022/03/angeln.jpg",
"menu_order": 0,
"post_type": "attachment",
"post_mime_type": "image/jpeg",
"comment_count": "0",
"filter": "raw"
}
},
"data": "2022/03/angeln.jpg",
"date": "2022-03-08 11:29:55 +0000"
}
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,7 @@ public function test_delete_item()
$create_response = $this->server->dispatch($request);

global $wpdb;
$rows = $wpdb->get_results("SELECT * from {$wpdb->prefix}" . ImpexExport::DB_CHUNKS_TABLENAME);
$rows = $wpdb->get_results("SELECT * from {$wpdb->prefix}" . Impex::DB_SNAPSHOTS_TABLENAME);
$this->assertCount(1, $rows, 'one slice should be exported');

// delete export
Expand All @@ -214,8 +214,8 @@ public function test_delete_item()
$exports = \get_option(ImpexExport::WP_OPTION_EXPORTS, []);
$this->assertCount(0, $exports, 'no exports should be avaliable');

$rows = $wpdb->get_results("SELECT * from {$wpdb->prefix}" . ImpexExport::DB_CHUNKS_TABLENAME);
$this->assertCount(0, $rows, 'table ' . $wpdb->prefix . ImpexExport::DB_CHUNKS_TABLENAME . ' should be empty');
$rows = $wpdb->get_results("SELECT * from {$wpdb->prefix}" . Impex::DB_SNAPSHOTS_TABLENAME);
$this->assertCount(0, $rows, 'table ' . $wpdb->prefix . Impex::DB_SNAPSHOTS_TABLENAME . ' should be empty');
}

public function test_get_item_slices()
Expand Down
8 changes: 4 additions & 4 deletions plugins/cm4all-wp-impex/tests/phpunit/test-impex-export.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ function setUp()
=> so we need to drop the table manually to have a consistent setup
*/
global $wpdb;
$wpdb->query('DROP TABLE IF EXISTS ' . $wpdb->prefix . ImpexExport::DB_CHUNKS_TABLENAME);
$wpdb->query('DROP TABLE IF EXISTS ' . $wpdb->prefix . Impex::DB_SNAPSHOTS_TABLENAME);
}

function tearDown()
Expand Down Expand Up @@ -98,7 +98,7 @@ function testExportDbTablesInstalled(): void
therefore tables created in wp unit tests are not visible using SHOW TABLES LIKE statements.
that's why we "test" the existence of a table using SELECT COUNT (=> which returns null / error in case the temporary table doesnt exists)
*/
$db_chunks_tablename = $wpdb->prefix . ImpexExport::DB_CHUNKS_TABLENAME;
$db_chunks_tablename = $wpdb->prefix . Impex::DB_SNAPSHOTS_TABLENAME;
$db_chunks_tablename_rowCount = $wpdb->get_var("SELECT COUNT(*) FROM $db_chunks_tablename");
$this->assertNotNull($db_chunks_tablename_rowCount, "ensure $db_chunks_tablename exists");
}
Expand Down Expand Up @@ -223,7 +223,7 @@ function testExportSave(): void
], $wp_option_exports[0], 'wp_options item created by save() matches the expected');

global $wpdb;
$rows = $wpdb->get_results('SELECT * FROM ' . $wpdb->prefix . ImpexExport::DB_CHUNKS_TABLENAME, ARRAY_A);
$rows = $wpdb->get_results('SELECT * FROM ' . $wpdb->prefix . Impex::DB_SNAPSHOTS_TABLENAME, ARRAY_A);

$this->assertCount(count($matchingWpOptions), $rows, 'for each generated slice a row should be created');

Expand Down Expand Up @@ -258,7 +258,7 @@ function testExportSaveFilters(): void
$transformationContext = Impex::getInstance()->Export->save($profile);

global $wpdb;
$rows = $wpdb->get_results('SELECT * FROM ' . $wpdb->prefix . ImpexExport::DB_CHUNKS_TABLENAME, ARRAY_A);
$rows = $wpdb->get_results('SELECT * FROM ' . $wpdb->prefix . Impex::DB_SNAPSHOTS_TABLENAME, ARRAY_A);

$this->assertCount(count($upload_names), $rows, 'for each attachment a slice(and also a row) should be created');

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -277,7 +277,7 @@ function ($_) use ($EXPORT_PATH) {

global $wpdb;
$inserted_slices = absint($wpdb->get_var(
$wpdb->prepare("SELECT COUNT(*) from {$wpdb->prefix}" . ImpexImport::DB_CHUNKS_TABLENAME . ' WHERE import_id=%s', $import_id)
$wpdb->prepare("SELECT COUNT(*) from {$wpdb->prefix}" . Impex::DB_SNAPSHOTS_TABLENAME . ' WHERE snapshot_id=%s', $import_id)
));
$this->assertEquals(count($slice_filenames), $inserted_slices, 'all uploaded chunks should be in the database');

Expand Down Expand Up @@ -318,7 +318,7 @@ function ($_) use ($EXPORT_PATH) {

global $wpdb;
$inserted_slices = absint($wpdb->get_var(
$wpdb->prepare("SELECT COUNT(*) from {$wpdb->prefix}" . ImpexImport::DB_CHUNKS_TABLENAME . ' WHERE import_id=%s', $import_id)
$wpdb->prepare("SELECT COUNT(*) from {$wpdb->prefix}" . Impex::DB_SNAPSHOTS_TABLENAME . ' WHERE snapshot_id=%s', $import_id)
));
$this->assertEquals(count($slice_filenames), $inserted_slices, 'uploaded chunks should be replaced in the database (but not newly inserted)');
}
Expand Down
8 changes: 4 additions & 4 deletions plugins/cm4all-wp-impex/tests/phpunit/test-impex-import.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ function setUp()
=> so we need to drop the table manually to have a consistent setup
*/
global $wpdb;
$wpdb->query('DROP TABLE IF EXISTS ' . $wpdb->prefix . ImpexImport::DB_CHUNKS_TABLENAME);
$wpdb->query('DROP TABLE IF EXISTS ' . $wpdb->prefix . Impex::DB_SNAPSHOTS_TABLENAME);

$this->user = $this->factory->user->create(['role' => 'administrator', 'user_login' => 'test-admin']);
}
Expand Down Expand Up @@ -176,7 +176,7 @@ function testImportDbTablesInstalled(): void
therefore tables created in wp unit tests are not visible using SHOW TABLES LIKE statements.
that's why we "test" the existence of a table using SELECT COUNT (=> which returns null / error in case the temporary table doesnt exists)
*/
$db_chunks_tablename = $wpdb->prefix . ImpexImport::DB_CHUNKS_TABLENAME;
$db_chunks_tablename = $wpdb->prefix . Impex::DB_SNAPSHOTS_TABLENAME;
$db_chunks_tablename_rowCount = $wpdb->get_var("SELECT COUNT(*) FROM $db_chunks_tablename");
$this->assertNotNull($db_chunks_tablename_rowCount, "ensure $db_chunks_tablename exists");
}
Expand Down Expand Up @@ -232,9 +232,9 @@ function ($_) use ($EXPORT_PATH) {
global $wpdb;

foreach ($slices as $position => $slice) {
$wpdb->insert($wpdb->prefix . ImpexImport::DB_CHUNKS_TABLENAME, [
$wpdb->insert($wpdb->prefix . Impex::DB_SNAPSHOTS_TABLENAME, [
'position' => $position,
'import_id' => $importTransformationContext->id,
'snapshot_id' => $importTransformationContext->id,
'slice' => json_encode($slice),
]);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ function testArgumentDerivation(): void
],
);
$this->assertStringStartsWith('Export ', $context->name);
$this->assertStringStartsWith('/var/www/html/wp-content/uploads/impex/export/', $context->path);
$this->assertStringStartsWith('/var/www/html/wp-content/uploads/impex/snapshots/', $context->path);
}

function testSerialization(): void
Expand Down Expand Up @@ -101,7 +101,7 @@ function testImportContext(): void

$this->assertInstanceOf(ImpexImportTransformationContext::class, $context,);
$this->assertStringStartsWith('Import ', $context->name);
$this->assertStringStartsWith('/var/www/html/wp-content/uploads/impex/import/', $context->path);
$this->assertStringStartsWith('/var/www/html/wp-content/uploads/impex/snapshots/', $context->path);

$json = $context->jsonSerialize();

Expand Down