forked from WordPress/playground-tools
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[Import/export] Support WXR, WXZ, and full-site import and export (Wo…
…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
Showing
11 changed files
with
3,396 additions
and
207 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.