From 51ba1186d5a869bb1fde69f902848d4e5ba6a8d6 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Mon, 28 Nov 2022 09:48:01 +0100 Subject: [PATCH] MOBILE-4081 h5p: Fix H5P assets broken on iOS update --- src/core/features/h5p/classes/file-storage.ts | 41 ++----- src/core/services/update-manager.ts | 25 ++++- src/core/singletons/path.ts | 102 ++++++++++++++++++ src/core/singletons/tests/path.test.ts | 55 ++++++++++ src/core/singletons/tests/text.test.ts | 10 -- src/core/singletons/text.ts | 20 +--- 6 files changed, 190 insertions(+), 63 deletions(-) create mode 100644 src/core/singletons/path.ts create mode 100644 src/core/singletons/tests/path.test.ts diff --git a/src/core/features/h5p/classes/file-storage.ts b/src/core/features/h5p/classes/file-storage.ts index 598e4a404..76dc636af 100644 --- a/src/core/features/h5p/classes/file-storage.ts +++ b/src/core/features/h5p/classes/file-storage.ts @@ -18,6 +18,7 @@ import { CoreSites } from '@services/sites'; import { CoreMimetypeUtils } from '@services/utils/mimetype'; import { CoreTextUtils } from '@services/utils/text'; import { CoreUtils } from '@services/utils/utils'; +import { CorePath } from '@singletons/path'; import { CoreH5PCore, CoreH5PDependencyAsset, @@ -64,7 +65,7 @@ export class CoreH5PFileStorage { const path = CoreText.concatenatePaths(cachedAssetsPath, fileName); // Store concatenated content. - const content = await this.concatenateFiles(assets, type); + const content = await this.concatenateFiles(assets, type, cachedAssetsPath); await CoreFile.writeFile(path, content); @@ -82,11 +83,11 @@ export class CoreH5PFileStorage { * Adds all files of a type into one file. * * @param assets A list of files. - * @param type The type of files in assets. Either 'scripts' or 'styles' + * @param type The type of files in assets. Either 'scripts' or 'styles'. + * @param newFolder The new folder where the concatenated content will be stored. * @return Promise resolved with all of the files content in one string. */ - protected async concatenateFiles(assets: CoreH5PDependencyAsset[], type: string): Promise { - const basePath = CoreFile.convertFileSrc(CoreFile.getBasePathInstant()); + protected async concatenateFiles(assets: CoreH5PDependencyAsset[], type: string, newFolder: string): Promise { let content = ''; for (const i in assets) { @@ -104,46 +105,22 @@ export class CoreH5PFileStorage { // Rewrite relative URLs used inside stylesheets. const matches = fileContent.match(/url\(['"]?([^"')]+)['"]?\)/ig); const assetPath = asset.path.replace(/(^\/|\/$)/g, ''); // Path without start/end slashes. - const treated = {}; + const treated: Record = {}; if (matches && matches.length) { matches.forEach((match) => { - let url = match.replace(/(url\(['"]?|['"]?\)$)/ig, ''); + const url = match.replace(/(url\(['"]?|['"]?\)$)/ig, ''); if (treated[url] || url.match(/^(data:|([a-z0-9]+:)?\/)/i)) { return; // Not relative or already treated, skip. } - const pathSplit = assetPath.split('/'); treated[url] = url; - - /* Find "../" in the URL. If it exists, we have to remove "../" and switch the last folder in the - filepath for the first folder in the url. */ - if (url.match(/^\.\.\//)) { - // Split and remove empty values. - const urlSplit = url.split('/').filter((i) => i); - - // Remove the file name from the asset path. - pathSplit.pop(); - - // Remove the first element from the file URL: ../ . - urlSplit.shift(); - - // Put the url's first folder into the asset path. - pathSplit[pathSplit.length - 1] = urlSplit[0]; - urlSplit.shift(); - - // Create the new URL and replace it in the file contents. - url = pathSplit.join('/') + '/' + urlSplit.join('/'); - - } else { - pathSplit[pathSplit.length - 1] = url; // Put the whole path to the end of the asset path. - url = pathSplit.join('/'); - } + const assetPathFolder = CoreFile.getFileAndDirectoryFromPath(assetPath).directory; fileContent = fileContent.replace( new RegExp(CoreTextUtils.escapeForRegex(match), 'g'), - 'url("' + CoreText.concatenatePaths(basePath, url) + '")', + 'url("' + CorePath.changeRelativePath(assetPathFolder, url, newFolder) + '")', ); }); } diff --git a/src/core/services/update-manager.ts b/src/core/services/update-manager.ts index 1c90569f0..f720228eb 100644 --- a/src/core/services/update-manager.ts +++ b/src/core/services/update-manager.ts @@ -25,8 +25,8 @@ import { CoreUtils } from './utils/utils'; import { CoreApp } from './app'; import { CoreZoomLevel } from '@features/settings/services/settings-helper'; import { CorePromisedValue } from '@classes/promised-value'; - -const VERSION_APPLIED = 'version_applied'; +import { CoreFile } from './file'; +import { CorePlatform } from './platform'; /** * Factory to handle app updates. This factory shouldn't be used outside of core. @@ -36,6 +36,9 @@ const VERSION_APPLIED = 'version_applied'; @Injectable({ providedIn: 'root' }) export class CoreUpdateManagerProvider { + protected static readonly VERSION_APPLIED = 'version_applied'; + protected static readonly PREVIOUS_APP_FOLDER = 'previous_app_folder'; + protected logger: CoreLogger; protected doneDeferred: CorePromisedValue; @@ -63,13 +66,22 @@ export class CoreUpdateManagerProvider { const promises: Promise[] = []; const versionCode = CoreConstants.CONFIG.versioncode; - const versionApplied = await CoreConfig.get(VERSION_APPLIED, 0); + const [versionApplied, previousAppFolder, currentAppFolder] = await Promise.all([ + CoreConfig.get(CoreUpdateManagerProvider.VERSION_APPLIED, 0), + CoreConfig.get(CoreUpdateManagerProvider.PREVIOUS_APP_FOLDER, ''), + CorePlatform.isMobile() ? CoreUtils.ignoreErrors(CoreFile.getBasePath(), '') : '', + ]); if (versionCode > versionApplied) { promises.push(this.checkCurrentSiteAllowed()); } - if (versionCode >= 3950 && versionApplied < 3950 && versionApplied > 0) { + if ( + (versionCode >= 3950 && versionApplied < 3950 && versionApplied > 0) || + (currentAppFolder && currentAppFolder !== previousAppFolder) + ) { + // Delete content indexes if the app folder has changed. + // This happens in iOS every time the app is updated, even if the version hasn't changed. promises.push(CoreH5P.h5pPlayer.deleteAllContentIndexes()); } @@ -80,7 +92,10 @@ export class CoreUpdateManagerProvider { try { await Promise.all(promises); - await CoreConfig.set(VERSION_APPLIED, versionCode); + await Promise.all([ + CoreConfig.set(CoreUpdateManagerProvider.VERSION_APPLIED, versionCode), + currentAppFolder ? CoreConfig.set(CoreUpdateManagerProvider.PREVIOUS_APP_FOLDER, currentAppFolder) : undefined, + ]); } catch (error) { this.logger.error(`Error applying update from ${versionApplied} to ${versionCode}`, error); } finally { diff --git a/src/core/singletons/path.ts b/src/core/singletons/path.ts new file mode 100644 index 000000000..d92bbf7a3 --- /dev/null +++ b/src/core/singletons/path.ts @@ -0,0 +1,102 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { CoreText } from './text'; + +/** + * Singleton with helper functions for paths. + */ +export class CorePath { + + // Avoid creating singleton instances. + private constructor() { + // Nothing to do. + } + + /** + * Calculate a relative path from a folder to another folder. + * + * E.g. if initial folder is foo/bar, and final folder is foo/baz/xyz, it will return ../bar/xyz. + * + * @param initialFolder The initial folder path. + * @param finalFolder The final folder. The "root" should be the same as initialFolder. + * @return Relative path. + */ + static calculateRelativePath(initialFolder: string, finalFolder: string): string { + initialFolder = CoreText.removeStartingSlash(CoreText.removeEndingSlash(initialFolder)); + finalFolder = CoreText.removeStartingSlash(CoreText.removeEndingSlash(finalFolder)); + + if (initialFolder === finalFolder) { + return ''; + } + + const initialFolderSplit = initialFolder === '' ? [] : initialFolder.split('/'); + const finalFolderSplit = finalFolder === '' ? [] : finalFolder.split('/'); + + let firstDiffIndex = initialFolderSplit.length > 0 && finalFolderSplit.length > 0 ? + initialFolderSplit.findIndex((value, index) => value !== finalFolderSplit[index]) : + 0; + + if (firstDiffIndex === -1) { + // All elements in initial folder are equal. The first diff is the first element in the final folder. + firstDiffIndex = initialFolderSplit.length; + } + + const newPathToFinalFolder = finalFolderSplit.slice(firstDiffIndex).join('/'); + + return '../'.repeat(initialFolderSplit.length - firstDiffIndex) + newPathToFinalFolder; + } + + /** + * Convert a relative path (based on a certain folder) to a relative path based on a different folder. + * + * E.g. if current folder is foo/bar, relative URL is test.jpg and new folder is foo/baz, + * it will return ../bar/test.jpg. + * + * @param currentFolder The current folder path. + * @param path The relative path. + * @param newFolder The folder to use to calculate the new relative path. The "root" should be the same as currentFolder. + * @return Relative path. + */ + static changeRelativePath(currentFolder: string, path: string, newFolder: string): string { + return CoreText.concatenatePaths(CorePath.calculateRelativePath(newFolder, currentFolder), path); + } + + /** + * Concatenate two paths, adding a slash between them if needed. + * + * @param leftPath Left path. + * @param rightPath Right path. + * @return Concatenated path. + */ + static concatenatePaths(leftPath: string, rightPath: string): string { + if (!leftPath) { + return rightPath; + } else if (!rightPath) { + return leftPath; + } + + const lastCharLeft = leftPath.slice(-1); + const firstCharRight = rightPath.charAt(0); + + if (lastCharLeft === '/' && firstCharRight === '/') { + return leftPath + rightPath.substring(1); + } else if (lastCharLeft !== '/' && firstCharRight !== '/') { + return leftPath + '/' + rightPath; + } else { + return leftPath + rightPath; + } + } + +} diff --git a/src/core/singletons/tests/path.test.ts b/src/core/singletons/tests/path.test.ts new file mode 100644 index 000000000..9cee687e0 --- /dev/null +++ b/src/core/singletons/tests/path.test.ts @@ -0,0 +1,55 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { CorePath } from '@singletons/path'; + +describe('CorePath', () => { + + it('calculates relative paths from one folder to another', () => { + expect(CorePath.calculateRelativePath('foo/bar', 'foo/bar')).toEqual(''); + expect(CorePath.calculateRelativePath('/foo/bar', 'foo/bar/')).toEqual(''); + expect(CorePath.calculateRelativePath('foo/bar', 'foo/baz')).toEqual('../baz'); + expect(CorePath.calculateRelativePath('foo', 'baz')).toEqual('../baz'); + expect(CorePath.calculateRelativePath('foo/bar/baz', 'foo/baz')).toEqual('../../baz'); + expect(CorePath.calculateRelativePath('foo/baz', 'foo/bar/baz')).toEqual('../bar/baz'); + expect(CorePath.calculateRelativePath('foo/bar/baz', 'foo/bar')).toEqual('../'); + expect(CorePath.calculateRelativePath('foo/bar', 'foo/bar/baz')).toEqual('baz'); + expect(CorePath.calculateRelativePath('', 'foo')).toEqual('foo'); + expect(CorePath.calculateRelativePath('foo', '')).toEqual('../'); + }); + + it('changes relative paths to a different folder', () => { + expect(CorePath.changeRelativePath('foo/bar', 'test.png', 'foo/bar')).toEqual('test.png'); + expect(CorePath.changeRelativePath('/foo/bar', 'test.png', 'foo/bar/')).toEqual('test.png'); + expect(CorePath.changeRelativePath('foo/bar', 'test.png', 'foo/baz')).toEqual('../bar/test.png'); + expect(CorePath.changeRelativePath('foo/bar', '../xyz/test.png', 'foo/baz')).toEqual('../bar/../xyz/test.png'); + expect(CorePath.changeRelativePath('foo', 'bar/test.png', 'baz')).toEqual('../foo/bar/test.png'); + expect(CorePath.changeRelativePath('foo/bar/baz', 'test.png', 'foo/baz')).toEqual('../bar/baz/test.png'); + expect(CorePath.changeRelativePath('foo/bar/baz', 'test.png', 'foo/bar')).toEqual('baz/test.png'); + expect(CorePath.changeRelativePath('foo/bar/baz', 'test.png', 'foo/bar/xyz')).toEqual('../baz/test.png'); + expect(CorePath.changeRelativePath('', 'test.png', 'foo')).toEqual('../test.png'); + expect(CorePath.changeRelativePath('foo', 'test.png', '')).toEqual('foo/test.png'); + }); + + it('concatenates paths', () => { + expect(CorePath.concatenatePaths('', 'foo/bar')).toEqual('foo/bar'); + expect(CorePath.concatenatePaths('foo/bar', '')).toEqual('foo/bar'); + expect(CorePath.concatenatePaths('foo', 'bar')).toEqual('foo/bar'); + expect(CorePath.concatenatePaths('foo/', 'bar')).toEqual('foo/bar'); + expect(CorePath.concatenatePaths('foo', '/bar')).toEqual('foo/bar'); + expect(CorePath.concatenatePaths('foo/', '/bar')).toEqual('foo/bar'); + expect(CorePath.concatenatePaths('foo/bar', 'baz')).toEqual('foo/bar/baz'); + }); + +}); diff --git a/src/core/singletons/tests/text.test.ts b/src/core/singletons/tests/text.test.ts index b4a6fff8a..b50f0db7d 100644 --- a/src/core/singletons/tests/text.test.ts +++ b/src/core/singletons/tests/text.test.ts @@ -36,14 +36,4 @@ describe('CoreText singleton', () => { expect(CoreText.removeStartingSlash('//foo')).toEqual('/foo'); }); - it('concatenates paths', () => { - expect(CoreText.concatenatePaths('', 'foo/bar')).toEqual('foo/bar'); - expect(CoreText.concatenatePaths('foo/bar', '')).toEqual('foo/bar'); - expect(CoreText.concatenatePaths('foo', 'bar')).toEqual('foo/bar'); - expect(CoreText.concatenatePaths('foo/', 'bar')).toEqual('foo/bar'); - expect(CoreText.concatenatePaths('foo', '/bar')).toEqual('foo/bar'); - expect(CoreText.concatenatePaths('foo/', '/bar')).toEqual('foo/bar'); - expect(CoreText.concatenatePaths('foo/bar', 'baz')).toEqual('foo/bar/baz'); - }); - }); diff --git a/src/core/singletons/text.ts b/src/core/singletons/text.ts index 8255e0588..782837242 100644 --- a/src/core/singletons/text.ts +++ b/src/core/singletons/text.ts @@ -12,6 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +import { CorePath } from './path'; + /** * Singleton with helper functions for text manipulation. */ @@ -74,24 +76,10 @@ export class CoreText { * @param leftPath Left path. * @param rightPath Right path. * @return Concatenated path. + * @deprecated since 4.1.0. Please use CorePath.concatenatePaths instead. */ static concatenatePaths(leftPath: string, rightPath: string): string { - if (!leftPath) { - return rightPath; - } else if (!rightPath) { - return leftPath; - } - - const lastCharLeft = leftPath.slice(-1); - const firstCharRight = rightPath.charAt(0); - - if (lastCharLeft === '/' && firstCharRight === '/') { - return leftPath + rightPath.substring(1); - } else if (lastCharLeft !== '/' && firstCharRight !== '/') { - return leftPath + '/' + rightPath; - } else { - return leftPath + rightPath; - } + return CorePath.concatenatePaths(leftPath, rightPath); } }