Skip to content

Commit

Permalink
[Import/export] Support WXR, WXZ, and full-site import and export (Wo…
Browse files Browse the repository at this point in the history
…rdPress#209)

* Export Playground as a full site export

* Introduce jsToPHPTranslator

It solves the problem of escaping arguments when writing PHP code in JavaScript.

Before:
```js
const code = `define('WP_HOME', "${absoluteUrl}");`
// if absoluteUrl contains the '"' character, this code will break
```

After:
```js
const code = t.define('WP_HOME', absoluteUrl).toString();
// absoluteUrl is correctly escaped and can even be an array
// or an object
```

* Import/export support: WXR, WXZ, and full-site

This commit adds multiple ways to move data in and out of Playground.
Specifically, it implements the following client functions:

* zipEntireSite()
* replaceSite()
* exportWXR()
* exportWXZ()
* submitImporterForm()

The WXR format is one natively used by the official WordPress importer plugin.

The WXZ format is supported through the following plugin:

https://github.com/akirk/export-wxz
https://github.com/akirk/wordpress-importer

The full-site export is simply a zip archive containing the entire site.
  • Loading branch information
adamziel authored Apr 18, 2023
1 parent c015930 commit c588dec
Show file tree
Hide file tree
Showing 11 changed files with 3,396 additions and 207 deletions.
254 changes: 150 additions & 104 deletions packages/playground/client/src/lib/import-export.ts
Original file line number Diff line number Diff line change
@@ -1,139 +1,185 @@
import { saveAs } from 'file-saver';
import type { PlaygroundClient } from '../';
import type { PHPResponse, PlaygroundClient } from '../';
import { jsToPHPTranslator } from '@php-wasm/web';

// @ts-ignore
import migration from './migration.php?raw';
import migrationsPHPCode from './migration.php?raw';

const databaseExportName = 'databaseExport.xml';
const databaseExportPath = '/' + databaseExportName;
const t = jsToPHPTranslator();

export async function exportFile(playground: PlaygroundClient) {
const databaseExportResponse = await playground.request({
url: '/wp-admin/export.php?download=true&content=all',
});
const databaseExportContent = databaseExportResponse.text;
await playground.writeFile(databaseExportPath, databaseExportContent);
/**
* Full site export support:
*/

/**
* Export the current site as a zip file.
*
* @param playground Playground client.
*/
export async function zipEntireSite(playground: PlaygroundClient) {
const wpVersion = await playground.wordPressVersion;
const phpVersion = await playground.phpVersion;
const documentRoot = await playground.documentRoot;
const exportName = `wordpress-playground--wp${wpVersion}--php${phpVersion}.zip`;
const exportPath = `/${exportName}`;
const exportWriteRequest = await playground.run({
code:
migration +
` generateZipFile('${exportPath}', '${databaseExportPath}', '${documentRoot}');`,
});
if (exportWriteRequest.exitCode !== 0) {
throw exportWriteRequest.errors;
}
const zipName = `wordpress-playground--wp${wpVersion}--php${phpVersion}.zip`;
const zipPath = `/${zipName}`;

const fileBuffer = await playground.readFileAsBuffer(exportName);
const file = new File([fileBuffer], exportName);
saveAs(file);
}
const documentRoot = await playground.documentRoot;
await phpMigration(playground, t.zipDir(documentRoot, zipPath));

export async function importFile(playground: PlaygroundClient, file: File) {
if (
// eslint-disable-next-line no-alert
!confirm(
'Are you sure you want to import this file? Previous data will be lost.'
)
) {
return false;
}
const fileBuffer = await playground.readFileAsBuffer(zipPath);
playground.unlink(zipPath);

// Write uploaded file to filesystem for processing with PHP
const fileArrayBuffer = await file.arrayBuffer();
const fileContent = new Uint8Array(fileArrayBuffer);
const importPath = '/import.zip';
return new File([fileBuffer], zipName);
}

await playground.writeFile(importPath, fileContent);
/**
* Replace the current site with the contents of a full site zip file.
*
* @param playground Playground client.
* @param fullSiteZip Zipped WordPress site.
*/
export async function replaceSite(
playground: PlaygroundClient,
fullSiteZip: File
) {
const zipPath = '/import.zip';
await playground.writeFile(
zipPath,
new Uint8Array(await fullSiteZip.arrayBuffer())
);

// Import the database
const databaseFromZipFileReadRequest = await playground.run({
code:
migration +
` readFileFromZipArchive('${importPath}', '${databaseExportPath}');`,
});
if (databaseFromZipFileReadRequest.exitCode !== 0) {
throw databaseFromZipFileReadRequest.errors;
}
const absoluteUrl = await playground.absoluteUrl;
const documentRoot = await playground.documentRoot;

const databaseFromZipFileContent = new TextDecoder().decode(
databaseFromZipFileReadRequest.bytes
await phpMigration(
playground,
`${t.delTree(documentRoot)};
${t.unzip(zipPath, '/')};`
);

const databaseFile = new File(
[databaseFromZipFileContent],
databaseExportName
await patchFile(
playground,
`${documentRoot}/wp-config.php`,
(contents) =>
`<?php
if(!defined('WP_HOME')) {
${t.define('WP_HOME', absoluteUrl)};
${t.define('WP_SITEURL', absoluteUrl)};
}
?>${contents}`
);
}

/**
* WXR and WXZ files support:
*/

/**
* Exports the WordPress database as a WXR file using
* the core WordPress export tool.
*
* @param playground Playground client
* @returns WXR file
*/
export async function exportWXR(playground: PlaygroundClient) {
const databaseExportResponse = await playground.request({
url: '/wp-admin/export.php?download=true&content=all',
});
return new File([databaseExportResponse.bytes], 'export.xml');
}

/**
* Exports the WordPress database as a WXZ file using
* the export-wxz plugin from https://github.com/akirk/export-wxz.
*
* @param playground Playground client
* @returns WXZ file
*/
export async function exportWXZ(playground: PlaygroundClient) {
const databaseExportResponse = await playground.request({
url: '/wp-admin/export.php?download=true&content=all&export_wxz=1',
});
return new File([databaseExportResponse.bytes], 'export.wxz');
}

/**
* Uploads a file to the WordPress importer and returns the response.
* Supports both WXR and WXZ files.
*
* @see https://github.com/WordPress/wordpress-importer/compare/master...akirk:wordpress-importer:import-wxz.patch
* @param playground Playground client.
* @param file The file to import.
*/
export async function submitImporterForm(
playground: PlaygroundClient,
file: File
) {
const importerPageOneResponse = await playground.request({
url: '/wp-admin/admin.php?import=wordpress',
});

const importerPageOneContent = new DOMParser().parseFromString(
importerPageOneResponse.text,
'text/html'
);

const firstUrlAction = importerPageOneContent
const firstUrlAction = DOM(importerPageOneResponse)
.getElementById('import-upload-form')
?.getAttribute('action');

const stepOneResponse = await playground.request({
url: `/wp-admin/${firstUrlAction}`,
method: 'POST',
files: { import: databaseFile },
files: { import: file },
});

const importerPageTwoContent = new DOMParser().parseFromString(
stepOneResponse.text,
'text/html'
);

const importerPageTwoForm = importerPageTwoContent.querySelector(
// Map authors of imported posts to existing users
const importForm = DOM(stepOneResponse).querySelector(
'#wpbody-content form'
);
const secondUrlAction = importerPageTwoForm?.getAttribute(
'action'
) as string;

const nonce = (
importerPageTwoForm?.querySelector(
"input[name='_wpnonce']"
) as HTMLInputElement
).value;

const referrer = (
importerPageTwoForm?.querySelector(
"input[name='_wp_http_referer']"
) as HTMLInputElement
).value;

const importId = (
importerPageTwoForm?.querySelector(
"input[name='import_id']"
) as HTMLInputElement
).value;

await playground.request({
url: secondUrlAction,
) as HTMLFormElement;

if (!importForm) {
console.log(stepOneResponse.text);
throw new Error(
'Could not find an importer form in response. See the response text above for details.'
);
}

const data = getFormData(importForm);
data['fetch_attachments'] = '1';
for (const key in data) {
if (key.startsWith('user_map[')) {
const newKey = 'user_new[' + key.slice(9, -1) + ']';
data[newKey] = '1'; // Hardcoded admin ID for now
}
}

return await playground.request({
url: importForm.action,
method: 'POST',
formData: {
_wpnonce: nonce,
_wp_http_referer: referrer,
import_id: importId,
},
formData: data,
});
}

// Import the file system
const importFileSystemRequest = await playground.run({
code: migration + ` importZipFile('${importPath}');`,
function DOM(response: PHPResponse) {
return new DOMParser().parseFromString(response.text, 'text/html');
}

function getFormData(form: HTMLFormElement): Record<string, unknown> {
return Object.fromEntries((new FormData(form) as any).entries());
}

async function patchFile(
playground: PlaygroundClient,
path: string,
callback: (contents: string) => string
) {
await playground.writeFile(
path,
callback(await playground.readFileAsText(path))
);
}

async function phpMigration(playground: PlaygroundClient, code: string) {
const result = await playground.run({
code: migrationsPHPCode + code,
});
if (importFileSystemRequest.exitCode !== 0) {
throw importFileSystemRequest.errors;
if (result.exitCode !== 0) {
console.log(result.errors);
throw result.errors;
}

return true;
return result;
}
9 changes: 7 additions & 2 deletions packages/playground/client/src/lib/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
export { exportFile } from './import-export';
export { importFile } from './import-export';
export {
zipEntireSite,
exportWXR,
exportWXZ,
replaceSite,
submitImporterForm,
} from './import-export';
export { login } from './login';
export { installTheme } from './install-theme';
export type { InstallThemeOptions } from './install-theme';
Expand Down
66 changes: 30 additions & 36 deletions packages/playground/client/src/lib/migration.php
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
<?php

function generateZipFile($exportPath, $databasePath, $docRoot) {
function zipDir($dir, $output, $additionalFiles = array())
{
$zip = new ZipArchive;
$res = $zip->open($exportPath, ZipArchive::CREATE);
$res = $zip->open($output, ZipArchive::CREATE);
if ($res === TRUE) {
$zip->addFile($databasePath);
$directories = array();
$directories[] = $docRoot . '/';

while(sizeof($directories)) {
foreach ($additionalFiles as $file) {
$zip->addFile($file);
}
$directories = array(
rtrim($dir, '/') . '/'
);
while (sizeof($directories)) {
$dir = array_pop($directories);

if ($handle = opendir($dir)) {
Expand All @@ -19,13 +22,9 @@ function generateZipFile($exportPath, $databasePath, $docRoot) {

$entry = $dir . $entry;

if (
is_dir($entry) &&
strpos($entry, 'wp-content/database') == false &&
strpos($entry, 'wp-includes') == false
) {
$directory_path = $entry . '/';
array_push($directories, $directory_path);
if (is_dir($entry)) {
$directory_path = $entry . '/';
array_push($directories, $directory_path);
} else if (is_file($entry)) {
$zip->addFile($entry);
}
Expand All @@ -34,35 +33,30 @@ function generateZipFile($exportPath, $databasePath, $docRoot) {
}
}
$zip->close();
chmod($exportPath, 0777);
chmod($output, 0777);
}
}

function readFileFromZipArchive($pathToZip, $pathToFile) {
chmod($pathToZip, 0777);
function unzip($zipPath, $extractTo, $overwrite = true)
{
if(!is_dir($extractTo)) {
mkdir($extractTo, 0777, true);
}
$zip = new ZipArchive;
$res = $zip->open($pathToZip);
$res = $zip->open($zipPath);
if ($res === TRUE) {
$file = $zip->getFromName($pathToFile);
echo $file;
$zip->extractTo($extractTo);
$zip->close();
chmod($extractTo, 0777);
}
}

function importZipFile($pathToZip) {
$zip = new ZipArchive;
$res = $zip->open($pathToZip);
if ($res === TRUE) {
$counter = 0;
while ($zip->statIndex($counter)) {
$file = $zip->statIndex($counter);
$filePath = $file['name'];
if (!file_exists(dirname($filePath))) {
mkdir(dirname($filePath), 0777, true);
}
$overwrite = fopen($filePath, 'w');
fwrite($overwrite, $zip->getFromIndex($counter));
$counter++;
}
$zip->close();

function delTree($dir)
{
$files = array_diff(scandir($dir), array('.', '..'));
foreach ($files as $file) {
(is_dir("$dir/$file")) ? delTree("$dir/$file") : unlink("$dir/$file");
}
return rmdir($dir);
}
Loading

0 comments on commit c588dec

Please sign in to comment.