diff --git a/package.json b/package.json index ac5b5ef..9c09eaf 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "lib-iitc-manager", - "version": "1.6.1", + "version": "1.7.0", "description": "Library for managing IITC plugins", "main": "src/index.js", "type": "module", @@ -21,15 +21,16 @@ "homepage": "https://github.com/IITC-CE/lib-iitc-manager#readme", "devDependencies": { "chai": "^4.3.6", - "http-server": "^14.1.0", - "mocha": "^9.2.2", - "node-fetch": "^3.2.3", "eslint": "^8.20.0", "eslint-config-prettier": "^8.5.0", "eslint-plugin-prettier": "^4.2.1", + "http-server": "^14.1.0", + "mocha": "^9.2.2", + "node-fetch": "^3.2.3", "prettier": "^2.7.1" }, "dependencies": { + "@bundled-es-modules/deepmerge": "^4.3.1", "xhr2": "^0.2.1" } } diff --git a/src/backup.js b/src/backup.js new file mode 100644 index 0000000..62a4c58 --- /dev/null +++ b/src/backup.js @@ -0,0 +1,212 @@ +// @license magnet:?xt=urn:btih:1f739d935676111cfff4b4693e3816e664797050&dn=gpl-3.0.txt GPL-v3 + +import { parseMeta } from './helpers.js'; +import deepmerge from '@bundled-es-modules/deepmerge'; + +/** + * Processes the input parameters for backup data retrieval. + * + * This function takes an input object containing parameters for backup data retrieval + * and returns a new object with processed parameters. If the input parameters are not + * an object, an empty object is used as the default value. The function combines the + * input parameters with default parameters to ensure all required properties are present. + * + * @param {BackupParams} params - The parameters for setting the backup data. + * @returns {Object} The processed parameters object. + */ +export function paramsProcessing(params) { + if (typeof params !== 'object') params = {}; + + // Default parameters + const default_params = { + settings: false, + data: false, + external: false, + }; + + // Combine the default parameters with the input parameters using spread syntax + return { ...default_params, ...params }; +} + +/** + * Exports specific IITC settings from the provided storage object. + * + * This function takes a storage object and extracts specific IITC settings based on + * predefined keys. It creates a new object containing only the specified IITC settings + * and returns it. + * + * @param {Object} all_storage - The storage object containing all data. + * @returns {Object} An object containing specific IITC settings. + */ +export const exportIitcSettings = (all_storage) => { + const iitc_settings = {}; + + // An array of predefined keys for IITC settings + const storage_keys = ['channel', 'network_host', 'release_update_check_interval', 'beta_update_check_interval', 'custom_update_check_interval']; + + // Loop through all_storage and check if the keys are present in storage_keys + // If present, add them to the iitc_settings object + for (const key in all_storage) { + if (storage_keys.includes(key)) { + iitc_settings[key] = all_storage[key]; + } + } + return iitc_settings; +}; + +/** + * Exports specific plugin settings from the provided storage object. + * + * This function takes a storage object and extracts plugin settings that have keys starting + * with the prefix 'VMin'. It creates a new object containing only the plugin settings + * and returns it. + * + * @param {Object} all_storage - The storage object containing all data. + * @returns {Object} An object containing specific plugin settings. + */ +export const exportPluginsSettings = (all_storage) => { + const plugins_storage = {}; + + // Loop through all_storage and check if the keys start with the prefix 'VMin' + // If so, add them to the plugins_storage object + for (const key in all_storage) { + if (key.startsWith('VMin')) { + plugins_storage[key] = all_storage[key]; + } + } + return plugins_storage; +}; + +/** + * Exports external plugins from the provided storage object. + * + * This function takes a storage object and extracts external plugins based on predefined keys. + * It creates a new object containing the external plugins organized by their channels and filenames, + * and returns it. + * + * @param {Object} all_storage - The storage object containing all data. + * @returns {Object} An object containing external plugins organized by channels and filenames. + */ +export const exportExternalPlugins = (all_storage) => { + const external_plugins = {}; + + // An array of predefined keys for external plugins + const storage_keys = ['release_plugins_user', 'beta_plugins_user', 'custom_plugins_user']; + + // Loop through all_storage and check if the keys are present in storage_keys + // If present, process and add the external plugins to the external_plugins object + for (const key in all_storage) { + if (storage_keys.includes(key)) { + // Extract the channel name from the key by splitting at '_' + const channel = key.split('_')[0]; + external_plugins[channel] = {}; + + // Loop through each plugin UID in the current key's storage data + for (const plugin_uid in all_storage[key]) { + // Get the plugin's filename and code from the storage data and add to the external_plugins object + const plugin_filename = all_storage[key][plugin_uid]['filename']; + external_plugins[channel][plugin_filename] = all_storage[key][plugin_uid]['code']; + } + } + } + + return external_plugins; +}; + +/** + * Imports IITC settings from the provided backup object. + * + * @async + * @param {Object} self - IITC manager object. + * @param {Object} backup - The backup object containing IITC settings to import. + * @returns {Promise} A promise that resolves when the import is complete. + */ +export const importIitcSettings = async (self, backup) => { + const backup_obj = Object.assign({}, backup); + const default_channel = self.channel; + + // Set the IITC settings from the backup object into the storage + await self.storage.set(backup_obj); + + // Check if the channel in the backup object is different from the original channel + const set_channel = backup_obj.channel; + if (set_channel !== default_channel) { + await self.setChannel(set_channel); + } +}; + +/** + * Imports plugin settings from the provided backup object. + * + * The function first retrieves all data from the storage object + * using `self.storage.get(null)` and filters out the records with keys starting with 'VMin' + * to create a new object `vMinRecords` containing only plugin-related data. The function then + * merges the `vMinRecords` object with the provided backup object using the `deepmerge` library, + * resulting in a new storage object `new_storage` that contains updated plugin settings. Finally, + * the updated storage object is set into the 'self' object using `self.storage.set()`. + * + * @async + * @param {Object} self - IITC manager object. + * @param {Object} backup - The backup object containing plugin settings to import. + * @returns {Promise} A promise that resolves when the import is complete. + */ +export const importPluginsSettings = async (self, backup) => { + const all_storage = await self.storage.get(null); + + // Create a new object containing only plugin-related data (keys starting with 'VMin') + const vMinRecords = {}; + Object.keys(all_storage).forEach((key) => { + if (key.startsWith('VMin')) { + vMinRecords[key] = all_storage[key]; + } + }); + + // Merge the 'vMinRecords' object with the provided backup object and set into storage + const new_storage = deepmerge(vMinRecords, backup); + await self.storage.set(new_storage); +}; + +/** + * Imports external plugins from the provided backup object. + * + * The function iterates through each channel in the backup object, + * sets the current channel using `self.setChannel()`, and then extracts the plugin information + * (metadata and code) for each plugin in the channel. The plugin information is added to the 'scripts' + * array, which is then passed to `self.addUserScripts()` to add the external plugins. After processing + * all channels, the function sets the default channel using `self.setChannel()` if it was changed during + * the import process. + * + * @async + * @param {Object} self - IITC manager object. + * @param {Object} backup - The backup object containing external plugins to import. + * @returns {Promise} A promise that resolves when the import is complete. + */ +export const importExternalPlugins = async (self, backup) => { + const default_channel = self.channel; + + // Iterate through each channel in the backup object + for (const channel of Object.keys(backup)) { + // Initialize an empty array to store the plugin information (metadata and code) + const scripts = []; + await self.setChannel(channel); + + // Iterate through each plugin in the current channel and extract plugin information + for (const [filename, code] of Object.entries(backup[channel])) { + // Parse the metadata from the plugin code using the 'parseMeta()' function + const meta = parseMeta(code); + meta['filename'] = filename; + + // Push the plugin information (metadata and code) to the 'scripts' array + scripts.push({ meta: meta, code: code }); + } + + // Add the external plugins using the 'self.addUserScripts()' method + await self.addUserScripts(scripts); + } + + // If the current channel is different from the default channel, + // set the default channel using the 'self.setChannel()' method + if (self.channel !== default_channel) { + await self.setChannel(default_channel); + } +}; diff --git a/src/manager.js b/src/manager.js index bc9f44e..85d24bc 100644 --- a/src/manager.js +++ b/src/manager.js @@ -3,6 +3,7 @@ import { Worker } from './worker.js'; import * as migrations from './migrations.js'; import { getUID, isSet } from './helpers.js'; +import * as backup from './backup.js'; /** * @classdesc This class contains methods for managing IITC and plugins. @@ -298,4 +299,59 @@ export class Manager extends Worker { if (all_plugins === undefined) return null; return all_plugins[uid]; } + + /** + * Asynchronously retrieves backup data based on the specified parameters. + * + * @async + * @param {BackupParams} params - The parameters for the backup data retrieval. + * @return {Promise} A promise that resolves to the backup data. + */ + async getBackupData(params) { + // Process the input parameters using the 'paramsProcessing' function from the 'backup' module. + params = backup.paramsProcessing(params); + + // Initialize the backup_data object with its properties. + const backup_data = { + external_plugins: {}, + data: { + iitc_settings: {}, + plugins_data: {}, + app: 'IITC Button', + }, + }; + + // Retrieve all_storage using the 'get' method of 'storage' module. + const all_storage = await this.storage.get(null); + + if (params.settings) backup_data.data.iitc_settings = backup.exportIitcSettings(all_storage); + if (params.data) backup_data.data.plugins_data = backup.exportPluginsSettings(all_storage); + if (params.external) backup_data.external_plugins = backup.exportExternalPlugins(all_storage); + + // Return the backup_data object. + return backup_data; + } + + /** + * Asynchronously sets backup data based on the specified parameters. + * + * This function takes the provided parameters and backup data object and sets the data + * accordingly. The input parameters are processed using the 'paramsProcessing' function + * from the 'backup' module. Depending on the parameters, the function imports IITC settings, + * plugin data, and external plugins into the 'this' object using appropriate functions from + * the 'backup' module. + * + * @async + * @param {BackupParams} params - The parameters for setting the backup data. + * @param {object} backup_data - The backup data object containing the data to be set. + * @return {Promise} A promise that resolves when the backup data is set. + */ + async setBackupData(params, backup_data) { + // Process the input parameters using the 'paramsProcessing' function from the 'backup' module. + params = backup.paramsProcessing(params); + + if (params.settings) await backup.importIitcSettings(this, backup_data.data.iitc_settings); + if (params.data) await backup.importPluginsSettings(this, backup_data.data.plugins_data); + if (params.external) await backup.importExternalPlugins(this, backup_data.external_plugins); + } } diff --git a/src/worker.js b/src/worker.js index f76ab54..e174567 100644 --- a/src/worker.js +++ b/src/worker.js @@ -141,6 +141,15 @@ import { ajaxGet, clearWait, getUID, isSet, parseMeta, wait } from './helpers.js * @property {string[]} grant */ +/** + * Parameters for retrieving backup data. + * + * @typedef {Object} BackupParams + * @property {boolean} settings - Whether to import/export IITC settings. + * @property {boolean} data - Whether to import/export plugins' data. + * @property {boolean} external - Whether to import/export external plugins. + */ + /** * @classdesc This class contains methods for managing IITC and plugins. */ diff --git a/test/manager.0.base.spec.js b/test/manager.0.base.spec.js index 3a07708..9375063 100644 --- a/test/manager.0.base.spec.js +++ b/test/manager.0.base.spec.js @@ -8,6 +8,7 @@ import { expect } from 'chai'; describe('manage.js base integration tests', function () { let manager = null; before(function () { + storage.resetStorage(); const params = { storage: storage, channel: 'beta', diff --git a/test/manager.1.build-in.spec.js b/test/manager.1.build-in.spec.js index 937e932..8d7f8b3 100644 --- a/test/manager.1.build-in.spec.js +++ b/test/manager.1.build-in.spec.js @@ -8,6 +8,7 @@ import { expect } from 'chai'; describe('manage.js build-in plugins integration tests', function () { let manager = null; before(function () { + storage.resetStorage(); const params = { storage: storage, channel: 'release', diff --git a/test/manager.2.external.spec.js b/test/manager.2.external.spec.js index 2cb0823..5804001 100644 --- a/test/manager.2.external.spec.js +++ b/test/manager.2.external.spec.js @@ -21,6 +21,7 @@ const expectThrowsAsync = async (method, errorMessage) => { describe('manage.js external plugins integration tests', function () { let manager = null; before(function () { + storage.resetStorage(); const params = { storage: storage, channel: 'release', @@ -387,7 +388,9 @@ describe('manage.js external plugins integration tests', function () { expect(db_data['release_plugins_flat'][external_1_uid]['status'], "release_plugins_flat['status']: " + external_1_uid).to.equal('off'); - expect(db_data['release_plugins_flat'][external_1_uid]['code'], "release_plugins_flat['code']: " + external_1_uid).to.have.lengthOf(578); + expect(db_data['release_plugins_flat'][external_1_uid]['code'], "release_plugins_flat['code']: " + external_1_uid).to.have.lengthOf( + external_code.length + ); expect(db_data['release_plugins_flat'][external_1_uid]['override'], "release_plugins_flat['override']: " + external_1_uid).to.not.be.true; }); diff --git a/test/manager.3.repo.spec.js b/test/manager.3.repo.spec.js index dd82b9a..9869491 100644 --- a/test/manager.3.repo.spec.js +++ b/test/manager.3.repo.spec.js @@ -8,6 +8,7 @@ import { expect } from 'chai'; describe('manage.js custom repo integration tests', function () { let manager = null; before(function () { + storage.resetStorage(); const params = { storage: storage, channel: 'release', diff --git a/test/manager.9.backup.spec.js b/test/manager.9.backup.spec.js new file mode 100644 index 0000000..d1c85f3 --- /dev/null +++ b/test/manager.9.backup.spec.js @@ -0,0 +1,166 @@ +// @license magnet:?xt=urn:btih:1f739d935676111cfff4b4693e3816e664797050&dn=gpl-3.0.txt GPL-v3 + +import { describe, it, before } from 'mocha'; +import { Manager } from '../src/manager.js'; +import storage from '../test/storage.js'; +import { expect } from 'chai'; + +describe('getBackupData and setBackupData', function () { + let manager = null; + const first_plugin_uid = 'Available AP statistics+https://github.com/IITC-CE/ingress-intel-total-conversion'; + const external_code = '// ==UserScript==\n// @name IITC plugin\n// ==/UserScript==\nreturn false;'; + const initialBackupData = { + external_plugins: { + beta: {}, + custom: {}, + release: { + 'bookmarks1.user.js': external_code, + }, + }, + data: { + iitc_settings: { + channel: 'release', + network_host: { + release: 'http://127.0.0.1:31606/release', + beta: 'http://127.0.0.1:31606/beta', + custom: 'http://127.0.0.1/', + }, + }, + plugins_data: { + VMin5555: 'test', + }, + app: 'IITC Button', + }, + }; + + const backupData = { + external_plugins: { + release: { + 'bookmarks2.user.js': external_code, + }, + beta: { + 'bookmarks3.user.js': external_code, + }, + }, + data: { + iitc_settings: { + channel: 'beta', + }, + plugins_data: { + VMin5555: 'backup1', + VMin9999: 'backup2', + }, + app: 'IITC Button', + }, + }; + + before(async function () { + storage.resetStorage(); + const params = { + storage: storage, + channel: 'release', + network_host: { + release: 'http://127.0.0.1:31606/release', + beta: 'http://127.0.0.1:31606/beta', + custom: 'http://127.0.0.1/', + }, + inject_user_script: function callBack(data) { + expect(data).to.include('// ==UserScript=='); + }, + inject_plugin: function callBack(data) { + expect(data['code']).to.include('// ==UserScript=='); + }, + progressbar: function callBack(is_show) { + expect(is_show).to.be.oneOf([true, false]); + }, + is_daemon: false, + }; + manager = new Manager(params); + }); + + describe('run', function () { + it('Should not return an error', async function () { + const run = await manager.run(); + expect(run).to.be.undefined; + }); + }); + + describe('Enable plugins and add plugin settings data', function () { + it('Enable first plugin', async function () { + const run = await manager.managePlugin(first_plugin_uid, 'on'); + expect(run).to.be.undefined; + }); + it('Add external plugin', async function () { + const scripts = [ + { + meta: { + id: 'bookmarks1', + namespace: 'https://github.com/IITC-CE/ingress-intel-total-conversion', + name: 'Bookmarks for maps and portals', + category: 'Controls', + filename: 'bookmarks1.user.js', + }, + code: external_code, + }, + ]; + const installed = { + 'Bookmarks for maps and portals+https://github.com/IITC-CE/ingress-intel-total-conversion': { + uid: 'Bookmarks for maps and portals+https://github.com/IITC-CE/ingress-intel-total-conversion', + id: 'bookmarks1', + namespace: 'https://github.com/IITC-CE/ingress-intel-total-conversion', + name: 'Bookmarks for maps and portals', + category: 'Controls', + status: 'on', + user: true, + code: external_code, + filename: 'bookmarks1.user.js', + }, + }; + const run = await manager.addUserScripts(scripts); + expect(run).to.deep.equal(installed); + }); + it('Add plugin settings data', async function () { + await storage.set({ VMin5555: 'test' }); + }); + }); + + describe('getBackupData', function () { + it('Should return the correct backup data', async function () { + const backupDataFromManager = await manager.getBackupData({ + settings: true, + data: true, + external: true, + }); + expect(backupDataFromManager).to.deep.equal(initialBackupData); + }); + }); + + describe('setBackupData', function () { + it('Should set the backup data correctly', async function () { + await manager.setBackupData( + { + settings: true, + data: true, + external: true, + }, + backupData + ); + + // Check if the data was set correctly in storage + expect(manager.channel).to.equal('beta'); + + const pluginsData = await storage.get(['VMin5555', 'VMin9999']); + expect(pluginsData).to.deep.equal({ + VMin5555: 'backup1', + VMin9999: 'backup2', + }); + + const externalPlugins = await storage.get(['release_plugins_user', 'beta_plugins_user']); + expect(externalPlugins['release_plugins_user']).to.have.all.keys( + 'Bookmarks for maps and portals+https://github.com/IITC-CE/ingress-intel-total-conversion', + 'bookmarks2.user.js+IITC plugin' + ); + expect(externalPlugins['beta_plugins_user']).to.have.all.keys('bookmarks3.user.js+IITC plugin'); + }); + }); +}); diff --git a/test/storage.js b/test/storage.js index 79d2699..33d4cbd 100644 --- a/test/storage.js +++ b/test/storage.js @@ -1,13 +1,25 @@ // @license magnet:?xt=urn:btih:1f739d935676111cfff4b4693e3816e664797050&dn=gpl-3.0.txt GPL-v3 const store = {}; + export default { + /** + * Converts the input key to an array if it is a string. + * @param {string|Array} key - The key or keys to be converted to an array. + * @returns {Array} An array containing the keys. + */ _one_to_array: function (key) { if (typeof key === 'string') { key = [key]; } return key; }, + + /** + * Retrieves the value associated with the given key from the store. + * @param {string} key - The key for the value to be retrieved. + * @returns {any} The value associated with the key, or null if the key is not found. + */ _get_one(key) { const value = key in store ? store[key] : null; @@ -19,6 +31,12 @@ export default { return value; } }, + + /** + * Sets the value associated with the given key in the store. + * @param {string} key - The key for the value to be set. + * @param {any} value - The value to be stored. + */ _set_one(key, value) { if (typeof value !== 'string') { value = JSON.stringify(value); @@ -26,7 +44,20 @@ export default { store[key] = value; }, + /** + * Retrieves data from the store for the specified keys. + * If 'keys' is null, returns a copy of all data in the store. + * @param {null|string|Array} keys - The key or keys for the data to be retrieved. + * @returns {Object} An object containing the data associated with the keys. + */ async get(keys) { + if (keys === null) { + const data = {}; + for (const key in store) { + data[key] = this._get_one(key); + } + return data; + } keys = this._one_to_array(keys); if (Array.isArray(keys)) { const data = {}; @@ -38,6 +69,11 @@ export default { console.error('Unexpected type of key when trying to get storage value: ' + typeof keys); } }, + + /** + * Sets data in the store for the specified keys and values. + * @param {Object} obj - An object containing the keys and values to be stored. + */ async set(obj) { if (typeof obj === 'object') { Object.entries(obj).forEach((entry) => { @@ -48,4 +84,13 @@ export default { console.error('Unexpected type of key when trying to set storage value: ' + typeof obj); } }, + + /** + * Resets the storage by removing all data from the store. + */ + resetStorage() { + for (const key in store) { + delete store[key]; + } + }, };