MOBILE-4081 h5p: Fix H5P assets broken on iOS update

main
Dani Palou 2022-11-28 09:48:01 +01:00
parent bf98c699da
commit 51ba1186d5
6 changed files with 190 additions and 63 deletions

View File

@ -18,6 +18,7 @@ import { CoreSites } from '@services/sites';
import { CoreMimetypeUtils } from '@services/utils/mimetype'; import { CoreMimetypeUtils } from '@services/utils/mimetype';
import { CoreTextUtils } from '@services/utils/text'; import { CoreTextUtils } from '@services/utils/text';
import { CoreUtils } from '@services/utils/utils'; import { CoreUtils } from '@services/utils/utils';
import { CorePath } from '@singletons/path';
import { import {
CoreH5PCore, CoreH5PCore,
CoreH5PDependencyAsset, CoreH5PDependencyAsset,
@ -64,7 +65,7 @@ export class CoreH5PFileStorage {
const path = CoreText.concatenatePaths(cachedAssetsPath, fileName); const path = CoreText.concatenatePaths(cachedAssetsPath, fileName);
// Store concatenated content. // Store concatenated content.
const content = await this.concatenateFiles(assets, type); const content = await this.concatenateFiles(assets, type, cachedAssetsPath);
await CoreFile.writeFile(path, content); await CoreFile.writeFile(path, content);
@ -82,11 +83,11 @@ export class CoreH5PFileStorage {
* Adds all files of a type into one file. * Adds all files of a type into one file.
* *
* @param assets A list of files. * @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. * @return Promise resolved with all of the files content in one string.
*/ */
protected async concatenateFiles(assets: CoreH5PDependencyAsset[], type: string): Promise<string> { protected async concatenateFiles(assets: CoreH5PDependencyAsset[], type: string, newFolder: string): Promise<string> {
const basePath = CoreFile.convertFileSrc(CoreFile.getBasePathInstant());
let content = ''; let content = '';
for (const i in assets) { for (const i in assets) {
@ -104,46 +105,22 @@ export class CoreH5PFileStorage {
// Rewrite relative URLs used inside stylesheets. // Rewrite relative URLs used inside stylesheets.
const matches = fileContent.match(/url\(['"]?([^"')]+)['"]?\)/ig); const matches = fileContent.match(/url\(['"]?([^"')]+)['"]?\)/ig);
const assetPath = asset.path.replace(/(^\/|\/$)/g, ''); // Path without start/end slashes. const assetPath = asset.path.replace(/(^\/|\/$)/g, ''); // Path without start/end slashes.
const treated = {}; const treated: Record<string, string> = {};
if (matches && matches.length) { if (matches && matches.length) {
matches.forEach((match) => { 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)) { if (treated[url] || url.match(/^(data:|([a-z0-9]+:)?\/)/i)) {
return; // Not relative or already treated, skip. return; // Not relative or already treated, skip.
} }
const pathSplit = assetPath.split('/');
treated[url] = url; treated[url] = url;
const assetPathFolder = CoreFile.getFileAndDirectoryFromPath(assetPath).directory;
/* 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('/');
}
fileContent = fileContent.replace( fileContent = fileContent.replace(
new RegExp(CoreTextUtils.escapeForRegex(match), 'g'), new RegExp(CoreTextUtils.escapeForRegex(match), 'g'),
'url("' + CoreText.concatenatePaths(basePath, url) + '")', 'url("' + CorePath.changeRelativePath(assetPathFolder, url, newFolder) + '")',
); );
}); });
} }

View File

@ -25,8 +25,8 @@ import { CoreUtils } from './utils/utils';
import { CoreApp } from './app'; import { CoreApp } from './app';
import { CoreZoomLevel } from '@features/settings/services/settings-helper'; import { CoreZoomLevel } from '@features/settings/services/settings-helper';
import { CorePromisedValue } from '@classes/promised-value'; import { CorePromisedValue } from '@classes/promised-value';
import { CoreFile } from './file';
const VERSION_APPLIED = 'version_applied'; import { CorePlatform } from './platform';
/** /**
* Factory to handle app updates. This factory shouldn't be used outside of core. * 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' }) @Injectable({ providedIn: 'root' })
export class CoreUpdateManagerProvider { export class CoreUpdateManagerProvider {
protected static readonly VERSION_APPLIED = 'version_applied';
protected static readonly PREVIOUS_APP_FOLDER = 'previous_app_folder';
protected logger: CoreLogger; protected logger: CoreLogger;
protected doneDeferred: CorePromisedValue<void>; protected doneDeferred: CorePromisedValue<void>;
@ -63,13 +66,22 @@ export class CoreUpdateManagerProvider {
const promises: Promise<unknown>[] = []; const promises: Promise<unknown>[] = [];
const versionCode = CoreConstants.CONFIG.versioncode; const versionCode = CoreConstants.CONFIG.versioncode;
const versionApplied = await CoreConfig.get<number>(VERSION_APPLIED, 0); const [versionApplied, previousAppFolder, currentAppFolder] = await Promise.all([
CoreConfig.get<number>(CoreUpdateManagerProvider.VERSION_APPLIED, 0),
CoreConfig.get<string>(CoreUpdateManagerProvider.PREVIOUS_APP_FOLDER, ''),
CorePlatform.isMobile() ? CoreUtils.ignoreErrors(CoreFile.getBasePath(), '') : '',
]);
if (versionCode > versionApplied) { if (versionCode > versionApplied) {
promises.push(this.checkCurrentSiteAllowed()); 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()); promises.push(CoreH5P.h5pPlayer.deleteAllContentIndexes());
} }
@ -80,7 +92,10 @@ export class CoreUpdateManagerProvider {
try { try {
await Promise.all(promises); 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) { } catch (error) {
this.logger.error(`Error applying update from ${versionApplied} to ${versionCode}`, error); this.logger.error(`Error applying update from ${versionApplied} to ${versionCode}`, error);
} finally { } finally {

View File

@ -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;
}
}
}

View File

@ -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');
});
});

View File

@ -36,14 +36,4 @@ describe('CoreText singleton', () => {
expect(CoreText.removeStartingSlash('//foo')).toEqual('/foo'); 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');
});
}); });

View File

@ -12,6 +12,8 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
import { CorePath } from './path';
/** /**
* Singleton with helper functions for text manipulation. * Singleton with helper functions for text manipulation.
*/ */
@ -74,24 +76,10 @@ export class CoreText {
* @param leftPath Left path. * @param leftPath Left path.
* @param rightPath Right path. * @param rightPath Right path.
* @return Concatenated path. * @return Concatenated path.
* @deprecated since 4.1.0. Please use CorePath.concatenatePaths instead.
*/ */
static concatenatePaths(leftPath: string, rightPath: string): string { static concatenatePaths(leftPath: string, rightPath: string): string {
if (!leftPath) { return CorePath.concatenatePaths(leftPath, rightPath);
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;
}
} }
} }