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
// 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<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 { 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,
},

View File

@ -24,6 +24,7 @@ import { AsyncInstance, asyncInstance } from '@/core/utils/async-instance';
export class CoreNativeService {
private plugins: Partial<Record<keyof MoodleAppPlugins, AsyncInstance>> = {};
private mocks: Partial<Record<keyof MoodleAppPlugins, MoodleAppPlugins[keyof MoodleAppPlugins]>> = {};
/**
* Get a native plugin instance.
@ -31,22 +32,33 @@ export class CoreNativeService {
* @param plugin Plugin name.
* @returns Plugin instance.
*/
plugin<Plugin extends keyof MoodleAppPlugins>(plugin: Plugin): AsyncInstance<MoodleAppPlugins[Plugin]> | null {
if (!CorePlatform.isMobile()) {
return null;
}
plugin<Plugin extends keyof MoodleAppPlugins>(plugin: Plugin): AsyncInstance<MoodleAppPlugins[Plugin]> {
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<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);