MOBILE-3839 browser: Support SecureStorage in browser

main
Dani Palou 2023-09-07 13:19:06 +02:00
parent c4ce1edfe0
commit 7e7aae4853
4 changed files with 162 additions and 10 deletions

View File

@ -12,12 +12,12 @@
// 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 { SecureStorage } from '../src/ts/plugins/SecureStorage'; import { SecureStorage as SecureStorageImpl } from '../src/ts/plugins/SecureStorage';
declare global { declare global {
interface MoodleAppPlugins { interface MoodleAppPlugins {
secureStorage: SecureStorage; secureStorage: SecureStorageImpl;
} }
interface Cordova { interface Cordova {
@ -25,3 +25,5 @@ declare global {
} }
} }
export type SecureStorage = InstanceType<typeof SecureStorageImpl>;

View File

@ -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<Record<string, string>> {
if (typeof names === 'string') {
names = [names];
}
const result: Record<string, string> = {};
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<string, string>, collection: string): Promise<void> {
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<void> {
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<void> {
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);
}
}
}
}

View File

@ -42,6 +42,8 @@ import { MediaCaptureMock } from './services/media-capture';
import { ZipMock } from './services/zip'; import { ZipMock } from './services/zip';
import { CorePlatform } from '@services/platform'; import { CorePlatform } from '@services/platform';
import { CoreLocalNotifications } from '@services/local-notifications'; 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. * 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, provide: APP_INITIALIZER,
useFactory: () => () => { useValue: async () => {
if (CorePlatform.is('cordova')) { if (CorePlatform.is('cordova')) {
return; return;
} }
return CoreEmulatorHelper.load(); CoreNative.registerBrowserMock('secureStorage', new SecureStorageMock());
await CoreEmulatorHelper.load();
}, },
multi: true, multi: true,
}, },

View File

@ -24,6 +24,7 @@ import { AsyncInstance, asyncInstance } from '@/core/utils/async-instance';
export class CoreNativeService { export class CoreNativeService {
private plugins: Partial<Record<keyof MoodleAppPlugins, AsyncInstance>> = {}; private plugins: Partial<Record<keyof MoodleAppPlugins, AsyncInstance>> = {};
private mocks: Partial<Record<keyof MoodleAppPlugins, MoodleAppPlugins[keyof MoodleAppPlugins]>> = {};
/** /**
* Get a native plugin instance. * Get a native plugin instance.
@ -31,22 +32,33 @@ export class CoreNativeService {
* @param plugin Plugin name. * @param plugin Plugin name.
* @returns Plugin instance. * @returns Plugin instance.
*/ */
plugin<Plugin extends keyof MoodleAppPlugins>(plugin: Plugin): AsyncInstance<MoodleAppPlugins[Plugin]> | null { plugin<Plugin extends keyof MoodleAppPlugins>(plugin: Plugin): AsyncInstance<MoodleAppPlugins[Plugin]> {
if (!CorePlatform.isMobile()) {
return null;
}
if (!(plugin in this.plugins)) { if (!(plugin in this.plugins)) {
this.plugins[plugin] = asyncInstance(async () => { this.plugins[plugin] = asyncInstance(async () => {
await CorePlatform.ready(); 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<MoodleAppPlugins[Plugin]>; return this.plugins[plugin] as AsyncInstance<MoodleAppPlugins[Plugin]>;
} }
/**
* Register a mock to use in browser instead of the native plugin implementation.
*
* @param plugin Plugin name.
* @param instance Instance to use.
*/
registerBrowserMock<Plugin extends keyof MoodleAppPlugins>(plugin: Plugin, instance: MoodleAppPlugins[Plugin]): void {
this.mocks[plugin] = instance;
}
} }
export const CoreNative = makeSingleton(CoreNativeService); export const CoreNative = makeSingleton(CoreNativeService);