From 1d6805100f41604abde7f1bb851eeeb2db7de3ad Mon Sep 17 00:00:00 2001 From: Thomas Taylor Date: Tue, 30 Jun 2026 17:49:17 +0100 Subject: [PATCH] Fix: Resolve import globs safely on Windows paths --- lib/AdaptFrameworkImport.js | 10 +++++----- tests/AdaptFrameworkImport.spec.js | 20 ++++++++++++++++++++ 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/lib/AdaptFrameworkImport.js b/lib/AdaptFrameworkImport.js index 3c9b629..6728e6d 100644 --- a/lib/AdaptFrameworkImport.js +++ b/lib/AdaptFrameworkImport.js @@ -322,7 +322,7 @@ class AdaptFrameworkImport { // nothing to do } // find and store the course data path - const courseDirs = await glob(`${this.path}/*/course`) + const courseDirs = await glob('*/course', { cwd: this.path, absolute: true, posix: true }) if (courseDirs.length === 0) { this.framework.log('error', 'NO_COURSE_DIR', this.path) throw App.instance.errors.FW_IMPORT_INVALID_COURSE.setData({ reason: 'no source course directory found in archive; expected a course folder nested one level deep' }) @@ -400,7 +400,7 @@ class AdaptFrameworkImport { */ async loadAssetData () { this.assetData = [] - const metaFiles = await glob(`${this.langPath}/assets.json`, { absolute: true }) + const metaFiles = await glob('assets.json', { cwd: this.langPath, absolute: true, posix: true }) if (metaFiles.length) { // process included asset metadata log('debug', 'processing metadata files', metaFiles) await Promise.all(metaFiles.map(async f => { @@ -408,7 +408,7 @@ class AdaptFrameworkImport { Object.entries(metaJson).forEach(([filename, metadata]) => this.assetData.push({ filename, ...metadata })) })) } else { // process the file metadata manually - const assetFiles = await glob(`${this.langPath}/*/*`, { absolute: true }) + const assetFiles = await glob('*/*', { cwd: this.langPath, absolute: true, posix: true }) log('debug', 'processing asset files manually', assetFiles.length) this.assetData.push(...assetFiles.map(f => Object.assign({}, { title: path.basename(f), filepath: f }))) } @@ -425,7 +425,7 @@ class AdaptFrameworkImport { * @return {Promise} */ async loadPluginData () { - const usedPluginPaths = await glob(`${this.path}/src/+(components|extensions|menu|theme)/*`, { absolute: true }) + const usedPluginPaths = await glob('src/+(components|extensions|menu|theme)/*', { cwd: this.path, absolute: true, posix: true }) const getPluginType = pluginData => { for (const type of ['component', 'extension', 'menu', 'theme']) { if (pluginData[type] !== undefined) return type @@ -596,7 +596,7 @@ class AdaptFrameworkImport { async importCourseAssets () { let imagesImported = this.settings.isDryRun ? this.assetData.length : 0 await Promise.all(this.assetData.map(async data => { - const filepath = data.filepath ?? (await glob(`${this.langPath}/*/${data.filename}`, { absolute: true }))[0] + const filepath = data.filepath ?? (await glob(`*/${data.filename}`, { cwd: this.langPath, absolute: true, posix: true }))[0] // remove unused filepath to avoid possible issues delete data.filepath if (this.settings.isDryRun) { diff --git a/tests/AdaptFrameworkImport.spec.js b/tests/AdaptFrameworkImport.spec.js index 87f7859..f331c7a 100644 --- a/tests/AdaptFrameworkImport.spec.js +++ b/tests/AdaptFrameworkImport.spec.js @@ -1,5 +1,8 @@ import { describe, it, mock } from 'node:test' import assert from 'node:assert/strict' +import fs from 'fs/promises' +import os from 'os' +import path from 'path' // Prevent log() from triggering App.instance boot during tests mock.module('../lib/utils/log.js', { @@ -624,4 +627,21 @@ describe('AdaptFrameworkImport', () => { assert.deepEqual(ctx.statusReport.error[0].data, ['adapt-contrib-missing']) }) }) + + describe('#loadAssetData()', () => { + it('should find assets when the course path contains glob-significant characters', async () => { + // interpolating the path into the pattern would parse '[id]' as a char class (and '\' as an escape on Windows) + const langPath = await fs.mkdtemp(path.join(os.tmpdir(), 'af-import[id]-')) + try { + await fs.mkdir(path.join(langPath, 'assets')) + await fs.writeFile(path.join(langPath, 'assets', 'logo.png'), '') + const ctx = { langPath, tags: [], assetData: [] } + await AdaptFrameworkImport.prototype.loadAssetData.call(ctx) + assert.equal(ctx.assetData.length, 1) + assert.equal(ctx.assetData[0].title, 'logo.png') + } finally { + await fs.rm(langPath, { recursive: true, force: true }) + } + }) + }) })