diff --git a/cordova-plugin-moodleapp/types/index.d.ts b/cordova-plugin-moodleapp/types/index.d.ts index 502be9d21..59ed28d30 100644 --- a/cordova-plugin-moodleapp/types/index.d.ts +++ b/cordova-plugin-moodleapp/types/index.d.ts @@ -12,12 +12,12 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { SecureStorage } from '../src/ts/plugins/SecureStorage'; +import { SecureStorage as SecureStorageImpl } from '../src/ts/plugins/SecureStorage'; declare global { interface MoodleAppPlugins { - secureStorage: SecureStorage; + secureStorage: SecureStorageImpl; } interface Cordova { @@ -25,3 +25,5 @@ declare global { } } + +export type SecureStorage = InstanceType; diff --git a/src/core/features/emulator/classes/SecureStorage.ts b/src/core/features/emulator/classes/SecureStorage.ts new file mode 100644 index 000000000..bec49d87c --- /dev/null +++ b/src/core/features/emulator/classes/SecureStorage.ts @@ -0,0 +1,134 @@ +// (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 { SecureStorage } from 'cordova-plugin-moodleapp'; + +/** + * Mock for SecureStorage plugin. It will store the data without being encrypted. + */ +export class SecureStorageMock implements SecureStorage { + + /** + * Get one or more values. + * + * @param names Names of the values to get. + * @param collection The collection where the values are stored. + * @returns Object with name -> value. If a name isn't found it won't be included in the result. + */ + async get(names: string | string[], collection: string): Promise> { + if (typeof names === 'string') { + names = [names]; + } + + const result: Record = {}; + + for (let i = 0; i < names.length; i++) { + const name = names[i]; + if (!name) { + continue; + } + + const storedValue = localStorage.getItem(this.getPrefixedName(name, collection)); + if (storedValue !== null) { + result[name] = storedValue; + } + } + + return result; + } + + /** + * Get the prefix to add to a name, including the collection. + * + * @param collection Collection name. + * @returns Prefix. + */ + private getCollectionPrefix(collection: string): string { + return `SecureStorage_${collection}_`; + } + + /** + * Get the full name to retrieve, store or delete an item. + * + * @param name Name inside collection. + * @param collection Collection name. + * @returns Full name. + */ + private getPrefixedName(name: string, collection: string): string { + return this.getCollectionPrefix(collection) + name; + } + + /** + * Set one or more values. + * + * @param data Object with values to store, in format name -> value. Null or undefined valid values will be ignored. + * @param collection The collection where to store the values. + */ + async store(data: Record, collection: string): Promise { + for (const name in data) { + const value = data[name]; + if (value === undefined || value === null) { + delete data[name]; + } else if (typeof value !== 'string') { + throw new Error(`SecureStorage: Invalid value for ${name}. Expected string, received ${typeof value}`); + } + } + + for (const name in data) { + if (!name) { + continue; + } + + const value = data[name]; + localStorage.setItem(this.getPrefixedName(name, collection), value); + } + } + + /** + * Delete one or more values. + * + * @param names Names to delete. + * @param collection The collection where to delete the values. + */ + async delete(names: string | string[], collection: string): Promise { + if (typeof names === 'string') { + names = [names]; + } + + for (let i = 0; i < names.length; i++) { + const name = names[i]; + if (!name) { + continue; + } + + localStorage.removeItem(this.getPrefixedName(name, collection)); + } + } + + /** + * Delete all values for a certain collection. + * + * @param collection The collection to delete. + */ + async deleteCollection(collection: string): Promise { + const names = Object.keys(localStorage); + for (let i = 0; i < names.length; i++) { + const name = names[i]; + if (name.startsWith(this.getCollectionPrefix(collection))) { + localStorage.removeItem(name); + } + } + } + +} diff --git a/src/core/features/emulator/emulator.module.ts b/src/core/features/emulator/emulator.module.ts index 48ef1aa05..e82936fe0 100644 --- a/src/core/features/emulator/emulator.module.ts +++ b/src/core/features/emulator/emulator.module.ts @@ -42,6 +42,8 @@ import { MediaCaptureMock } from './services/media-capture'; import { ZipMock } from './services/zip'; import { CorePlatform } from '@services/platform'; import { CoreLocalNotifications } from '@services/local-notifications'; +import { CoreNative } from '@features/native/services/native'; +import { SecureStorageMock } from '@features/emulator/classes/SecureStorage'; /** * This module handles the emulation of Cordova plugins in browser and desktop. @@ -101,12 +103,14 @@ import { CoreLocalNotifications } from '@services/local-notifications'; }, { provide: APP_INITIALIZER, - useFactory: () => () => { + useValue: async () => { if (CorePlatform.is('cordova')) { return; } - return CoreEmulatorHelper.load(); + CoreNative.registerBrowserMock('secureStorage', new SecureStorageMock()); + + await CoreEmulatorHelper.load(); }, multi: true, }, diff --git a/src/core/features/native/services/native.ts b/src/core/features/native/services/native.ts index a7a1f6423..90654fc90 100644 --- a/src/core/features/native/services/native.ts +++ b/src/core/features/native/services/native.ts @@ -24,6 +24,7 @@ import { AsyncInstance, asyncInstance } from '@/core/utils/async-instance'; export class CoreNativeService { private plugins: Partial> = {}; + private mocks: Partial> = {}; /** * Get a native plugin instance. @@ -31,22 +32,33 @@ export class CoreNativeService { * @param plugin Plugin name. * @returns Plugin instance. */ - plugin(plugin: Plugin): AsyncInstance | null { - if (!CorePlatform.isMobile()) { - return null; - } - + plugin(plugin: Plugin): AsyncInstance { if (!(plugin in this.plugins)) { this.plugins[plugin] = asyncInstance(async () => { await CorePlatform.ready(); - return window.cordova?.MoodleApp?.[plugin]; + const instance = CorePlatform.isMobile() ? window.cordova?.MoodleApp?.[plugin] : this.mocks[plugin]; + if (!instance) { + throw new Error(`Plugin ${plugin} not found.`); + } + + return instance; }); } return this.plugins[plugin] as AsyncInstance; } + /** + * Register a mock to use in browser instead of the native plugin implementation. + * + * @param plugin Plugin name. + * @param instance Instance to use. + */ + registerBrowserMock(plugin: Plugin, instance: MoodleAppPlugins[Plugin]): void { + this.mocks[plugin] = instance; + } + } export const CoreNative = makeSingleton(CoreNativeService);