diff --git a/src/lib/import-export/export/export-manager.ts b/src/lib/import-export/export/export-manager.ts index c4f5d1e87..66208906f 100644 --- a/src/lib/import-export/export/export-manager.ts +++ b/src/lib/import-export/export/export-manager.ts @@ -8,10 +8,12 @@ export async function exportBackup( onEvent: ( data: ImportExportEventData ) => void, exporters: NewExporter[] = defaultExporterOptions ): Promise< void > { + let foundValidExporter; for ( const Exporter of exporters ) { const exporterInstance = new Exporter( exportOptions ); const removeExportListeners = handleEvents( exporterInstance, onEvent, ExportEvents ); - if ( await exporterInstance.canHandle() ) { + foundValidExporter = await exporterInstance.canHandle(); + if ( foundValidExporter ) { try { await exporterInstance.export(); } finally { @@ -20,6 +22,10 @@ export async function exportBackup( break; } } + if ( ! foundValidExporter ) { + onEvent( { event: ExportEvents.EXPORT_ERROR, data: null } ); + throw new Error( 'No suitable exporter found for the site' ); + } } export const defaultExporterOptions: NewExporter[] = [ DefaultExporter, SqlExporter ]; diff --git a/src/lib/import-export/export/exporters/default-exporter.ts b/src/lib/import-export/export/exporters/default-exporter.ts index 6a97e24ec..20feb9d15 100644 --- a/src/lib/import-export/export/exporters/default-exporter.ts +++ b/src/lib/import-export/export/exporters/default-exporter.ts @@ -45,12 +45,22 @@ export class DefaultExporter extends EventEmitter implements Exporter { return false; } - const requiredPaths = [ 'wp-content', 'wp-includes', 'wp-load.php', 'wp-config.php' ]; + const requiredPaths = [ + { path: 'wp-content', isDir: true }, + { path: 'wp-includes', isDir: true }, + { path: 'wp-load.php', isDir: false }, + { path: 'wp-config.php', isDir: false }, + ]; this.siteFiles = await this.getSiteFiles(); return requiredPaths.every( ( requiredPath ) => - this.siteFiles.some( ( file ) => file.includes( requiredPath ) ) + this.siteFiles.some( ( file ) => { + const relativePath = path.relative( this.options.site.path, file ); + return requiredPath.isDir + ? relativePath.startsWith( requiredPath.path ) + : relativePath === requiredPath.path; + } ) ); } diff --git a/src/lib/import-export/tests/export/export-manager.test.ts b/src/lib/import-export/tests/export/export-manager.test.ts index e21829534..49e472c51 100644 --- a/src/lib/import-export/tests/export/export-manager.test.ts +++ b/src/lib/import-export/tests/export/export-manager.test.ts @@ -60,4 +60,32 @@ describe( 'exportBackup', () => { expect( MockExporter2 ).toHaveBeenCalledWith( mockExportOptions ); expect( ExportMethod2 ).toHaveBeenCalled(); } ); + + it( 'fails if no exporter is found', async () => { + const ExportMethod1 = jest.fn(); + const MockExporter1 = jest.fn( () => ( { + canHandle: jest.fn().mockResolvedValue( false ), + export: ExportMethod1, + on: jest.fn(), + emit: jest.fn(), + } ) ); + + const ExportMethod2 = jest.fn(); + const MockExporter2 = jest.fn( () => ( { + canHandle: jest.fn().mockResolvedValue( false ), + export: ExportMethod2, + on: jest.fn(), + emit: jest.fn(), + } ) ); + + const exporters: NewExporter[] = [ MockExporter1, MockExporter2 ]; + await expect( exportBackup( mockExportOptions, jest.fn(), exporters ) ).rejects.toThrow( + 'No suitable exporter found for the site' + ); + + expect( MockExporter1 ).toHaveBeenCalledWith( mockExportOptions ); + expect( ExportMethod1 ).not.toHaveBeenCalled(); + expect( MockExporter2 ).toHaveBeenCalledWith( mockExportOptions ); + expect( ExportMethod2 ).not.toHaveBeenCalled(); + } ); } ); diff --git a/src/lib/import-export/tests/export/exporters/default-exporter.test.ts b/src/lib/import-export/tests/export/exporters/default-exporter.test.ts index af48517e4..25934207d 100644 --- a/src/lib/import-export/tests/export/exporters/default-exporter.test.ts +++ b/src/lib/import-export/tests/export/exporters/default-exporter.test.ts @@ -53,7 +53,7 @@ describe( 'DefaultExporter', () => { { path: '/path/to/site/wp-content/plugins/plugin1', name: 'plugin1.php', isFile: () => true }, { path: '/path/to/site/wp-content/themes/theme1', name: 'index.php', isFile: () => true }, { path: '/path/to/site/wp-includes/index.php', name: 'index.php', isFile: () => true }, - { path: '/path/to/site/wp-load.php', name: 'wp-load.php', isFile: () => true }, + { path: '/path/to/site', name: 'wp-load.php', isFile: () => true }, ]; ( fsPromises.readdir as jest.Mock ).mockResolvedValue( mockFiles );