diff --git a/config.xml b/config.xml index 18f423f71..92e603145 100644 --- a/config.xml +++ b/config.xml @@ -67,11 +67,12 @@ + - + diff --git a/cordova-plugin-moodleapp/plugin.xml b/cordova-plugin-moodleapp/plugin.xml index a6abaa381..ddc3cf863 100644 --- a/cordova-plugin-moodleapp/plugin.xml +++ b/cordova-plugin-moodleapp/plugin.xml @@ -7,10 +7,20 @@ - - + + - + + + + + + + + + + + diff --git a/cordova-plugin-moodleapp/scripts/copy-javascript.js b/cordova-plugin-moodleapp/scripts/copy-javascript.js index 18f287d08..e843ac866 100755 --- a/cordova-plugin-moodleapp/scripts/copy-javascript.js +++ b/cordova-plugin-moodleapp/scripts/copy-javascript.js @@ -22,10 +22,14 @@ const { resolve } = require('path'); const bundle = readFileSync(resolve(__dirname, '../www/index.js')).toString(); const template = readFileSync(resolve(__dirname, './templates/cordova-plugin.js')).toString(); +const pluginsPath = resolve(__dirname, '../../plugins/'); const platformsPath = resolve(__dirname, '../../platforms/'); const filePaths = [ + resolve(pluginsPath, 'cordova-plugin-moodleapp/www/index.js'), resolve(platformsPath, 'android/app/src/main/assets/www/plugins/cordova-plugin-moodleapp/www/index.js'), resolve(platformsPath, 'android/platform_www/plugins/cordova-plugin-moodleapp/www/index.js'), + resolve(platformsPath, 'ios/platform_www/plugins/cordova-plugin-moodleapp/www/index.js'), + resolve(platformsPath, 'ios/www/plugins/cordova-plugin-moodleapp/www/index.js'), ]; const pluginIndex = template .replace('[[PLUGIN_NAME]]', 'cordova-plugin-moodleapp.moodleapp') diff --git a/cordova-plugin-moodleapp/src/android/SecureStorage.java b/cordova-plugin-moodleapp/src/android/SecureStorage.java new file mode 100644 index 000000000..b60070a08 --- /dev/null +++ b/cordova-plugin-moodleapp/src/android/SecureStorage.java @@ -0,0 +1,178 @@ +// (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. + +package com.moodle.moodlemobile; + +import android.os.Build; +import android.util.Log; +import android.content.Context; +import android.content.SharedPreferences; +import java.security.GeneralSecurityException; +import java.io.IOException; + +import org.json.JSONArray; +import org.json.JSONObject; +import org.json.JSONException; +import org.apache.cordova.CordovaPlugin; +import org.apache.cordova.CallbackContext; +import org.apache.cordova.PluginResult; + +import com.adobe.phonegap.push.EncryptionHandler; + +public class SecureStorage extends CordovaPlugin { + + private static final String TAG = "SecureStorage"; + private static final String SHARED_PREFS_NAME = "moodlemobile_shared_prefs"; + + @Override + public boolean execute(String action, JSONArray args, CallbackContext callbackContext) { + try { + switch (action) { + case "get": + callbackContext.success(this.get(args.getJSONArray(0), args.getString(1))); + + return true; + case "store": + this.store(args.getJSONObject(0), args.getString(1)); + callbackContext.success(); + + return true; + case "delete": + this.delete(args.getJSONArray(0), args.getString(1)); + callbackContext.success(); + + return true; + case "deleteCollection": + this.deleteCollection(args.getString(0)); + callbackContext.success(); + + return true; + } + } catch (Throwable e) { + Log.e(TAG, "Failed executing action: " + action, e); + callbackContext.error(e.getMessage()); + callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.ERROR)); + } + + return false; + } + + /** + * Get several values from secure storage. + * + * @param names List of names to get. + * @param collection The collection where the values are stored. + * @return Values for each name. + */ + private JSONObject get(JSONArray names, String collection) throws GeneralSecurityException, IOException, JSONException { + Context context = this.cordova.getActivity().getApplicationContext(); + SharedPreferences sharedPreferences = getSharedPreferences(collection); + JSONObject result = new JSONObject(); + + Log.d(TAG, "Get values with names " + names.toString()); + + for(int i = 0; i < names.length(); i++) { + String name = names.optString(i); + + if (name == null || name.isEmpty()) { + continue; + } + + String rawValue = sharedPreferences.getString(name, null); + if (rawValue == null) { + continue; + } + + result.put(name, EncryptionHandler.Companion.decrypt(context, rawValue)); + } + + return result; + } + + /** + * Store data in secure storage. + * + * @param data Data to store, using a name -> value format. + * @param collection The collection where to store the values. + */ + private void store(JSONObject data, String collection) throws GeneralSecurityException, IOException, JSONException { + Context context = this.cordova.getActivity().getApplicationContext(); + SharedPreferences sharedPreferences = getSharedPreferences(collection); + SharedPreferences.Editor editor = sharedPreferences.edit(); + JSONArray names = data.names(); + + Log.d(TAG, "Store values with names " + names.toString()); + + for(int i = 0; i < names.length(); i++) { + String name = names.optString(i); + + if (name != null && !name.isEmpty()) { + editor.putString(name, EncryptionHandler.Companion.encrypt(context, data.getString(name))); + } + } + + editor.apply(); + } + + /** + * Delete some values from secure storage. + * + * @param names Names to delete. + * @param collection The collection where to delete the values. + */ + private void delete(JSONArray names, String collection) throws GeneralSecurityException, IOException { + Log.d(TAG, "Delete value with names " + names.toString()); + + SharedPreferences sharedPreferences = getSharedPreferences(collection); + SharedPreferences.Editor editor = sharedPreferences.edit(); + + for(int i = 0; i < names.length(); i++) { + String name = names.optString(i); + + if (name != null && !name.isEmpty()) { + editor.remove(name); + } + } + + editor.apply(); + } + + /** + * Delete all values from a collection. + * + * @param collection The collection to delete. + */ + private void deleteCollection(String collection) throws GeneralSecurityException, IOException { + Log.d(TAG, "Delete all values in collection " + collection); + + SharedPreferences sharedPreferences = getSharedPreferences(collection); + SharedPreferences.Editor editor = sharedPreferences.edit(); + editor.clear(); + editor.apply(); + } + + /** + * Get shared preferences instance. + * + * @param collection The collection to use. + * @return Shared preferences instance. + */ + private SharedPreferences getSharedPreferences(String collection) { + return this.cordova.getActivity().getApplicationContext().getSharedPreferences( + SHARED_PREFS_NAME + "_" + collection, + Context.MODE_PRIVATE + ); + } + +} diff --git a/cordova-plugin-moodleapp/src/android/SystemUI.java b/cordova-plugin-moodleapp/src/android/SystemUI.java deleted file mode 100644 index ed20bd28a..000000000 --- a/cordova-plugin-moodleapp/src/android/SystemUI.java +++ /dev/null @@ -1,45 +0,0 @@ -// (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. - -package com.moodle.moodlemobile; - -import android.graphics.Color; -import android.os.Build; -import android.util.Log; -import android.view.Window; - -import org.apache.cordova.CordovaPlugin; -import org.apache.cordova.CallbackContext; - -import org.json.JSONArray; - -public class SystemUI extends CordovaPlugin { - - private static final String TAG = "SystemUI"; - - @Override - public boolean execute(String action, JSONArray args, CallbackContext callbackContext) { - try { - switch (action) { - // No actions yet. - } - } catch (Throwable e) { - Log.e(TAG, "Failed executing action: " + action, e); - } - - return false; - } - - -} diff --git a/cordova-plugin-moodleapp/src/ios/SecureStorage.h b/cordova-plugin-moodleapp/src/ios/SecureStorage.h new file mode 100644 index 000000000..71f1a8c6b --- /dev/null +++ b/cordova-plugin-moodleapp/src/ios/SecureStorage.h @@ -0,0 +1,11 @@ +#import +#import + +@interface SecureStorage : CDVPlugin {} + +- (void)get:(CDVInvokedUrlCommand*)command; +- (void)store:(CDVInvokedUrlCommand*)command; +- (void)delete:(CDVInvokedUrlCommand*)command; +- (void)deleteCollection:(CDVInvokedUrlCommand*)command; + +@end diff --git a/cordova-plugin-moodleapp/src/ios/SecureStorage.m b/cordova-plugin-moodleapp/src/ios/SecureStorage.m new file mode 100644 index 000000000..c71e752ff --- /dev/null +++ b/cordova-plugin-moodleapp/src/ios/SecureStorage.m @@ -0,0 +1,218 @@ +#import +#import +#import +#import "SecureStorage.h" + +@implementation SecureStorage + +- (void)get:(CDVInvokedUrlCommand*)command { + NSArray* names = [command argumentAtIndex:0]; + NSString* collection = [command argumentAtIndex:1 withDefault:@""]; + NSMutableDictionary* result = [NSMutableDictionary new]; + + NSLog(@"SecureStorage: Get values with names %@ in collection %@", names, collection); + + for (NSString* name in names) { + NSString* value = [self getValue:name inCollection:collection]; + if (value != nil) { + result[name] = value; + } + } + + CDVPluginResult* pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:result]; + [self.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId]; +} + +- (NSString *)getValue:(NSString*)name inCollection:(NSString*)collection { + if ([name length] == 0) { + return nil; + } + + NSDictionary* query = @{ + (id)kSecClass: (id)kSecClassGenericPassword, + (id)kSecAttrAccount: name, + (id)kSecAttrService: collection, + (id)kSecReturnData: @YES, + (id)kSecMatchLimit: (id)kSecMatchLimitOne + }; + NSData* storedData = NULL; + + OSStatus status = SecItemCopyMatching((CFDictionaryRef)query, (void *) &storedData); + + if (status == errSecSuccess) { + return [[NSString alloc] initWithData:storedData encoding:NSUTF8StringEncoding]; + } else if (status != errSecItemNotFound) { + NSLog(@"Error getting value for %@ in collection %@. Status: %d", name, collection, (int) status); + } + + return nil; +} + +- (void)store:(CDVInvokedUrlCommand*)command { + + NSDictionary* data = [command argumentAtIndex:0]; + NSString* collection = [command argumentAtIndex:1 withDefault:@""]; + NSArray* names = [data allKeys]; + BOOL error = false; + + // Variables to be able to rollback changes if something fails. + NSMutableArray* insertedNames = [NSMutableArray new]; + NSMutableDictionary* previousValues = [NSMutableDictionary new]; + + NSLog(@"SecureStorage: Store values with names %@ in collection %@", names, collection); + + for (NSString* name in data) { + OSStatus status; + NSString* storedValue = [self getValue:name inCollection:collection]; + + if (storedValue != nil) { + status = [self updateName:name withValue:data[name] inCollection: collection]; + } else { + status = [self addName:name withValue:data[name] inCollection: collection]; + } + + if (status != errSecSuccess) { + NSLog(@"Error storing value for %@ in collection %@. Status: %d", name, collection, (int) status); + error = true; + + // Rollback. + for (NSString *name in insertedNames) { + [self deleteName:name fromCollection:collection]; + } + for(NSString *name in previousValues) { + [self updateName:name withValue:previousValues[name] inCollection: collection]; + } + + break; + } else if (storedValue != nil) { + previousValues[name] = storedValue; + } else { + [insertedNames addObject:name]; + } + } + + CDVPluginResult* pluginResult; + if (error) { + pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR + messageAsString:@"Error storing one or more values in secure storage."]; + } else { + pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK]; + } + + [self.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId]; +} + +- (OSStatus)addName:(NSString*)name withValue:(NSString*)value inCollection:(NSString*)collection { + NSData* newValue = [value dataUsingEncoding:NSUTF8StringEncoding]; + NSMutableDictionary* query = [NSMutableDictionary new]; + query[(id)kSecClass] = (id)kSecClassGenericPassword; + query[(id)kSecAttrAccount] = name; + query[(id)kSecAttrService] = collection; + query[(id)kSecAttrAccessible] = (id)kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly; + query[(id)kSecValueData] = newValue; + + return SecItemAdd((CFDictionaryRef)query, nil); +} + +- (OSStatus)updateName:(NSString*)name withValue:(NSString*)value inCollection:(NSString*)collection { + NSData* newValue = [value dataUsingEncoding:NSUTF8StringEncoding]; + NSMutableDictionary* query = [NSMutableDictionary new]; + query[(id)kSecClass] = (id)kSecClassGenericPassword; + query[(id)kSecAttrAccount] = name; + query[(id)kSecAttrService] = collection; + query[(id)kSecAttrAccessible] = (id)kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly; + + return SecItemUpdate((CFDictionaryRef)query, (CFDictionaryRef)@{ + (id)kSecValueData: newValue + }); +} + +- (void)delete:(CDVInvokedUrlCommand*)command { + NSArray* names = [command argumentAtIndex:0]; + NSString* collection = [command argumentAtIndex:1 withDefault:@""]; + BOOL error = false; + + // Variable to be able to rollback changes if something fails. + NSMutableDictionary* deletedValues = [NSMutableDictionary new]; + + NSLog(@"SecureStorage: Delete values with names %@ in collection %@", names, collection); + + for (NSString* name in names) { + if ([name length] == 0) { + continue; + } + + NSString* storedValue = [self getValue:name inCollection:collection]; + if (storedValue == nil) { + continue; + } + + OSStatus status = [self deleteName:name fromCollection:collection]; + + if (status != errSecSuccess && status != errSecItemNotFound) { + NSLog(@"Error deleting entry with name %@ in collection %@. Status: %d", name, collection, (int) status); + error = true; + + // Rollback. + for (NSString *name in deletedValues) { + [self addName:name withValue:deletedValues[name] inCollection:collection]; + } + } else { + deletedValues[name] = storedValue; + } + } + + CDVPluginResult* pluginResult; + if (error) { + pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR + messageAsString:@"Error deleting one or more values from secure storage."]; + } else { + pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK]; + } + + [self.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId]; +} + +- (OSStatus)deleteName:(NSString*)name fromCollection:(NSString*)collection { + NSDictionary* query = @{ + (id)kSecClass: (id)kSecClassGenericPassword, + (id)kSecAttrAccount: name, + (id)kSecAttrService: collection, + }; + + return SecItemDelete((CFDictionaryRef)query); +} + +- (void)deleteCollection:(CDVInvokedUrlCommand*)command { + NSString* collection = [command argumentAtIndex:0 withDefault:@""]; + + if ([collection length] == 0) { + NSLog(@"SecureStorage: Collection cannot be empty in deleteCollection"); + CDVPluginResult* pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK]; + [self.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId]; + + return; + } + + NSLog(@"SecureStorage: Delete all values in collection %@", collection); + + NSDictionary* query = @{ + (id)kSecClass: (id)kSecClassGenericPassword, + (id)kSecAttrService: collection, + }; + + OSStatus status = SecItemDelete((CFDictionaryRef)query); + + CDVPluginResult* pluginResult; + if (status == errSecSuccess || status == errSecItemNotFound) { + pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK]; + } else { + NSLog(@"Error deleting all values in collection %@. Status: %d", collection, (int) status); + pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_IO_EXCEPTION + messageAsString:@"Error deleting values from secure storage."]; + } + + [self.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId]; +} + +@end diff --git a/cordova-plugin-moodleapp/src/ts/index.ts b/cordova-plugin-moodleapp/src/ts/index.ts index 27a15ad10..a11c3eba9 100644 --- a/cordova-plugin-moodleapp/src/ts/index.ts +++ b/cordova-plugin-moodleapp/src/ts/index.ts @@ -12,10 +12,10 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { SystemUI } from './plugins/SystemUI'; +import { SecureStorage } from './plugins/SecureStorage'; const api: MoodleAppPlugins = { - systemUI: new SystemUI(), + secureStorage: new SecureStorage(), }; // This is necessary to work around the default transpilation behavior, diff --git a/cordova-plugin-moodleapp/src/ts/plugins/SecureStorage.ts b/cordova-plugin-moodleapp/src/ts/plugins/SecureStorage.ts new file mode 100644 index 000000000..26e15a65a --- /dev/null +++ b/cordova-plugin-moodleapp/src/ts/plugins/SecureStorage.ts @@ -0,0 +1,85 @@ +// (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. + +/** + * Allows retrieving and storing items in a secure storage. + */ +export class 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]; + } + + return new Promise((resolve, reject) => { + cordova.exec(resolve, reject, 'SecureStorage', 'get', [names, collection]); + }); + } + + /** + * 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}`); + } + } + + await new Promise((resolve, reject) => { + cordova.exec(resolve, reject, 'SecureStorage', 'store', [data, collection]); + }); + } + + /** + * 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]; + } + + await new Promise((resolve, reject) => { + cordova.exec(resolve, reject, 'SecureStorage', 'delete', [names, collection]); + }); + } + + /** + * Delete all values for a certain collection. + * + * @param collection The collection to delete. + */ + async deleteCollection(collection: string): Promise { + await new Promise((resolve, reject) => { + cordova.exec(resolve, reject, 'SecureStorage', 'deleteCollection', [collection]); + }); + } + +} diff --git a/cordova-plugin-moodleapp/src/ts/plugins/SystemUI.ts b/cordova-plugin-moodleapp/src/ts/plugins/SystemUI.ts deleted file mode 100644 index 28494b2b1..000000000 --- a/cordova-plugin-moodleapp/src/ts/plugins/SystemUI.ts +++ /dev/null @@ -1,31 +0,0 @@ -// (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. - -/** - * Manages system UI settings. - */ -export class SystemUI { - - /** - * Set navigation bar color. - * - * @param color Color. - */ - async setNavigationBarColor(color: string): Promise { - await new Promise((resolve, reject) => { - cordova.exec(resolve, reject, 'SystemUI', 'setNavigationBarColor', [color]); - }); - } - -} diff --git a/cordova-plugin-moodleapp/types/index.d.ts b/cordova-plugin-moodleapp/types/index.d.ts index 77722c985..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 { SystemUI } from '../src/ts/plugins/SystemUI'; +import { SecureStorage as SecureStorageImpl } from '../src/ts/plugins/SecureStorage'; declare global { interface MoodleAppPlugins { - systemUI: SystemUI; + secureStorage: SecureStorageImpl; } interface Cordova { @@ -25,3 +25,5 @@ declare global { } } + +export type SecureStorage = InstanceType; diff --git a/package-lock.json b/package-lock.json index 6872e36fc..3fa03dfcd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4835,9 +4835,9 @@ } }, "@moodlehq/phonegap-plugin-push": { - "version": "4.0.0-moodle.6", - "resolved": "https://registry.npmjs.org/@moodlehq/phonegap-plugin-push/-/phonegap-plugin-push-4.0.0-moodle.6.tgz", - "integrity": "sha512-0ddoef5tXsCRfv8MKNfsUZdO+63GK+s8dYON8E8hFhO0RxEp1mhqeTj8vnxBvjyjl2IlVGUXiOPwshgabxoBRA==" + "version": "4.0.0-moodle.7", + "resolved": "https://registry.npmjs.org/@moodlehq/phonegap-plugin-push/-/phonegap-plugin-push-4.0.0-moodle.7.tgz", + "integrity": "sha512-Suzk1v4oLogcU/Xpm5Yl1KkzPwamGnzxculkYNIrqaAs6IZ6cSyZvIy0JoM98zOjGJBjKCSEYAAmtMZXLMGhjg==" }, "@mrmlnc/readdir-enhanced": { "version": "2.2.1", @@ -8617,6 +8617,7 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, "requires": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" @@ -9800,7 +9801,8 @@ "binary-extensions": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", - "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==" + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "dev": true }, "bindings": { "version": "1.5.0", @@ -10963,6 +10965,7 @@ "version": "3.5.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "dev": true, "requires": { "anymatch": "~3.1.2", "braces": "~3.0.2", @@ -10978,6 +10981,7 @@ "version": "2.3.2", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, "optional": true } } @@ -11144,6 +11148,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", + "dev": true, "requires": { "string-width": "^3.1.0", "strip-ansi": "^5.2.0", @@ -11153,12 +11158,14 @@ "ansi-regex": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", - "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==" + "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==", + "dev": true }, "ansi-styles": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, "requires": { "color-convert": "^1.9.0" } @@ -11166,17 +11173,20 @@ "emoji-regex": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", - "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==" + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", + "dev": true }, "is-fullwidth-code-point": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==" + "integrity": "sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==", + "dev": true }, "string-width": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dev": true, "requires": { "emoji-regex": "^7.0.1", "is-fullwidth-code-point": "^2.0.0", @@ -11187,6 +11197,7 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, "requires": { "ansi-regex": "^4.1.0" } @@ -11195,6 +11206,7 @@ "version": "5.1.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", + "dev": true, "requires": { "ansi-styles": "^3.2.0", "string-width": "^3.0.0", @@ -12182,6 +12194,115 @@ "version": "file:cordova-plugin-moodleapp", "dev": true, "dependencies": { + "@babel/runtime": { + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.22.6.tgz", + "integrity": "sha512-wDb5pWm4WDdF6LFUde3Jl8WzPA+3ZbxYqkC6xAXuD3irdEHN1k0NfTRrJD8ZD378SJ61miMLCqIOXYhd8x+AJQ==", + "requires": { + "regenerator-runtime": "^0.13.11" + } + }, + "@esbuild/darwin-x64": { + "version": "0.18.11", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.11.tgz", + "integrity": "sha512-N15Vzy0YNHu6cfyDOjiyfJlRJCB/ngKOAvoBf1qybG3eOq0SL2Lutzz9N7DYUbb7Q23XtHPn6lMDF6uWbGv9Fw==", + "optional": true + }, + "ansi-regex": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", + "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==" + }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "requires": { + "color-convert": "^1.9.0" + } + }, + "anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "requires": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + } + }, + "binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==" + }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "requires": { + "fill-range": "^7.0.1" + } + }, + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==" + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "requires": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "fsevents": "~2.3.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + } + }, "chokidar-cli": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/chokidar-cli/-/chokidar-cli-3.0.0.tgz", @@ -12189,29 +12310,451 @@ "requires": { "chokidar": "^3.5.2", "lodash.debounce": "^4.0.8", + "lodash.throttle": "^4.1.1", "yargs": "^13.3.0" } }, + "cliui": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", + "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", + "requires": { + "string-width": "^3.1.0", + "strip-ansi": "^5.2.0", + "wrap-ansi": "^5.1.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" + }, "concurrently": { "version": "8.2.0", "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-8.2.0.tgz", "integrity": "sha512-nnLMxO2LU492mTUj9qX/az/lESonSZu81UznYDoXtz1IQf996ixVqPAgHXwvHiHCAef/7S8HIK+fTFK7Ifk8YA==", "requires": { + "chalk": "^4.1.2", "date-fns": "^2.30.0", "lodash": "^4.17.21", + "rxjs": "^7.8.1", + "shell-quote": "^1.8.1", "spawn-command": "0.0.2", - "tree-kill": "^1.2.2" + "supports-color": "^8.1.1", + "tree-kill": "^1.2.2", + "yargs": "^17.7.2" + }, + "dependencies": { + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==" + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" + }, + "string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + } + }, + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "requires": { + "ansi-regex": "^5.0.1" + } + }, + "wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + }, + "y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==" + }, + "yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "requires": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + } + }, + "yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==" + } } }, + "date-fns": { + "version": "2.30.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", + "integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==", + "requires": { + "@babel/runtime": "^7.21.0" + } + }, + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==" + }, + "emoji-regex": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==" + }, "esbuild": { "version": "0.18.11", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.11.tgz", - "integrity": "sha512-i8u6mQF0JKJUlGR3OdFLKldJQMMs8OqM9Cc3UCi9XXziJ9WERM5bfkHaEAy0YAvPRMgqSW55W7xYn84XtEFTtA==" + "integrity": "sha512-i8u6mQF0JKJUlGR3OdFLKldJQMMs8OqM9Cc3UCi9XXziJ9WERM5bfkHaEAy0YAvPRMgqSW55W7xYn84XtEFTtA==", + "requires": { + "@esbuild/darwin-x64": "0.18.11" + } + }, + "escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==" + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "requires": { + "locate-path": "^3.0.0" + } + }, + "fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "optional": true + }, + "get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==" + }, + "glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "requires": { + "is-glob": "^4.0.1" + } + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "requires": { + "binary-extensions": "^2.0.0" + } + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==" + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==" + }, + "is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==" + }, + "locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "requires": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + } + }, + "lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, + "lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==" + }, + "lodash.throttle": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-4.1.1.tgz", + "integrity": "sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==" + }, + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==" + }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "requires": { + "p-limit": "^2.0.0" + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==" + }, + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==" + }, + "picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==" + }, + "readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "requires": { + "picomatch": "^2.2.1" + } + }, + "regenerator-runtime": { + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==" + }, + "require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==" + }, + "require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==" + }, + "rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "requires": { + "tslib": "^2.1.0" + } + }, + "set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==" + }, + "shell-quote": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.1.tgz", + "integrity": "sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==" + }, + "spawn-command": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/spawn-command/-/spawn-command-0.0.2.tgz", + "integrity": "sha512-zC8zGoGkmc8J9ndvml8Xksr1Amk9qBujgbF0JAIWO7kXr43w0h/0GJNM/Vustixu+YE8N/MTrQ7N31FvHUACxQ==" + }, + "string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + } + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "requires": { + "ansi-regex": "^4.1.0" + } + }, + "supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "requires": { + "has-flag": "^4.0.0" + } + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "requires": { + "is-number": "^7.0.0" + } + }, + "tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==" + }, + "tslib": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.0.tgz", + "integrity": "sha512-7At1WUettjcSRHXCyYtTselblcHl9PJFFVKiCAy/bY97+BPZXSQ2wbq0P9s8tK2G7dFQfNnlJnPAiArVBVBsfA==" }, "typescript": { "version": "5.1.6", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.1.6.tgz", "integrity": "sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==" + }, + "which-module": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", + "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==" + }, + "wrap-ansi": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", + "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", + "requires": { + "ansi-styles": "^3.2.0", + "string-width": "^3.0.0", + "strip-ansi": "^5.0.0" + } + }, + "y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==" + }, + "yargs": { + "version": "13.3.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz", + "integrity": "sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==", + "requires": { + "cliui": "^5.0.0", + "find-up": "^3.0.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^3.0.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^13.1.2" + } + }, + "yargs-parser": { + "version": "13.1.2", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz", + "integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==", + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } } } }, @@ -13034,6 +13577,7 @@ "version": "2.30.0", "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", "integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==", + "dev": true, "requires": { "@babel/runtime": "^7.21.0" } @@ -13070,7 +13614,8 @@ "decamelize": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==" + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "dev": true }, "decimal.js": { "version": "10.4.3", @@ -16013,7 +16558,8 @@ "get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==" + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true }, "get-intrinsic": { "version": "1.2.1", @@ -18091,6 +18637,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, "requires": { "binary-extensions": "^2.0.0" } @@ -21704,7 +22251,8 @@ "normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==" + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true }, "normalize-range": { "version": "0.1.2", @@ -24677,6 +25225,7 @@ "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, "requires": { "picomatch": "^2.2.1" } @@ -25031,7 +25580,8 @@ "require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==" + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true }, "require-from-string": { "version": "2.0.2", @@ -25041,7 +25591,8 @@ "require-main-filename": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", - "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==" + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "dev": true }, "requires-port": { "version": "1.0.0", @@ -26572,7 +27123,8 @@ "spawn-command": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/spawn-command/-/spawn-command-0.0.2.tgz", - "integrity": "sha512-zC8zGoGkmc8J9ndvml8Xksr1Amk9qBujgbF0JAIWO7kXr43w0h/0GJNM/Vustixu+YE8N/MTrQ7N31FvHUACxQ==" + "integrity": "sha512-zC8zGoGkmc8J9ndvml8Xksr1Amk9qBujgbF0JAIWO7kXr43w0h/0GJNM/Vustixu+YE8N/MTrQ7N31FvHUACxQ==", + "dev": true }, "spdx-correct": { "version": "3.2.0", @@ -27906,7 +28458,8 @@ "tree-kill": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", - "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==" + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true }, "ts-dedent": { "version": "2.2.0", @@ -30072,7 +30625,8 @@ "which-module": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", - "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==" + "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==", + "dev": true }, "which-typed-array": { "version": "1.1.9", @@ -30381,7 +30935,8 @@ "y18n": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", - "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==" + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", + "dev": true }, "yallist": { "version": "4.0.0", @@ -30398,6 +30953,7 @@ "version": "13.3.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz", "integrity": "sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==", + "dev": true, "requires": { "cliui": "^5.0.0", "find-up": "^3.0.0", @@ -30414,22 +30970,26 @@ "ansi-regex": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", - "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==" + "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==", + "dev": true }, "emoji-regex": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", - "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==" + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", + "dev": true }, "is-fullwidth-code-point": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==" + "integrity": "sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==", + "dev": true }, "string-width": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dev": true, "requires": { "emoji-regex": "^7.0.1", "is-fullwidth-code-point": "^2.0.0", @@ -30440,6 +31000,7 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, "requires": { "ansi-regex": "^4.1.0" } @@ -30450,6 +31011,7 @@ "version": "13.1.2", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz", "integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==", + "dev": true, "requires": { "camelcase": "^5.0.0", "decamelize": "^1.2.0" @@ -30458,7 +31020,8 @@ "camelcase": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==" + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true } } }, diff --git a/package.json b/package.json index a7e78e80a..0b62e4e83 100644 --- a/package.json +++ b/package.json @@ -87,7 +87,7 @@ "@moodlehq/cordova-plugin-statusbar": "4.0.0-moodle.2", "@moodlehq/cordova-plugin-zip": "3.1.0-moodle.1", "@moodlehq/ionic-native-push": "5.36.0-moodle.2", - "@moodlehq/phonegap-plugin-push": "4.0.0-moodle.6", + "@moodlehq/phonegap-plugin-push": "4.0.0-moodle.7", "@ngx-translate/core": "^13.0.0", "@ngx-translate/http-loader": "^6.0.0", "@types/chart.js": "^2.9.31", diff --git a/resources/android/xml/backup_rules.xml b/resources/android/xml/backup_rules.xml new file mode 100644 index 000000000..468ee0a6a --- /dev/null +++ b/resources/android/xml/backup_rules.xml @@ -0,0 +1,6 @@ + + + + + + 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 2220a621d..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.isAndroid()) { - 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); diff --git a/src/core/services/sites.ts b/src/core/services/sites.ts index fed018cee..265232db3 100644 --- a/src/core/services/sites.ts +++ b/src/core/services/sites.ts @@ -63,6 +63,7 @@ import { CoreConfig } from './config'; import { CoreNetwork } from '@services/network'; import { CoreUserGuestSupportConfig } from '@features/user/classes/support/guest-support-config'; import { CoreLang, CoreLangFormat } from '@services/lang'; +import { CoreNative } from '@features/native/services/native'; export const CORE_SITE_SCHEMAS = new InjectionToken('CORE_SITE_SCHEMAS'); export const CORE_SITE_CURRENT_SITE_ID_CONFIG = 'current_site_id'; @@ -821,16 +822,22 @@ export class CoreSitesProvider { config?: CoreSiteConfig, oauthId?: number, ): Promise { - await this.sitesTable.insert({ + const promises: Promise[] = []; + const site: SiteDBEntry = { id, siteUrl, - token, + token: '', info: info ? JSON.stringify(info) : undefined, - privateToken, + privateToken: '', config: config ? JSON.stringify(config) : undefined, loggedOut: 0, oauthId, - }); + }; + + promises.push(this.sitesTable.insert(site)); + promises.push(this.storeTokensInSecureStorage(id, token, privateToken)); + + await Promise.all(promises); } /** @@ -1058,15 +1065,14 @@ export class CoreSitesProvider { // Site DB deleted, now delete the app from the list of sites. delete this.sites[siteId]; - try { - await this.sitesTable.deleteByPrimaryKey({ id: siteId }); - } catch (err) { - // DB remove shouldn't fail, but we'll go ahead even if it does. - } + // DB remove shouldn't fail, but we'll go ahead even if it does. + await CoreUtils.ignoreErrors(this.sitesTable.deleteByPrimaryKey({ id: siteId })); // Site deleted from sites list, now delete the folder. await site.deleteFolder(); + await CoreUtils.ignoreErrors(CoreNative.plugin('secureStorage').deleteCollection(siteId)); + CoreEvents.trigger(CoreEvents.SITE_DELETED, site, siteId); } @@ -1107,7 +1113,7 @@ export class CoreSitesProvider { // Retrieve and create the site. let record: SiteDBEntry; try { - record = await this.sitesTable.getOneByPrimaryKey({ id: siteId }); + record = await this.loadSiteTokens(await this.sitesTable.getOneByPrimaryKey({ id: siteId })); } catch { throw new CoreError('SiteId not found.'); } @@ -1144,7 +1150,7 @@ export class CoreSitesProvider { * @returns Promise resolved with the site. */ async getSiteByUrl(siteUrl: string): Promise { - const data = await this.sitesTable.getOne({ siteUrl }); + const data = await this.loadSiteTokens(await this.sitesTable.getOne({ siteUrl })); return this.addSiteFromSiteListEntry(data); } @@ -1491,14 +1497,17 @@ export class CoreSitesProvider { site.privateToken = privateToken; site.setLoggedOut(false); // Token updated means the user authenticated again, not logged out anymore. - await this.sitesTable.update( - { - token, - privateToken, - loggedOut: 0, - }, - { id: siteId }, - ); + const promises: Promise[] = []; + const newData: Partial = { + token: '', + privateToken: '', + loggedOut: 0, + }; + + promises.push(this.sitesTable.update(newData, { id: siteId })); + promises.push(this.storeTokensInSecureStorage(siteId, token, privateToken)); + + await Promise.all(promises); } /** @@ -1601,6 +1610,8 @@ export class CoreSitesProvider { const ids: string[] = []; await Promise.all(siteEntries.map(async (site) => { + site = await this.loadSiteTokens(site); + await this.addSiteFromSiteListEntry(site); if (this.sites[site.id].containsUrl(url)) { @@ -1962,6 +1973,80 @@ export class CoreSitesProvider { return this.schemasTables[siteId]; } + /** + * Move all tokens stored in DB to a secure storage. + */ + async moveTokensToSecureStorage(): Promise { + const sites = await this.sitesTable.getMany(); + + await Promise.all(sites.map(async site => { + if (!site.token && !site.privateToken) { + return; // Tokens are empty, no need to treat them. + } + + try { + await this.storeTokensInSecureStorage(site.id, site.token, site.privateToken); + } catch { + this.logger.error('Error storing tokens in secure storage for site ' + site.id); + } + })); + + // Remove tokens from DB even if they couldn't be stored in secure storage. + await this.sitesTable.update({ token: '', privateToken: '' }); + } + + /** + * Get tokens from secure storage. + * + * @param siteId Site ID. + * @returns Stored tokens. + */ + protected async getTokensFromSecureStorage(siteId: string): Promise<{ token: string; privateToken?: string }> { + const result = await CoreNative.plugin('secureStorage').get(['token', 'privateToken'], siteId); + + return { + token: result?.token ?? '', + privateToken: result?.privateToken ?? undefined, + }; + } + + /** + * Store tokens in secure storage. + * + * @param siteId Site ID. + * @param token Site token. + * @param privateToken Site private token. + */ + protected async storeTokensInSecureStorage( + siteId: string, + token: string, + privateToken?: string, + ): Promise { + await CoreNative.plugin('secureStorage').store({ + token: token, + privateToken: privateToken ?? '', + }, siteId); + } + + /** + * Given a site, load its tokens if needed. + * + * @param site Site data. + * @returns Site with tokens loaded. + */ + protected async loadSiteTokens(site: SiteDBEntry): Promise { + if (site.token) { + return site; + } + + const tokens = await this.getTokensFromSecureStorage(site.id); + + return { + ...site, + ...tokens, + }; + } + } export const CoreSites = makeSingleton(CoreSitesProvider); diff --git a/src/core/services/update-manager.ts b/src/core/services/update-manager.ts index 3093e938b..e7015b1b6 100644 --- a/src/core/services/update-manager.ts +++ b/src/core/services/update-manager.ts @@ -89,6 +89,10 @@ export class CoreUpdateManagerProvider { promises.push(this.upgradeFontSizeNames()); } + if (versionCode >= 43000 && versionApplied < 43000 && versionApplied > 0) { + promises.push(CoreSites.moveTokensToSecureStorage()); + } + try { await Promise.all(promises);