From 13f191fe0bbfdcb60048d5f1c13184bad2881699 Mon Sep 17 00:00:00 2001 From: Julian Knight <1591850+TotallyInformation@users.noreply.github.com> Date: Tue, 22 Aug 2023 14:03:51 +0100 Subject: [PATCH 01/85] Comment --- nodes/libs/web.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/nodes/libs/web.js b/nodes/libs/web.js index db10bdce..8f70b2a9 100644 --- a/nodes/libs/web.js +++ b/nodes/libs/web.js @@ -718,6 +718,7 @@ class UibWeb { // TODO: X-XSS-Protection only needed for html (and js?), not for css, etc res + // Headers only accessible in the browser via web workers .header({ // Help reduce risk of XSS and other attacks 'X-XSS-Protection': '1;mode=block', @@ -937,7 +938,7 @@ class UibWeb { uibuilder Instance Debug Page - + \n
\n

uibuilder Detailed Information Page

\n

\n Note that this page is only accessible to users with Node-RED admin authority.\n

\n `\n\n /** Index of uibuilder instances */\n page += `\n

Index of uibuilder pages

\n

'Folders' refer to locations on your Node-RED's server. 'Paths' refer to URL's in the browser.

\n \n \n \n \n \n \n `\n Object.keys(uib.instances).forEach(key => {\n page += `\n \n \n \n \n \n `\n })\n page += `\n
URLSource Node Instance (2)Server Filing System Folder
${uib.instances[key]}${key}${path.join(rootFolder, uib.instances[key])}
\n

Notes:

\n
    \n
  1. \n Each instance of uibuilder uses its own socket.io namespace that matches httpNodeRoot/url. \n You can use this to manually send messages to your user interface.\n
  2. \n
  3. \n Paste the Source Node Instance into the search feature in the Node-RED admin ui to find the instance.\n The \"Filing System Folder\" shows you where the front-end (client browser) code lives.\n
  4. \n
\n `\n\n /** Table of Vendor Libraries available */\n page += `\n

Vendor Client Libraries

\n

\n You can include these libraries (packages) in any uibuilder served web page.\n Note though that you need to find out the correct file and relative folder either by looking on \n your Node-RED server in the location shown or by looking at the packages source online.\n

\n \n \n \n \n \n \n \n `\n const installedPackages = packageMgt.uibPackageJson.uibuilder.packages\n Object.keys(installedPackages).forEach(packageName => {\n const pj = installedPackages[packageName]\n\n page += `\n \n \n \n \n \n \n `\n })\n page += `\n
PackageVersionBrowser Entry Point (est.) (1) (2)Server Filing System Folder
${packageName}${pj.installedVersion}${pj.url}${pj.installFolder}
\n

Notes:

\n
    \n
  1. \n Always use relative URL's. All vendor URL's start ../uibuilder/vendor/, \n all uibuilder and custom file URL's start ./.
    \n Using relative URL's saves you from needing to worry about http(s), ip names/addresses and port numbers.\n
  2. \n
  3. \n The 'Main Entry Point' shown is usually a JavaScript file that you will want in your index.html. \n However, because this is reported by the authors of the package, it may refer to something completely different, \n uibuilder has no way of knowing. Treat it as a hint rather than absolute truth. Check the packages documentation \n for the correct library files to load.\n
  4. \n
\n `\n\n /** Configuration info */\n page += `\n

Configuration

\n\n

uibuilder

\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
uibuilder Version${uib.version}
uib.rootFolder${rootFolder}All uibuilder data lives here
uib.configFolder${uib.configFolder}uibuilder Global Configuration Folder
uib.commonFolder${uib.commonFolder}Used for loading common resources between multiple uibuilder instances
Common URL../${uib.moduleName}/commonThe common folder maps to this URL
uib_socketPath${sockets.uib_socketPath}Unique path given to Socket.IO to ensure isolation from other Nodes that might also use it
uib.masterTemplateFolder${uib.masterTemplateFolder}The built-in source templates, can be copied to any instance
\n\n

Configuration Files

\n\n

All are kept in the master configuration folder: ${uib.configFolder}

\n\n
\n
${uib.sioUseMwName}
\n
Custom Socket.IO Middleware file, also uibMiddleware.js.
\n
uibMiddleware.js
\n
Custom ExpressJS Middleware file.
\n
\n\n

Dump of all uib master configuration settings

\n
${tilib.syntaxHighlight( uibSummary )}
\n\n

Dump of all uib settings.js entries

\n
${tilib.syntaxHighlight( RED.settings.uibuilder ? RED.settings.uibuilder : 'NOT DEFINED' )}
\n\n

Node-RED

\n

See the <userDir>/settings.js file and the \n Node-RED documentation for details.

\n \n \n \n \n \n
userDir${RED.settings.userDir}
httpNodeRoot${uib.nodeRoot}
Node-RED Version${RED.settings.version}
Min. Version Required by uibuilder${uib.me['node-red'].version}
\n\n

Node.js

\n \n \n \n
Version${uib.nodeVersion.join('.')}
Min. version required by uibuilder${uib.me.engines.node}
\n `\n\n /** ExpressJS Configuration info */\n page += `\n

ExpressJS Configuration

\n

\n See the ExpressJS documentation for details.\n Note that ExpressJS Views are not current used by uibuilder\n

\n \n \n \n \n
Views Folder${web.app.get('views')}
Views Engine${web.app.get('view engine')}
Views Cache${web.app.get('view cache')}
\n

app.locals

\n
${tilib.syntaxHighlight( web.app.locals )}
\n

app.mountpath

\n
${tilib.syntaxHighlight( web.app.mountpath )}
\n `\n\n // Show the ExpressJS paths currently defined\n // web.routers contains descriptive info on routes, routes.user contains the ExpressJS route stack info.\n // console.log('>> web >>', web.htmlBuildTable( web.routers.user, ['name', 'desc', 'path', 'type', 'folder'] ))\n page += `\n

uibuilder ExpressJS Routes

\n

These tables show all of the web URL routes for uibuilder.

\n

User-Facing Routes

\n ${web.htmlBuildTable( web.routers.user, ['name', 'desc', 'path', 'type', 'folder'] )}\n

ExpressJS technical route data for admin routes

\n
Application Routes (../*)
\n ${web.htmlBuildTable( routes.user.app, ['name', 'path', 'folder', 'route'] )}\n
uibuilder generic Routes (../uibuilder/*)
\n ${web.htmlBuildTable( routes.user.uibRouter, ['name', 'path', 'folder', 'route'] )}\n
Vendor Routes (../uibuilder/vendor/*)
\n ${web.htmlBuildTable( routes.user.vendorRouter, ['name', 'path', 'folder', 'route'] )}\n
\n

Per-Instance User-Facing Routes

\n `\n Object.keys(routes.instances).forEach( url => {\n page += `\n

${url}

\n ${web.htmlBuildTable( web.routers.instances[url], ['name', 'desc', 'path', 'type', 'folder'] )}\n
ExpressJS technical route data for ${url} (../${url}/*)
\n ${web.htmlBuildTable( routes.instances[url], ['name', 'path', 'folder', 'route'] )}\n `\n } )\n page += `\n
\n

Admin-Facing Routes

\n ${web.htmlBuildTable( web.routers.admin, ['name', 'desc', 'path', 'type', 'folder'] )}\n

ExpressJS technical route data for admin routes

\n
Node-RED Admin Routes (../*)
\n

Note: Shows ALL Node-RED top-level admin routes, not just uibuilder

\n ${web.htmlBuildTable( routes.admin.app, ['name', 'path', 'folder', 'route'] )}\n
Admin uibuilder Routes (../uibuilder/*)
\n ${web.htmlBuildTable( routes.admin.admin, ['name', 'path', 'folder', 'route'] )}\n
Admin v3 API Routes (../uibuilder/admin)
\n

Note: This route uses the following methods: all, get, put, post, delete.

\n ${web.htmlBuildTable( routes.admin.v3, ['name', 'path', 'folder', 'route'] )}\n
Admin v2 API Routes (../uibuilder/*)
\n ${web.htmlBuildTable( routes.admin.v2, ['name', 'path', 'folder', 'route'] )}\n `\n\n page += '
'\n\n return page\n}\n\n/** Return a router but allow parameters to be passed in\n * @param {uibConfig} uib Reference to uibuilder's master uib object\n * @param {*} log Reference to uibuilder's log functions\n * @returns {express.Router} The v3 admin API ExpressJS router\n */\nfunction adminRouterV2(uib, log) {\n\n /** uibuilder v3 unified Admin API router - new API commands should be added here */\n // admin_Router_V3.route('/:url')\n\n const RED = uib.RED\n\n /** Create a simple NR admin API to return the content of a file in the `/uibuilder//src` folder\n * @param {string} url The admin api url to create\n * @param {object} permissions The permissions required for access\n * @param {Function} cb\n **/\n v2AdminRouter.get('/uibgetfile', function(/** @type {express.Request} */ req, /** @type {express.Response} */ res) {\n //#region --- Parameter validation ---\n /** req.query parameters\n * url\n * fname\n * folder\n */\n const params = req.query\n\n // @ts-ignore\n const chkUrl = chkParamUrl(params)\n if ( chkUrl.status !== 0 ) {\n log.error(`[uibuilder:apiv2:uibgetfile] Admin API. ${chkUrl.statusMessage}`)\n res.statusMessage = chkUrl.statusMessage\n res.status(chkUrl.status).end()\n return\n }\n\n // @ts-ignore\n const chkFname = chkParamFname(params)\n if ( chkFname.status !== 0 ) {\n log.error(`[uibuilder:apiv2:uibgetfile] Admin API. ${chkFname.statusMessage}. url=${params.url}`)\n res.statusMessage = chkFname.statusMessage\n res.status(chkFname.status).end()\n return\n }\n\n // @ts-ignore\n const chkFldr = chkParamFldr(params)\n if ( chkFldr.status !== 0 ) {\n log.error(`[uibuilder:apiv2:uibgetfile] Admin API. ${chkFldr.statusMessage}. url=${params.url}`)\n res.statusMessage = chkFldr.statusMessage\n res.status(chkFldr.status).end()\n return\n }\n //#endregion ---- ----\n\n log.trace(`[uibuilder:apiv2:uibgetfile] Admin API. File get requested. url=${params.url}, file=${params.folder}/${params.fname}`)\n\n // if fldr = root, no folder\n if ( params.folder === 'root' ) params.folder = ''\n\n // @ts-ignore\n const filePathRoot = path.join(uib.rootFolder, params.url, params.folder)\n // @ts-ignore\n const filePath = path.join(filePathRoot, req.query.fname)\n\n // Does the file exist?\n if ( fs.existsSync(filePath) ) {\n // Send back a plain text response body containing content of the file\n res.type('text/plain').sendFile(\n // @ts-ignore\n req.query.fname,\n {\n // Prevent injected relative paths from escaping `src` folder\n 'root': filePathRoot,\n // Turn off caching\n 'lastModified': false,\n 'cacheControl': false,\n 'dotfiles': 'allow',\n }\n )\n } else {\n log.error(`[uibuilder:apiv2:uibgetfile] Admin API. File does not exist '${filePath}'. url=${params.url}`)\n res.statusMessage = 'File does not exist'\n res.status(500).end()\n }\n }) // ---- End of uibgetfile ---- //\n\n /** Create a simple NR admin API to UPDATE the content of a file in the `/uibuilder//` folder\n * @param {string} url The admin api url to create\n * @param {object} permissions The permissions required for access (Express middleware)\n * @param {Function} cb\n **/\n v2AdminRouter.post('/uibputfile', function(/** @type {express.Request} */ req, /** @type {express.Response} */ res) {\n //#region ====== Parameter validation ====== //\n const params = req.body\n\n const chkUrl = chkParamUrl(params)\n if ( chkUrl.status !== 0 ) {\n log.error(`[uibuilder:apiv2:uibputfile] Admin API v2. ${chkUrl.statusMessage}`)\n res.statusMessage = chkUrl.statusMessage\n res.status(chkUrl.status).end()\n return\n }\n\n const chkFname = chkParamFname(params)\n if ( chkFname.status !== 0 ) {\n log.error(`[uibuilder:apiv2:uibputfile] Admin API. ${chkFname.statusMessage}. url=${params.url}`)\n res.statusMessage = chkFname.statusMessage\n res.status(chkFname.status).end()\n return\n }\n\n const chkFldr = chkParamFldr(params)\n if ( chkFldr.status !== 0 ) {\n log.error(`[uibuilder:apiv2:uibputfile] Admin API. ${chkFldr.statusMessage}. url=${params.url}`)\n res.statusMessage = chkFldr.statusMessage\n res.status(chkFldr.status).end()\n return\n }\n //#endregion ====== ====== //\n\n log.trace(`[uibuilder:apiv2:uibputfile] Admin API. File put requested. url=${params.url}, file=${params.folder}/${params.fname}, reload? ${params.reload}`)\n\n // Fix for Issue #155 - if fldr = root, no folder\n if ( params.folder === 'root' ) params.folder = '.'\n\n const fullname = path.join(uib.rootFolder, params.url, params.folder, params.fname)\n\n // eslint-disable-next-line no-unused-vars\n fs.writeFile(fullname, req.body.data, function (err, data) {\n if (err) {\n // Send back a response message and code 200 = OK, 500 (Internal Server Error)=Update failed\n log.error(`[uibuilder:apiv2:uibputfile] Admin API. File write FAIL. url=${params.url}, file=${params.folder}/${params.fname}`, err)\n res.statusMessage = err\n res.status(500).end()\n } else {\n // Send back a response message and code 200 = OK, 500 (Internal Server Error)=Update failed\n log.trace(`[uibuilder:apiv2:uibputfile] Admin API. File write SUCCESS. url=${params.url}, file=${params.folder}/${params.fname}`)\n res.statusMessage = 'File written successfully'\n res.status(200).end()\n // Reload connected clients if required by sending them a reload msg\n if ( params.reload === 'true' ) {\n sockets.sendToFe2({\n '_uib': {\n 'reload': true,\n }\n }, params.url)\n }\n }\n })\n }) // ---- End of uibputfile ---- //\n\n /** Create an index web page or JSON return listing all uibuilder endpoints\n * Also allows confirmation of whether a url is in use ('check' parameter) or a simple list of urls in use.\n */\n v2AdminRouter.get('/uibindex', function(/** @type {express.Request} */ req, /** @type {express.Response} */ res) {\n log.trace('[uibindex] User Page/API. List all available uibuilder endpoints')\n\n // If using own Express server, correct the URL's\n const url = new URL(req.headers.referer)\n url.pathname = ''\n if (uib.customServer.port) {\n // @ts-expect-error ts(2322)\n url.port = uib.customServer.port\n }\n const urlPrefix = url.href\n\n /** Return full details based on type parameter */\n switch (req.query.type) {\n case 'json': {\n res.json(uib.instances)\n break\n }\n case 'urls': {\n res.json(Object.values(uib.instances))\n break\n }\n // default to 'html' output type\n default: {\n const page = detailsPage(uib, urlPrefix)\n res.send(page)\n break\n }\n }\n }) // ---- End of uibindex ---- //\n\n /** Check & update installed front-end library packages, return list as JSON - this runs when NR Editor is loaded if a uib instance deployed */\n v2AdminRouter.get('/uibvendorpackages', function(/** @type {express.Request} */ req, /** @type {express.Response} */ res) {\n\n // Update the installed packages list\n // web.serveVendorPackages()\n\n res.json( packageMgt.uibPackageJson.uibuilder.packages )\n\n }) // ---- End of uibvendorpackages ---- //\n\n /** Call npm. Schema: {name:{(url),cmd}}\n * If url parameter not provided, uibPath = , else uibPath = /\n * Valid commands:\n * install, remove, update\n * * = run as npm command with --json output\n * @param {string} [req.query.url=userDir] Optional. If present, CWD is set to the uibuilder folder for that instance. Otherwise CWD is set to the userDir.\n * @param {string} req.query.cmd Command to run (see notes for this function)\n */\n v2AdminRouter.get('/uibnpmmanage', function(/** @type {express.Request} */ req, /** @type {express.Response} */ res) {\n //#region --- Parameter validation (cmd, package) ---\n\n const params = req.query\n\n // Validate the npm command to be used.\n if ( params.cmd === undefined ) {\n log.error('[uibuilder:apiv2:uibnpmmanage] uibuilder Admin API. No command provided for npm management.')\n res.statusMessage = 'npm command parameter not provided'\n res.status(500).end()\n return\n }\n switch (params.cmd) {\n case 'install':\n case 'remove':\n case 'update':\n break\n\n default:\n log.error('[uibuilder:apiv2:uibnpmmanage] Admin API. Invalid command provided for npm management.')\n res.statusMessage = 'npm command parameter is invalid'\n res.status(500).end()\n return\n }\n\n // package name must not exceed 255 characters\n // we have to have a package name\n if ( params.package === undefined ) {\n log.error('[uibuilder:apiv2:uibnpmmanage] Admin API. package parameter not provided')\n res.statusMessage = 'package parameter not provided'\n res.status(500).end()\n return\n }\n if ( params.package.length > 255 ) {\n log.error('[uibuilder:apiv2:uibnpmmanage] Admin API. package name parameter is too long (>255 characters)')\n res.statusMessage = 'package name parameter is too long. Max 255 characters'\n res.status(500).end()\n return\n }\n\n // If install/update, we need the node instance as well\n if ( params.cmd !== 'remove' ) {\n // @ts-ignore\n const chkUrl = chkParamUrl(params)\n if ( chkUrl.status !== 0 ) {\n log.error(`[uibuilder:apiv2:uibnpmmanage] Admin API. ${chkUrl.statusMessage}`)\n res.statusMessage = chkUrl.statusMessage\n res.status(chkUrl.status).end()\n return\n }\n }\n\n //#endregion ---- ----\n\n const folder = RED.settings.userDir\n\n log.info(`[uibuilder:apiv2:uibnpmmanage] Admin API. Running npm ${params.cmd} for package ${params.package} with tag/version '${params.tag}'`)\n\n // delete package lock file as it seems to mess up sometimes - no error if it fails\n fs.removeSync(path.join(folder, 'package-lock.json'))\n\n // Formulate the command to be run\n switch (params.cmd) {\n case 'update':\n case 'install': {\n // @ts-expect-error\n packageMgt.npmInstallPackage(params.url, params.package, params.tag)\n .then((npmOutput) => {\n // Get the updated package.json file into packageMgt.uibPackageJson\n packageMgt.getUibRootPackageJson()\n\n // Do a fast update of the min data in pj.uibuilder.packages required for web.serveVendorPackages() - re-saves the package.json file\n packageMgt.pkgsQuickUpd()\n\n // Update the packageList\n web.serveVendorPackages()\n\n res.json({ 'success': true, 'result': [npmOutput, packageMgt.uibPackageJson.uibuilder.packages] })\n\n // return success\n return true\n })\n .catch((err) => {\n // log.warn(`[uibuilder:apiv2:uibnpmmanage] Admin API. ERROR Running npm ${params.cmd} for package ${params.package}`, err.stdout)\n console.dir(err)\n log.warn(`[uibuilder:apiv2:uibnpmmanage:install] Admin API. ERROR Running: \\n'${err.command}' \\n${err.all}`)\n res.json({ 'success': false, 'result': err.all })\n return false\n })\n break\n }\n case 'remove': {\n // @ts-ignore\n packageMgt.npmRemovePackage(params.package)\n .then((npmOutput) => {\n // Get the updated package.json file into packageMgt.uibPackageJson\n packageMgt.getUibRootPackageJson()\n\n // Do a fast update of the min data in pj.uibuilder.packages required for web.serveVendorPackages() - re-saves the package.json file\n packageMgt.pkgsQuickUpd()\n\n // TODO remove - just send back success\n\n // Update the packageList\n web.serveVendorPackages()\n\n res.json({ 'success': true, 'result': npmOutput })\n return true\n })\n .catch((err) => {\n // log.warn(`[uibuilder:apiv2:uibnpmmanage] Admin API. ERROR Running npm ${params.cmd} for package ${params.package}`, err.stdout)\n log.warn(`[uibuilder:apiv2:uibnpmmanage:remove] Admin API. ERROR Running: \\n'${err.command}' \\n${err.all}`)\n res.json({ 'success': false, 'result': err.all })\n return false\n })\n break\n }\n default: {\n log.error(`[uibuilder:apiv2:uibnpmmanage] Admin API. Command ${params.cmd} is not a valid command. Must be 'install', 'remove' or 'update'.`)\n res.statusMessage = 'No valid npm command available'\n res.status(500).end()\n break\n }\n }\n\n }) // ---- End of npmmanage ---- //\n\n return v2AdminRouter\n}\n\nmodule.exports = adminRouterV2\n\n// EOF\n"], + "mappings": "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", + "names": ["express", "path", "fs", "web", "sockets", "packageMgt", "tilib", "errUibRootFldr", "v2AdminRouter", "chkParamUrl", "params", "res", "chkParamFname", "fname", "chkParamFldr", "folder", "detailsPage", "uib", "urlPrefix", "rootFolder", "RED", "log", "routes", "urlRoot", "uibSummary", "e", "page", "key", "installedPackages", "packageName", "pj", "url", "adminRouterV2", "req", "chkUrl", "chkFname", "chkFldr", "filePathRoot", "filePath", "fullname", "err", "data", "npmOutput"] +} diff --git a/nodes/libs/admin-api-v3.js b/nodes/libs/admin-api-v3.js index b8c6d4e1..2be11c8f 100644 --- a/nodes/libs/admin-api-v3.js +++ b/nodes/libs/admin-api-v3.js @@ -1,633 +1,2 @@ -/** v3 Admin API ExpressJS Router Handler - * - * See: https://expressjs.com/en/4x/api.html#router, https://expressjs.com/en/guide/routing.html - * - * Copyright (c) 2021-2023 Julian Knight (Totally Information) - * https://it.knightnet.org.uk, https://github.com/TotallyInformation/node-red-contrib-uibuilder - * - * 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. - */ -'use strict' - -/** --- Type Defs --- - * @typedef {import('../../typedefs.js').uibConfig} uibConfig - */ - -const express = require('express') -const path = require('path') -const fs = require('fs-extra') // https://github.com/jprichardson/node-fs-extra#nodejs-fs-extra -const fg = require('fast-glob') // https://github.com/mrmlnc/fast-glob -const uiblib = require('./uiblib') // Utility library for uibuilder -const web = require('./web') -const sockets = require('./socket') -const templateConf = require('../../templates/template_dependencies') // Template configuration metadata - -const v3AdminRouter = express.Router() // eslint-disable-line new-cap - -const errUibRootFldr = new Error('uib.rootFolder is null') - -//#region === REST API Validation functions === // - -/** Validate url query parameter - * @param {object} params The GET (res.query) or POST (res.body) parameters - * @param {string} params.url The uibuilder url to check - * @returns {{statusMessage: string, status: number}} Status message - */ -function chkParamUrl(params) { - const res = { 'statusMessage': '', 'status': 0 } - - // We have to have a url to work with - the url defines the start folder - if ( params.url === undefined ) { - res.statusMessage = 'url parameter not provided' - res.status = 500 - return res - } - - // Trim the url - params.url = params.url.trim() - - // URL must not exceed 20 characters - if ( params.url.length > 20 ) { - res.statusMessage = `url parameter is too long. Max 20 characters: ${params.url}` - res.status = 500 - return res - } - - // URL must be more than 0 characters - if ( params.url.length < 1 ) { - res.statusMessage = 'url parameter is empty, please provide a value' - res.status = 500 - return res - } - - // URL cannot contain .. to prevent escaping sub-folder structure - if ( params.url.includes('..') ) { - res.statusMessage = `url parameter may not contain "..": ${params.url}` - res.status = 500 - return res - } - - // Actually, since uib auto-creates folder if not exists, this just gets in the way - // Does this url have a matching instance root folder? - // if ( ! fs.existsSync(path.join(uib.rootFolder, params.url)) ) { - // res.statusMessage = `url does not have a matching instance root folder. url='${params.url}', Master root folder='${uib.rootFolder}'` - // res.status = 500 - // return res - // } - - return res -} // ---- End of fn chkParamUrl ---- // - -/** Validate fname (filename) query parameter - * @param {object} params The GET (res.query) or POST (res.body) parameters - * @param {string} params.fname The uibuilder url to check - * @returns {{statusMessage: string, status: number}} Status message - */ -function chkParamFname(params) { - const res = { 'statusMessage': '', 'status': 0 } - const fname = params.fname - - // We have to have an fname (file name) to work with - if ( fname === undefined ) { - res.statusMessage = 'file name not provided' - res.status = 500 - return res - } - // Blank file name probably means no files available so we will ignore - if ( fname === '' ) { - res.statusMessage = 'file name cannot be blank' - res.status = 500 - return res - } - // fname must not exceed 255 characters - if ( fname.length > 255 ) { - res.statusMessage = `file name is too long. Max 255 characters: ${params.fname}` - res.status = 500 - return res - } - // fname cannot contain .. to prevent escaping sub-folder structure - if ( fname.includes('..') ) { - res.statusMessage = `file name may not contain "..": ${params.fname}` - res.status = 500 - return res - } - - return res -} // ---- End of fn chkParamFname ---- // - -/** Validate folder query parameter - * @param {object} params The GET (res.query) or POST (res.body) parameters - * @param {string} params.folder The uibuilder url to check - * @returns {{statusMessage: string, status: number}} Status message - */ -function chkParamFldr(params) { - const res = { 'statusMessage': '', 'status': 0 } - const folder = params.folder - - // we have to have a folder name - if ( folder === undefined ) { - res.statusMessage = 'folder name not provided' - res.status = 500 - return res - } - // folder name must be >0 in length - if ( folder === '' ) { - res.statusMessage = 'folder name cannot be blank' - res.status = 500 - return res - } - // folder name must not exceed 255 characters - if ( folder.length > 255 ) { - res.statusMessage = `folder name is too long. Max 255 characters: ${folder}` - res.status = 500 - return res - } - // folder name cannot contain .. to prevent escaping sub-folder structure - if ( folder.includes('..') ) { - res.statusMessage = `folder name may not contain "..": ${folder}` - res.status = 500 - return res - } - - return res -} // ---- End of fn chkParamFldr ---- // - -//#endregion === End of API validation functions === // - -/** Return a router but allow parameters to be passed in - * @param {uibConfig} uib Reference to uibuilder's master uib object - * @param {*} log Reference to uibuilder's log functions - * @returns {express.Router} The v3 admin API ExpressJS router - */ -function adminRouterV3(uib, log) { - - /** uibuilder v3 unified Admin API router - new API commands should be added here - * Typical URL is: http://127.0.0.1:1880/red/uibuilder/admin/nodeurl?cmd=listfolders - */ - v3AdminRouter.route('/:url') - // For all routes (this function is called before more specific ones) - .all(function(/** @type {express.Request} */ req, /** @type {express.Response} */ res, /** @type {express.NextFunction} */ next) { - // @ts-ignore - const params = res.allparams = Object.assign({}, req.query, req.body, req.params) - params.type = 'all' - // params.headers = req.headers - - // Validate URL - params.url - const chkUrl = chkParamUrl(params) - if ( chkUrl.status !== 0 ) { - log.error(`[uibuilder:adminRouterV3:ALL] Admin API. ${chkUrl.statusMessage}`) - res.statusMessage = chkUrl.statusMessage - res.status(chkUrl.status).end() - return - } - - next() - }) - // Get something and return it - .get(function(/** @type {express.Request} */ req, /** @type {express.Response} */ res) { - if (uib.rootFolder === null) throw errUibRootFldr - - // @ts-ignore - const params = res.allparams - params.type = 'get' - - // Commands ... - switch (params.cmd) { - // List all folders and files for this uibuilder instance - case 'listall': { - log.trace(`[uibuilder:adminRouterV3:GET] Admin API. List all folders and files. url=${params.url}, root fldr=${uib.rootFolder}`) - - // get list of all (sub)folders (follow symlinks as well) - const out = { 'root': [] } - const root2 = uib.rootFolder.replace(/\\/g, '/') - fg.stream( - [ - // '**', - // '!node_modules', - // '!.git', - // '!.vscode', - // '!_*', - // '!/**/_*/', - `${root2}/${params.url}/**`, - `!${root2}/${params.url}/node_modules`, - `!${root2}/${params.url}/.git`, - `!${root2}/${params.url}/.vscode`, - `!${root2}/${params.url}/_*`, - `!${root2}/${params.url}/**/[_]*`, - - ], - { - // cwd: `${root2}/${params.url}/`, - dot: true, - onlyFiles: false, - deep: 10, - followSymbolicLinks: true, - markDirectories: true, - } - ) - .on('data', entry => { - entry = entry.replace(`${root2}/${params.url}/`, '') - let fldr - if ( entry.endsWith('/') ) { - // remove trailing / - fldr = entry.slice(0, -1) - // For the root folder of the instance, use "root" as the name (matches editor processing) - if ( fldr === '' ) fldr = 'root' - out[fldr] = [] - } else { - const splitEntry = entry.split('/') - const last = splitEntry.pop() - fldr = splitEntry.join('/') - if ( fldr === '' ) fldr = 'root' - // Wrap in a try because we can't exclude xxx/_yyyy/som.thing and that seems to crash the push. - try { - out[fldr].push(last) - } catch (e) { /* Nothing needed here */ } - } - }) - .on('end', () => { - res.statusMessage = 'Folders and Files listed successfully' - res.status(200).json(out) - }) - - break - } // -- end of listall -- // - - // List all folders and files for this uibuilder instance - case 'listfolders': { - log.trace(`[uibuilder:adminRouterV3:GET] Admin API. List all folders. url=${params.url}, root fldr=${uib.rootFolder}`) - - // get list of all (sub)folders (follow symlinks as well) - // const out = { 'root': [] } - const out = [] - const root2 = uib.rootFolder.replace(/\\/g, '/') - fg.stream( - [ - // '**', - // '!node_modules', - // '!.git', - // '!.vscode', - // '!_*', - // '!/**/_*/', - `${root2}/${params.url}/**`, - `!${root2}/${params.url}/node_modules`, - `!${root2}/${params.url}/.git`, - `!${root2}/${params.url}/.vscode`, - `!${root2}/${params.url}/_*`, - `!${root2}/${params.url}/**/[_]*`, - - ], - { - // cwd: `${root2}/${params.url}/`, - dot: true, - onlyFiles: false, - onlyDirectories: true, - deep: 10, - followSymbolicLinks: true, - markDirectories: false, - } - ) - .on('data', entry => { - entry = entry.replace(`${root2}/${params.url}/`, '') - out.push(entry) - }) - .on('end', () => { - res.statusMessage = 'Folders listed successfully' - res.status(200).json(out) - }) - - break - } // -- end of listfolders -- // - - // Check if URL is already in use - case 'checkurls': { - log.trace(`[uibuilder:adminRouterV3:GET:checkurls] Check if URL is already in use. URL: ${params.url}`) - - /** @returns {boolean} True if the given url exists, else false */ - const chkInstances = Object.values(uib.instances).includes(params.url) - const chkFolders = fs.existsSync(path.join(uib.rootFolder, params.url)) - - res.statusMessage = 'Instances and Folders checked' - res.status(200).json( chkInstances || chkFolders ) - - break - } // -- end of checkurls -- // - - // List all of the deployed instance urls - case 'listinstances': { - - log.trace('[uibuilder:adminRouterV3:GET:listinstances] Returning a list of deployed URLs (instances of uib).') - - /** @returns {boolean} True if the given url exists, else false */ - // let chkInstances = Object.values(uib.instances).includes(params.url) - // let chkFolders = fs.existsSync(path.join(uib.rootFolder, params.url)) - - res.statusMessage = 'Instances listed' - res.status(200).json( uib.instances ) - - break - } // -- end of listinstances -- // - - // Return a list of all user urls in use by ExpressJS - case 'listurls': { - // TODO Not currently working - let route - const routes = [] - web.app._router.stack.forEach( (middleware) => { - if (middleware.route) { // routes registered directly on the app - const path = middleware.route.path - const methods = middleware.route.methods - routes.push({ path: path, methods: methods }) - } else if (middleware.name === 'router') { // router middleware - middleware.handle.stack.forEach(function(handler) { - route = handler.route - route && routes.push(route) - }) - } - }) - // console.log(web.app._router.stack[0]) - - log.trace('[uibuilder:adminRouterV3:GET:listurls] Admin API. List of all user urls in use.') - res.statusMessage = 'URLs listed successfully' - // res.status(200).json(routes) - res.status(200).json(web.app._router.stack) - - break - } // -- end of listurls -- // - - // See if a node's custom folder exists. Return true if it does, else false - case 'checkfolder': { - log.trace(`[uibuilder:adminRouterV3:GET:checkfolder] See if a node's custom folder exists. URL: ${params.url}`) - - const folder = path.join( uib.rootFolder, params.url) - - fs.access(folder, fs.constants.F_OK) - .then( () => { - res.statusMessage = 'Folder checked' - res.status(200).json( true ) - return true - }) - .catch( () => { // err) => { - res.statusMessage = 'Folder checked' - res.status(200).json( false ) - return false - }) - - break - } // -- end of checkfolder -- // - - default: { - break - } - } - }) - - // TODO Write file contents - .put(function(/** @type {express.Request} */ req, /** @type {express.Response} */ res) { - if (uib.rootFolder === null) throw errUibRootFldr - - // @ts-expect-error - const params = res.allparams - params.type = 'put' - - const fullname = path.join(uib.rootFolder, params.url) - - // Commands ... - switch (params.cmd) { - // Tell uibuilder to delete the instance local folder when this instance is deleted - see html file oneditdelete & uiblib.processClose - case 'deleteondelete': { - log.trace(`[uibuilder:adminRouterV3:PUT:deleteondelete] url=${params.url}`) - uib.deleteOnDelete[params.url] = true - res.statusMessage = 'PUT successful' - res.status(200).json({}) - return - } - - case 'updatepackage': { - log.trace(`[uibuilder:adminRouterV3:PUT:updatepackage] url=${params.url}`) - // console.log(`[uibuilder:adminRouterV3:PUT:updatepackage] url=${params.url}, pkg=${params.pkgName}`) - - res.statusMessage = 'PUT successful' - res.status(200).json({ - newVersion: '' - }) - return - } - } - - // If we get here, we've failed - log.trace(`[uibuilder:adminRouterV3:PUT] Unsuccessful. command=${params.cmd}, url=${params.url}`) - res.statusMessage = 'PUT unsuccessful' - res.status(500).json({ - 'cmd': params.cmd, - 'fullname': fullname, - 'params': params, - 'message': 'PUT unsuccessful', - }) - - }) - - // Load new template or Create a new folder or file - .post(function(/** @type {express.Request} */ req, /** @type {express.Response} */ res) { - if (uib.rootFolder === null) throw errUibRootFldr - - // @ts-ignore - const params = res.allparams - params.type = 'post' - - if ( params.cmd === 'replaceTemplate' ) { - - uiblib.replaceTemplate(params.url, params.template, params.extTemplate, params.cmd, templateConf, uib, log) - .then( resp => { - res.statusMessage = resp.statusMessage - if ( resp.status === 200 ) res.status(200).json(resp.json) - else res.status(resp.status).end() - // Reload connected clients if required by sending them a reload msg - if ( params.reload === 'true' ) { - sockets.sendToFe2({ - '_uib': { - 'reload': true, - } - }, params.url) - } - return true - }) - .catch( err => { - let statusMsg, mystr - if ( err.code === 'MISSING_REF' ) { - statusMsg = `Degit clone error. CHECK External Template Name. Name='${params.extTemplate}', url=${params.url}, cmd=${params.cmd}. ${err.message}` - } else { - if ( params.template === 'external' ) mystr = `, ${params.extTemplate}` - statusMsg = `Replace template error. ${err.message}. url=${params.url}. ${params.template}${mystr}` - } - log.error(`[uibuilder:adminapi:POST:replaceTemplate] ${statusMsg}`, err) - res.statusMessage = statusMsg - res.status(500).end() - } ) - - } else { - - // Validate folder name - params.folder - const chkFldr = chkParamFldr(params) - if ( chkFldr.status !== 0 ) { - log.error(`[uibuilder:adminRouterV3:POST] Admin API. ${chkFldr.statusMessage}. url=${params.url}`) - res.statusMessage = chkFldr.statusMessage - res.status(chkFldr.status).end() - return - } - // Validate command - must be present and either be 'newfolder' or 'newfile' - if ( !(params.cmd && (params.cmd === 'newfolder' || params.cmd === 'newfile')) ) { - const statusMsg = `cmd parameter not present or wrong value (must be 'newfolder' or 'newfile'). url=${params.url}, cmd=${params.cmd}` - log.error(`[uibuilder:adminRouterV3:POST] Admin API. ${statusMsg}`) - res.statusMessage = statusMsg - res.status(500).end() - return - } - // If newfile, validate file name - params.fname - if (params.cmd === 'newfile' ) { - const chkFname = chkParamFname(params) - if ( chkFname.status !== 0 ) { - log.error(`[uibuilder:adminRouterV3:POST] Admin API. ${chkFname.statusMessage}. url=${params.url}`) - res.statusMessage = chkFname.statusMessage - res.status(chkFname.status).end() - return - } - } - - // Fix for Issue #155 - if fldr = root, no folder - if ( params.folder === 'root' ) params.folder = '' - - let fullname = path.join(uib.rootFolder, params.url, params.folder) - if (params.cmd === 'newfile' ) { - fullname = path.join(fullname, params.fname) - } - - // Does folder or file already exist? If so, return error - if ( fs.pathExistsSync(fullname) ) { - const statusMsg = `selected ${params.cmd === 'newfolder' ? 'folder' : 'file'} already exists. url=${params.url}, cmd=${params.cmd}, folder=${params.folder}` - log.error(`[uibuilder:adminRouterV3:POST] Admin API. ${statusMsg}`) - res.statusMessage = statusMsg - res.status(500).end() - return - } - - // try to create folder/file - if fail, return error - try { - if ( params.cmd === 'newfolder') { - fs.ensureDirSync(fullname) - } else { - fs.ensureFileSync(fullname) - } - } catch (e) { - const statusMsg = `could not create ${params.cmd === 'newfolder' ? 'folder' : 'file'}. url=${params.url}, cmd=${params.cmd}, folder=${params.folder}, error=${e.message}` - log.error(`[uibuilder:adminRouterV3:POST] Admin API. ${statusMsg}`) - res.statusMessage = statusMsg - res.status(500).end() - return - } - - log.trace(`[uibuilder:adminRouterV3:POST] Admin API. Folder/File create SUCCESS. url=${params.url}, file=${params.folder}/${params.fname}`) - res.statusMessage = 'Folder/File created successfully' - res.status(200).json({ - 'fullname': fullname, - 'params': params, - }) - - } // end of else - - }) // --- End of POST processing --- // - - // Delete a folder or a file - .delete(function(/** @type {express.Request} */ req, /** @type {express.Response} */ res) { - if (uib.rootFolder === null) throw errUibRootFldr - - // @ts-ignore ts(2339) - const params = res.allparams - params.type = 'delete' - - // Several command options available: deletefolder, deletefile - - // deletefolder or deletefile: - - // Validate folder name - params.folder - const chkFldr = chkParamFldr(params) - if ( chkFldr.status !== 0 ) { - log.error(`[uibuilder:adminRouterV3:DELETE] Admin API. ${chkFldr.statusMessage}. url=${params.url}`) - res.statusMessage = chkFldr.statusMessage - res.status(chkFldr.status).end() - return - } - // Validate command - must be present and either be 'deletefolder' or 'deletefile' - if ( !(params.cmd && (params.cmd === 'deletefolder' || params.cmd === 'deletefile')) ) { - const statusMsg = `cmd parameter not present or wrong value (must be 'deletefolder' or 'deletefile'). url=${params.url}, cmd=${params.cmd}` - log.error(`[uibuilder:adminRouterV3:DELETE] Admin API. ${statusMsg}`) - res.statusMessage = statusMsg - res.status(500).end() - return - } - // If newfile, validate file name - params.fname - if (params.cmd === 'deletefile' ) { - const chkFname = chkParamFname(params) - if ( chkFname.status !== 0 ) { - log.error(`[uibuilder:adminRouterV3:DELETE] Admin API. ${chkFname.statusMessage}. url=${params.url}`) - res.statusMessage = chkFname.statusMessage - res.status(chkFname.status).end() - return - } - } - - // Fix for Issue #155 - if fldr = root, no folder - if ( params.folder === 'root' ) params.folder = '' - - let fullname = path.join(uib.rootFolder, params.url, params.folder) - if (params.cmd === 'deletefile' ) { - fullname = path.join(fullname, params.fname) - } - - // Does folder or file does not exist? Return error - if ( !fs.pathExistsSync(fullname) ) { - const statusMsg = `selected ${params.cmd === 'deletefolder' ? 'folder' : 'file'} does not exist. url=${params.url}, cmd=${params.cmd}, folder=${params.folder}` - log.error(`[uibuilder:adminRouterV3:DELETE] Admin API. ${statusMsg}`) - res.statusMessage = statusMsg - res.status(500).end() - return - } - - // try to create folder/file - if fail, return error - try { - fs.removeSync(fullname) // deletes both files and folders - } catch (e) { - const statusMsg = `could not delete ${params.cmd === 'deletefolder' ? 'folder' : 'file'}. url=${params.url}, cmd=${params.cmd}, folder=${params.folder}, error=${e.message}` - log.error(`[uibuilder:adminRouterV3:DELETE] Admin API. ${statusMsg}`) - res.statusMessage = statusMsg - res.status(500).end() - return - } - - log.trace(`[uibuilder:adminRouterV3:DELETE] Admin API. Folder/File delete SUCCESS. url=${params.url}, file=${params.folder}/${params.fname}`) - res.statusMessage = 'Folder/File deleted successfully' - res.status(200).json({ - 'fullname': fullname, - 'params': params, - }) - }) - - /** @see https://expressjs.com/en/4x/api.html#app.METHOD for other methods - * patch, report, search ? - */ - - return v3AdminRouter -} - -module.exports = adminRouterV3 - -// EOF +"use strict";const F=require("express"),i=require("path"),d=require("fs-extra"),m=require("fast-glob"),k=require("./uiblib"),$=require("./web"),E=require("./socket"),A=require("../../templates/template_dependencies"),p=F.Router(),f=new Error("uib.rootFolder is null");function R(l){const t={statusMessage:"",status:0};return l.url===void 0?(t.statusMessage="url parameter not provided",t.status=500,t):(l.url=l.url.trim(),l.url.length>20?(t.statusMessage=`url parameter is too long. Max 20 characters: ${l.url}`,t.status=500,t):l.url.length<1?(t.statusMessage="url parameter is empty, please provide a value",t.status=500,t):(l.url.includes("..")&&(t.statusMessage=`url parameter may not contain "..": ${l.url}`,t.status=500),t))}function M(l){const t={statusMessage:"",status:0},n=l.fname;return n===void 0?(t.statusMessage="file name not provided",t.status=500,t):n===""?(t.statusMessage="file name cannot be blank",t.status=500,t):n.length>255?(t.statusMessage=`file name is too long. Max 255 characters: ${l.fname}`,t.status=500,t):(n.includes("..")&&(t.statusMessage=`file name may not contain "..": ${l.fname}`,t.status=500),t)}function g(l){const t={statusMessage:"",status:0},n=l.folder;return n===void 0?(t.statusMessage="folder name not provided",t.status=500,t):n===""?(t.statusMessage="folder name cannot be blank",t.status=500,t):n.length>255?(t.statusMessage=`folder name is too long. Max 255 characters: ${n}`,t.status=500,t):(n.includes("..")&&(t.statusMessage=`folder name may not contain "..": ${n}`,t.status=500),t)}function T(l,t){return p.route("/:url").all(function(n,s,e){const u=s.allparams=Object.assign({},n.query,n.body,n.params);u.type="all";const a=R(u);if(a.status!==0){t.error(`[uibuilder:adminRouterV3:ALL] Admin API. ${a.statusMessage}`),s.statusMessage=a.statusMessage,s.status(a.status).end();return}e()}).get(function(n,s){if(l.rootFolder===null)throw f;const e=s.allparams;switch(e.type="get",e.cmd){case"listall":{t.trace(`[uibuilder:adminRouterV3:GET] Admin API. List all folders and files. url=${e.url}, root fldr=${l.rootFolder}`);const u={root:[]},a=l.rootFolder.replace(/\\/g,"/");m.stream([`${a}/${e.url}/**`,`!${a}/${e.url}/node_modules`,`!${a}/${e.url}/.git`,`!${a}/${e.url}/.vscode`,`!${a}/${e.url}/_*`,`!${a}/${e.url}/**/[_]*`],{dot:!0,onlyFiles:!1,deep:10,followSymbolicLinks:!0,markDirectories:!0}).on("data",r=>{r=r.replace(`${a}/${e.url}/`,"");let o;if(r.endsWith("/"))o=r.slice(0,-1),o===""&&(o="root"),u[o]=[];else{const c=r.split("/"),h=c.pop();o=c.join("/"),o===""&&(o="root");try{u[o].push(h)}catch{}}}).on("end",()=>{s.statusMessage="Folders and Files listed successfully",s.status(200).json(u)});break}case"listfolders":{t.trace(`[uibuilder:adminRouterV3:GET] Admin API. List all folders. url=${e.url}, root fldr=${l.rootFolder}`);const u=[],a=l.rootFolder.replace(/\\/g,"/");m.stream([`${a}/${e.url}/**`,`!${a}/${e.url}/node_modules`,`!${a}/${e.url}/.git`,`!${a}/${e.url}/.vscode`,`!${a}/${e.url}/_*`,`!${a}/${e.url}/**/[_]*`],{dot:!0,onlyFiles:!1,onlyDirectories:!0,deep:10,followSymbolicLinks:!0,markDirectories:!1}).on("data",r=>{r=r.replace(`${a}/${e.url}/`,""),u.push(r)}).on("end",()=>{s.statusMessage="Folders listed successfully",s.status(200).json(u)});break}case"checkurls":{t.trace(`[uibuilder:adminRouterV3:GET:checkurls] Check if URL is already in use. URL: ${e.url}`);const u=Object.values(l.instances).includes(e.url),a=d.existsSync(i.join(l.rootFolder,e.url));s.statusMessage="Instances and Folders checked",s.status(200).json(u||a);break}case"listinstances":{t.trace("[uibuilder:adminRouterV3:GET:listinstances] Returning a list of deployed URLs (instances of uib)."),s.statusMessage="Instances listed",s.status(200).json(l.instances);break}case"listurls":{let u;const a=[];$.app._router.stack.forEach(r=>{if(r.route){const o=r.route.path,c=r.route.methods;a.push({path:o,methods:c})}else r.name==="router"&&r.handle.stack.forEach(function(o){u=o.route,u&&a.push(u)})}),t.trace("[uibuilder:adminRouterV3:GET:listurls] Admin API. List of all user urls in use."),s.statusMessage="URLs listed successfully",s.status(200).json($.app._router.stack);break}case"checkfolder":{t.trace(`[uibuilder:adminRouterV3:GET:checkfolder] See if a node's custom folder exists. URL: ${e.url}`);const u=i.join(l.rootFolder,e.url);d.access(u,d.constants.F_OK).then(()=>(s.statusMessage="Folder checked",s.status(200).json(!0),!0)).catch(()=>(s.statusMessage="Folder checked",s.status(200).json(!1),!1));break}default:break}}).put(function(n,s){if(l.rootFolder===null)throw f;const e=s.allparams;e.type="put";const u=i.join(l.rootFolder,e.url);switch(e.cmd){case"deleteondelete":{t.trace(`[uibuilder:adminRouterV3:PUT:deleteondelete] url=${e.url}`),l.deleteOnDelete[e.url]=!0,s.statusMessage="PUT successful",s.status(200).json({});return}case"updatepackage":{t.trace(`[uibuilder:adminRouterV3:PUT:updatepackage] url=${e.url}`),s.statusMessage="PUT successful",s.status(200).json({newVersion:""});return}}t.trace(`[uibuilder:adminRouterV3:PUT] Unsuccessful. command=${e.cmd}, url=${e.url}`),s.statusMessage="PUT unsuccessful",s.status(500).json({cmd:e.cmd,fullname:u,params:e,message:"PUT unsuccessful"})}).post(function(n,s){if(l.rootFolder===null)throw f;const e=s.allparams;if(e.type="post",e.cmd==="replaceTemplate")k.replaceTemplate(e.url,e.template,e.extTemplate,e.cmd,A,l,t).then(u=>(s.statusMessage=u.statusMessage,u.status===200?s.status(200).json(u.json):s.status(u.status).end(),e.reload==="true"&&E.sendToFe2({_uib:{reload:!0}},e.url),!0)).catch(u=>{let a,r;u.code==="MISSING_REF"?a=`Degit clone error. CHECK External Template Name. Name='${e.extTemplate}', url=${e.url}, cmd=${e.cmd}. ${u.message}`:(e.template==="external"&&(r=`, ${e.extTemplate}`),a=`Replace template error. ${u.message}. url=${e.url}. ${e.template}${r}`),t.error(`[uibuilder:adminapi:POST:replaceTemplate] ${a}`,u),s.statusMessage=a,s.status(500).end()});else{const u=g(e);if(u.status!==0){t.error(`[uibuilder:adminRouterV3:POST] Admin API. ${u.statusMessage}. url=${e.url}`),s.statusMessage=u.statusMessage,s.status(u.status).end();return}if(!(e.cmd&&(e.cmd==="newfolder"||e.cmd==="newfile"))){const r=`cmd parameter not present or wrong value (must be 'newfolder' or 'newfile'). url=${e.url}, cmd=${e.cmd}`;t.error(`[uibuilder:adminRouterV3:POST] Admin API. ${r}`),s.statusMessage=r,s.status(500).end();return}if(e.cmd==="newfile"){const r=M(e);if(r.status!==0){t.error(`[uibuilder:adminRouterV3:POST] Admin API. ${r.statusMessage}. url=${e.url}`),s.statusMessage=r.statusMessage,s.status(r.status).end();return}}e.folder==="root"&&(e.folder="");let a=i.join(l.rootFolder,e.url,e.folder);if(e.cmd==="newfile"&&(a=i.join(a,e.fname)),d.pathExistsSync(a)){const r=`selected ${e.cmd==="newfolder"?"folder":"file"} already exists. url=${e.url}, cmd=${e.cmd}, folder=${e.folder}`;t.error(`[uibuilder:adminRouterV3:POST] Admin API. ${r}`),s.statusMessage=r,s.status(500).end();return}try{e.cmd==="newfolder"?d.ensureDirSync(a):d.ensureFileSync(a)}catch(r){const o=`could not create ${e.cmd==="newfolder"?"folder":"file"}. url=${e.url}, cmd=${e.cmd}, folder=${e.folder}, error=${r.message}`;t.error(`[uibuilder:adminRouterV3:POST] Admin API. ${o}`),s.statusMessage=o,s.status(500).end();return}t.trace(`[uibuilder:adminRouterV3:POST] Admin API. Folder/File create SUCCESS. url=${e.url}, file=${e.folder}/${e.fname}`),s.statusMessage="Folder/File created successfully",s.status(200).json({fullname:a,params:e})}}).delete(function(n,s){if(l.rootFolder===null)throw f;const e=s.allparams;e.type="delete";const u=g(e);if(u.status!==0){t.error(`[uibuilder:adminRouterV3:DELETE] Admin API. ${u.statusMessage}. url=${e.url}`),s.statusMessage=u.statusMessage,s.status(u.status).end();return}if(!(e.cmd&&(e.cmd==="deletefolder"||e.cmd==="deletefile"))){const r=`cmd parameter not present or wrong value (must be 'deletefolder' or 'deletefile'). url=${e.url}, cmd=${e.cmd}`;t.error(`[uibuilder:adminRouterV3:DELETE] Admin API. ${r}`),s.statusMessage=r,s.status(500).end();return}if(e.cmd==="deletefile"){const r=M(e);if(r.status!==0){t.error(`[uibuilder:adminRouterV3:DELETE] Admin API. ${r.statusMessage}. url=${e.url}`),s.statusMessage=r.statusMessage,s.status(r.status).end();return}}e.folder==="root"&&(e.folder="");let a=i.join(l.rootFolder,e.url,e.folder);if(e.cmd==="deletefile"&&(a=i.join(a,e.fname)),!d.pathExistsSync(a)){const r=`selected ${e.cmd==="deletefolder"?"folder":"file"} does not exist. url=${e.url}, cmd=${e.cmd}, folder=${e.folder}`;t.error(`[uibuilder:adminRouterV3:DELETE] Admin API. ${r}`),s.statusMessage=r,s.status(500).end();return}try{d.removeSync(a)}catch(r){const o=`could not delete ${e.cmd==="deletefolder"?"folder":"file"}. url=${e.url}, cmd=${e.cmd}, folder=${e.folder}, error=${r.message}`;t.error(`[uibuilder:adminRouterV3:DELETE] Admin API. ${o}`),s.statusMessage=o,s.status(500).end();return}t.trace(`[uibuilder:adminRouterV3:DELETE] Admin API. Folder/File delete SUCCESS. url=${e.url}, file=${e.folder}/${e.fname}`),s.statusMessage="Folder/File deleted successfully",s.status(200).json({fullname:a,params:e})}),p}module.exports=T; +//# sourceMappingURL=admin-api-v3.js.map diff --git a/nodes/libs/admin-api-v3.js.map b/nodes/libs/admin-api-v3.js.map new file mode 100644 index 00000000..c1f128d5 --- /dev/null +++ b/nodes/libs/admin-api-v3.js.map @@ -0,0 +1,7 @@ +{ + "version": 3, + "sources": ["src/libs/admin-api-v3.js"], + "sourcesContent": ["/** v3 Admin API ExpressJS Router Handler\n *\n * See: https://expressjs.com/en/4x/api.html#router, https://expressjs.com/en/guide/routing.html\n *\n * Copyright (c) 2021-2023 Julian Knight (Totally Information)\n * https://it.knightnet.org.uk, https://github.com/TotallyInformation/node-red-contrib-uibuilder\n *\n * Licensed under the Apache License, Version 2.0 (the 'License');\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an 'AS IS' BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use strict'\n\n/** --- Type Defs ---\n * @typedef {import('../../typedefs.js').uibConfig} uibConfig\n */\n\nconst express = require('express')\nconst path = require('path')\nconst fs = require('fs-extra') // https://github.com/jprichardson/node-fs-extra#nodejs-fs-extra\nconst fg = require('fast-glob') // https://github.com/mrmlnc/fast-glob\nconst uiblib = require('./uiblib') // Utility library for uibuilder\nconst web = require('./web')\nconst sockets = require('./socket')\nconst templateConf = require('../../templates/template_dependencies') // Template configuration metadata\n\nconst v3AdminRouter = express.Router() // eslint-disable-line new-cap\n\nconst errUibRootFldr = new Error('uib.rootFolder is null')\n\n//#region === REST API Validation functions === //\n\n/** Validate url query parameter\n * @param {object} params The GET (res.query) or POST (res.body) parameters\n * @param {string} params.url The uibuilder url to check\n * @returns {{statusMessage: string, status: number}} Status message\n */\nfunction chkParamUrl(params) {\n const res = { 'statusMessage': '', 'status': 0 }\n\n // We have to have a url to work with - the url defines the start folder\n if ( params.url === undefined ) {\n res.statusMessage = 'url parameter not provided'\n res.status = 500\n return res\n }\n\n // Trim the url\n params.url = params.url.trim()\n\n // URL must not exceed 20 characters\n if ( params.url.length > 20 ) {\n res.statusMessage = `url parameter is too long. Max 20 characters: ${params.url}`\n res.status = 500\n return res\n }\n\n // URL must be more than 0 characters\n if ( params.url.length < 1 ) {\n res.statusMessage = 'url parameter is empty, please provide a value'\n res.status = 500\n return res\n }\n\n // URL cannot contain .. to prevent escaping sub-folder structure\n if ( params.url.includes('..') ) {\n res.statusMessage = `url parameter may not contain \"..\": ${params.url}`\n res.status = 500\n return res\n }\n\n // Actually, since uib auto-creates folder if not exists, this just gets in the way - // Does this url have a matching instance root folder?\n // if ( ! fs.existsSync(path.join(uib.rootFolder, params.url)) ) {\n // res.statusMessage = `url does not have a matching instance root folder. url='${params.url}', Master root folder='${uib.rootFolder}'`\n // res.status = 500\n // return res\n // }\n\n return res\n} // ---- End of fn chkParamUrl ---- //\n\n/** Validate fname (filename) query parameter\n * @param {object} params The GET (res.query) or POST (res.body) parameters\n * @param {string} params.fname The uibuilder url to check\n * @returns {{statusMessage: string, status: number}} Status message\n */\nfunction chkParamFname(params) {\n const res = { 'statusMessage': '', 'status': 0 }\n const fname = params.fname\n\n // We have to have an fname (file name) to work with\n if ( fname === undefined ) {\n res.statusMessage = 'file name not provided'\n res.status = 500\n return res\n }\n // Blank file name probably means no files available so we will ignore\n if ( fname === '' ) {\n res.statusMessage = 'file name cannot be blank'\n res.status = 500\n return res\n }\n // fname must not exceed 255 characters\n if ( fname.length > 255 ) {\n res.statusMessage = `file name is too long. Max 255 characters: ${params.fname}`\n res.status = 500\n return res\n }\n // fname cannot contain .. to prevent escaping sub-folder structure\n if ( fname.includes('..') ) {\n res.statusMessage = `file name may not contain \"..\": ${params.fname}`\n res.status = 500\n return res\n }\n\n return res\n} // ---- End of fn chkParamFname ---- //\n\n/** Validate folder query parameter\n * @param {object} params The GET (res.query) or POST (res.body) parameters\n * @param {string} params.folder The uibuilder url to check\n * @returns {{statusMessage: string, status: number}} Status message\n */\nfunction chkParamFldr(params) {\n const res = { 'statusMessage': '', 'status': 0 }\n const folder = params.folder\n\n // we have to have a folder name\n if ( folder === undefined ) {\n res.statusMessage = 'folder name not provided'\n res.status = 500\n return res\n }\n // folder name must be >0 in length\n if ( folder === '' ) {\n res.statusMessage = 'folder name cannot be blank'\n res.status = 500\n return res\n }\n // folder name must not exceed 255 characters\n if ( folder.length > 255 ) {\n res.statusMessage = `folder name is too long. Max 255 characters: ${folder}`\n res.status = 500\n return res\n }\n // folder name cannot contain .. to prevent escaping sub-folder structure\n if ( folder.includes('..') ) {\n res.statusMessage = `folder name may not contain \"..\": ${folder}`\n res.status = 500\n return res\n }\n\n return res\n} // ---- End of fn chkParamFldr ---- //\n\n//#endregion === End of API validation functions === //\n\n/** Return a router but allow parameters to be passed in\n * @param {uibConfig} uib Reference to uibuilder's master uib object\n * @param {*} log Reference to uibuilder's log functions\n * @returns {express.Router} The v3 admin API ExpressJS router\n */\nfunction adminRouterV3(uib, log) {\n\n /** uibuilder v3 unified Admin API router - new API commands should be added here\n * Typical URL is: http://127.0.0.1:1880/red/uibuilder/admin/nodeurl?cmd=listfolders\n */\n v3AdminRouter.route('/:url')\n // For all routes (this function is called before more specific ones)\n .all(function(/** @type {express.Request} */ req, /** @type {express.Response} */ res, /** @type {express.NextFunction} */ next) {\n // @ts-ignore\n const params = res.allparams = Object.assign({}, req.query, req.body, req.params)\n params.type = 'all'\n // params.headers = req.headers\n\n // Validate URL - params.url\n const chkUrl = chkParamUrl(params)\n if ( chkUrl.status !== 0 ) {\n log.error(`[uibuilder:adminRouterV3:ALL] Admin API. ${chkUrl.statusMessage}`)\n res.statusMessage = chkUrl.statusMessage\n res.status(chkUrl.status).end()\n return\n }\n\n next()\n })\n // Get something and return it\n .get(function(/** @type {express.Request} */ req, /** @type {express.Response} */ res) {\n if (uib.rootFolder === null) throw errUibRootFldr\n\n // @ts-ignore\n const params = res.allparams\n params.type = 'get'\n\n // Commands ...\n switch (params.cmd) {\n // List all folders and files for this uibuilder instance\n case 'listall': {\n log.trace(`[uibuilder:adminRouterV3:GET] Admin API. List all folders and files. url=${params.url}, root fldr=${uib.rootFolder}`)\n\n // get list of all (sub)folders (follow symlinks as well)\n const out = { 'root': [] }\n const root2 = uib.rootFolder.replace(/\\\\/g, '/')\n fg.stream(\n [\n // '**',\n // '!node_modules',\n // '!.git',\n // '!.vscode',\n // '!_*',\n // '!/**/_*/',\n `${root2}/${params.url}/**`,\n `!${root2}/${params.url}/node_modules`,\n `!${root2}/${params.url}/.git`,\n `!${root2}/${params.url}/.vscode`,\n `!${root2}/${params.url}/_*`,\n `!${root2}/${params.url}/**/[_]*`,\n\n ],\n {\n // cwd: `${root2}/${params.url}/`,\n dot: true,\n onlyFiles: false,\n deep: 10,\n followSymbolicLinks: true,\n markDirectories: true,\n }\n )\n .on('data', entry => {\n entry = entry.replace(`${root2}/${params.url}/`, '')\n let fldr\n if ( entry.endsWith('/') ) {\n // remove trailing /\n fldr = entry.slice(0, -1)\n // For the root folder of the instance, use \"root\" as the name (matches editor processing)\n if ( fldr === '' ) fldr = 'root'\n out[fldr] = []\n } else {\n const splitEntry = entry.split('/')\n const last = splitEntry.pop()\n fldr = splitEntry.join('/')\n if ( fldr === '' ) fldr = 'root'\n // Wrap in a try because we can't exclude xxx/_yyyy/som.thing and that seems to crash the push.\n try {\n out[fldr].push(last)\n } catch (e) { /* Nothing needed here */ }\n }\n })\n .on('end', () => {\n res.statusMessage = 'Folders and Files listed successfully'\n res.status(200).json(out)\n })\n\n break\n } // -- end of listall -- //\n\n // List all folders and files for this uibuilder instance\n case 'listfolders': {\n log.trace(`[uibuilder:adminRouterV3:GET] Admin API. List all folders. url=${params.url}, root fldr=${uib.rootFolder}`)\n\n // get list of all (sub)folders (follow symlinks as well)\n // const out = { 'root': [] }\n const out = []\n const root2 = uib.rootFolder.replace(/\\\\/g, '/')\n fg.stream(\n [\n // '**',\n // '!node_modules',\n // '!.git',\n // '!.vscode',\n // '!_*',\n // '!/**/_*/',\n `${root2}/${params.url}/**`,\n `!${root2}/${params.url}/node_modules`,\n `!${root2}/${params.url}/.git`,\n `!${root2}/${params.url}/.vscode`,\n `!${root2}/${params.url}/_*`,\n `!${root2}/${params.url}/**/[_]*`,\n\n ],\n {\n // cwd: `${root2}/${params.url}/`,\n dot: true,\n onlyFiles: false,\n onlyDirectories: true,\n deep: 10,\n followSymbolicLinks: true,\n markDirectories: false,\n }\n )\n .on('data', entry => {\n entry = entry.replace(`${root2}/${params.url}/`, '')\n out.push(entry)\n })\n .on('end', () => {\n res.statusMessage = 'Folders listed successfully'\n res.status(200).json(out)\n })\n\n break\n } // -- end of listfolders -- //\n\n // Check if URL is already in use\n case 'checkurls': {\n log.trace(`[uibuilder:adminRouterV3:GET:checkurls] Check if URL is already in use. URL: ${params.url}`)\n\n /** @returns {boolean} True if the given url exists, else false */\n const chkInstances = Object.values(uib.instances).includes(params.url)\n const chkFolders = fs.existsSync(path.join(uib.rootFolder, params.url))\n\n res.statusMessage = 'Instances and Folders checked'\n res.status(200).json( chkInstances || chkFolders )\n\n break\n } // -- end of checkurls -- //\n\n // List all of the deployed instance urls\n case 'listinstances': {\n\n log.trace('[uibuilder:adminRouterV3:GET:listinstances] Returning a list of deployed URLs (instances of uib).')\n\n /** @returns {boolean} True if the given url exists, else false */\n // let chkInstances = Object.values(uib.instances).includes(params.url)\n // let chkFolders = fs.existsSync(path.join(uib.rootFolder, params.url))\n\n res.statusMessage = 'Instances listed'\n res.status(200).json( uib.instances )\n\n break\n } // -- end of listinstances -- //\n\n // Return a list of all user urls in use by ExpressJS\n case 'listurls': {\n // TODO Not currently working\n let route\n const routes = []\n web.app._router.stack.forEach( (middleware) => {\n if (middleware.route) { // routes registered directly on the app\n const path = middleware.route.path\n const methods = middleware.route.methods\n routes.push({ path: path, methods: methods })\n } else if (middleware.name === 'router') { // router middleware\n middleware.handle.stack.forEach(function(handler) {\n route = handler.route\n route && routes.push(route)\n })\n }\n })\n // console.log(web.app._router.stack[0])\n\n log.trace('[uibuilder:adminRouterV3:GET:listurls] Admin API. List of all user urls in use.')\n res.statusMessage = 'URLs listed successfully'\n // res.status(200).json(routes)\n res.status(200).json(web.app._router.stack)\n\n break\n } // -- end of listurls -- //\n\n // See if a node's custom folder exists. Return true if it does, else false\n case 'checkfolder': {\n log.trace(`[uibuilder:adminRouterV3:GET:checkfolder] See if a node's custom folder exists. URL: ${params.url}`)\n\n const folder = path.join( uib.rootFolder, params.url)\n\n fs.access(folder, fs.constants.F_OK)\n .then( () => {\n res.statusMessage = 'Folder checked'\n res.status(200).json( true )\n return true\n })\n .catch( () => { // err) => {\n res.statusMessage = 'Folder checked'\n res.status(200).json( false )\n return false\n })\n\n break\n } // -- end of checkfolder -- //\n\n default: {\n break\n }\n }\n })\n\n // TODO Write file contents\n .put(function(/** @type {express.Request} */ req, /** @type {express.Response} */ res) {\n if (uib.rootFolder === null) throw errUibRootFldr\n\n // @ts-expect-error\n const params = res.allparams\n params.type = 'put'\n\n const fullname = path.join(uib.rootFolder, params.url)\n\n // Commands ...\n switch (params.cmd) {\n // Tell uibuilder to delete the instance local folder when this instance is deleted - see html file oneditdelete & uiblib.processClose\n case 'deleteondelete': {\n log.trace(`[uibuilder:adminRouterV3:PUT:deleteondelete] url=${params.url}`)\n uib.deleteOnDelete[params.url] = true\n res.statusMessage = 'PUT successful'\n res.status(200).json({})\n return\n }\n\n case 'updatepackage': {\n log.trace(`[uibuilder:adminRouterV3:PUT:updatepackage] url=${params.url}`)\n // console.log(`[uibuilder:adminRouterV3:PUT:updatepackage] url=${params.url}, pkg=${params.pkgName}`)\n\n res.statusMessage = 'PUT successful'\n res.status(200).json({\n newVersion: ''\n })\n return\n }\n }\n\n // If we get here, we've failed\n log.trace(`[uibuilder:adminRouterV3:PUT] Unsuccessful. command=${params.cmd}, url=${params.url}`)\n res.statusMessage = 'PUT unsuccessful'\n res.status(500).json({\n 'cmd': params.cmd,\n 'fullname': fullname,\n 'params': params,\n 'message': 'PUT unsuccessful',\n })\n\n })\n\n // Load new template or Create a new folder or file\n .post(function(/** @type {express.Request} */ req, /** @type {express.Response} */ res) {\n if (uib.rootFolder === null) throw errUibRootFldr\n\n // @ts-ignore\n const params = res.allparams\n params.type = 'post'\n\n if ( params.cmd === 'replaceTemplate' ) {\n\n uiblib.replaceTemplate(params.url, params.template, params.extTemplate, params.cmd, templateConf, uib, log)\n .then( resp => {\n res.statusMessage = resp.statusMessage\n if ( resp.status === 200 ) res.status(200).json(resp.json)\n else res.status(resp.status).end()\n // Reload connected clients if required by sending them a reload msg\n if ( params.reload === 'true' ) {\n sockets.sendToFe2({\n '_uib': {\n 'reload': true,\n }\n }, params.url)\n }\n return true\n })\n .catch( err => {\n let statusMsg, mystr\n if ( err.code === 'MISSING_REF' ) {\n statusMsg = `Degit clone error. CHECK External Template Name. Name='${params.extTemplate}', url=${params.url}, cmd=${params.cmd}. ${err.message}`\n } else {\n if ( params.template === 'external' ) mystr = `, ${params.extTemplate}`\n statusMsg = `Replace template error. ${err.message}. url=${params.url}. ${params.template}${mystr}`\n }\n log.error(`[uibuilder:adminapi:POST:replaceTemplate] ${statusMsg}`, err)\n res.statusMessage = statusMsg\n res.status(500).end()\n } )\n\n } else {\n\n // Validate folder name - params.folder\n const chkFldr = chkParamFldr(params)\n if ( chkFldr.status !== 0 ) {\n log.error(`[uibuilder:adminRouterV3:POST] Admin API. ${chkFldr.statusMessage}. url=${params.url}`)\n res.statusMessage = chkFldr.statusMessage\n res.status(chkFldr.status).end()\n return\n }\n // Validate command - must be present and either be 'newfolder' or 'newfile'\n if ( !(params.cmd && (params.cmd === 'newfolder' || params.cmd === 'newfile')) ) {\n const statusMsg = `cmd parameter not present or wrong value (must be 'newfolder' or 'newfile'). url=${params.url}, cmd=${params.cmd}`\n log.error(`[uibuilder:adminRouterV3:POST] Admin API. ${statusMsg}`)\n res.statusMessage = statusMsg\n res.status(500).end()\n return\n }\n // If newfile, validate file name - params.fname\n if (params.cmd === 'newfile' ) {\n const chkFname = chkParamFname(params)\n if ( chkFname.status !== 0 ) {\n log.error(`[uibuilder:adminRouterV3:POST] Admin API. ${chkFname.statusMessage}. url=${params.url}`)\n res.statusMessage = chkFname.statusMessage\n res.status(chkFname.status).end()\n return\n }\n }\n\n // Fix for Issue #155 - if fldr = root, no folder\n if ( params.folder === 'root' ) params.folder = ''\n\n let fullname = path.join(uib.rootFolder, params.url, params.folder)\n if (params.cmd === 'newfile' ) {\n fullname = path.join(fullname, params.fname)\n }\n\n // Does folder or file already exist? If so, return error\n if ( fs.pathExistsSync(fullname) ) {\n const statusMsg = `selected ${params.cmd === 'newfolder' ? 'folder' : 'file'} already exists. url=${params.url}, cmd=${params.cmd}, folder=${params.folder}`\n log.error(`[uibuilder:adminRouterV3:POST] Admin API. ${statusMsg}`)\n res.statusMessage = statusMsg\n res.status(500).end()\n return\n }\n\n // try to create folder/file - if fail, return error\n try {\n if ( params.cmd === 'newfolder') {\n fs.ensureDirSync(fullname)\n } else {\n fs.ensureFileSync(fullname)\n }\n } catch (e) {\n const statusMsg = `could not create ${params.cmd === 'newfolder' ? 'folder' : 'file'}. url=${params.url}, cmd=${params.cmd}, folder=${params.folder}, error=${e.message}`\n log.error(`[uibuilder:adminRouterV3:POST] Admin API. ${statusMsg}`)\n res.statusMessage = statusMsg\n res.status(500).end()\n return\n }\n\n log.trace(`[uibuilder:adminRouterV3:POST] Admin API. Folder/File create SUCCESS. url=${params.url}, file=${params.folder}/${params.fname}`)\n res.statusMessage = 'Folder/File created successfully'\n res.status(200).json({\n 'fullname': fullname,\n 'params': params,\n })\n\n } // end of else\n\n }) // --- End of POST processing --- //\n\n // Delete a folder or a file\n .delete(function(/** @type {express.Request} */ req, /** @type {express.Response} */ res) {\n if (uib.rootFolder === null) throw errUibRootFldr\n\n // @ts-ignore ts(2339)\n const params = res.allparams\n params.type = 'delete'\n\n // Several command options available: deletefolder, deletefile\n\n // deletefolder or deletefile:\n\n // Validate folder name - params.folder\n const chkFldr = chkParamFldr(params)\n if ( chkFldr.status !== 0 ) {\n log.error(`[uibuilder:adminRouterV3:DELETE] Admin API. ${chkFldr.statusMessage}. url=${params.url}`)\n res.statusMessage = chkFldr.statusMessage\n res.status(chkFldr.status).end()\n return\n }\n // Validate command - must be present and either be 'deletefolder' or 'deletefile'\n if ( !(params.cmd && (params.cmd === 'deletefolder' || params.cmd === 'deletefile')) ) {\n const statusMsg = `cmd parameter not present or wrong value (must be 'deletefolder' or 'deletefile'). url=${params.url}, cmd=${params.cmd}`\n log.error(`[uibuilder:adminRouterV3:DELETE] Admin API. ${statusMsg}`)\n res.statusMessage = statusMsg\n res.status(500).end()\n return\n }\n // If newfile, validate file name - params.fname\n if (params.cmd === 'deletefile' ) {\n const chkFname = chkParamFname(params)\n if ( chkFname.status !== 0 ) {\n log.error(`[uibuilder:adminRouterV3:DELETE] Admin API. ${chkFname.statusMessage}. url=${params.url}`)\n res.statusMessage = chkFname.statusMessage\n res.status(chkFname.status).end()\n return\n }\n }\n\n // Fix for Issue #155 - if fldr = root, no folder\n if ( params.folder === 'root' ) params.folder = ''\n\n let fullname = path.join(uib.rootFolder, params.url, params.folder)\n if (params.cmd === 'deletefile' ) {\n fullname = path.join(fullname, params.fname)\n }\n\n // Does folder or file does not exist? Return error\n if ( !fs.pathExistsSync(fullname) ) {\n const statusMsg = `selected ${params.cmd === 'deletefolder' ? 'folder' : 'file'} does not exist. url=${params.url}, cmd=${params.cmd}, folder=${params.folder}`\n log.error(`[uibuilder:adminRouterV3:DELETE] Admin API. ${statusMsg}`)\n res.statusMessage = statusMsg\n res.status(500).end()\n return\n }\n\n // try to create folder/file - if fail, return error\n try {\n fs.removeSync(fullname) // deletes both files and folders\n } catch (e) {\n const statusMsg = `could not delete ${params.cmd === 'deletefolder' ? 'folder' : 'file'}. url=${params.url}, cmd=${params.cmd}, folder=${params.folder}, error=${e.message}`\n log.error(`[uibuilder:adminRouterV3:DELETE] Admin API. ${statusMsg}`)\n res.statusMessage = statusMsg\n res.status(500).end()\n return\n }\n\n log.trace(`[uibuilder:adminRouterV3:DELETE] Admin API. Folder/File delete SUCCESS. url=${params.url}, file=${params.folder}/${params.fname}`)\n res.statusMessage = 'Folder/File deleted successfully'\n res.status(200).json({\n 'fullname': fullname,\n 'params': params,\n })\n })\n\n /** @see https://expressjs.com/en/4x/api.html#app.METHOD for other methods\n * patch, report, search ?\n */\n\n return v3AdminRouter\n}\n\nmodule.exports = adminRouterV3\n\n// EOF\n"], + "mappings": "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", + "names": ["express", "path", "fs", "fg", "uiblib", "web", "sockets", "templateConf", "v3AdminRouter", "errUibRootFldr", "chkParamUrl", "params", "res", "chkParamFname", "fname", "chkParamFldr", "folder", "adminRouterV3", "uib", "log", "req", "next", "chkUrl", "out", "root2", "entry", "fldr", "splitEntry", "last", "chkInstances", "chkFolders", "route", "routes", "middleware", "methods", "handler", "fullname", "resp", "err", "statusMsg", "mystr", "chkFldr", "chkFname", "e"] +} diff --git a/nodes/libs/package-mgt.js b/nodes/libs/package-mgt.js index 47133b71..3cb438ce 100644 --- a/nodes/libs/package-mgt.js +++ b/nodes/libs/package-mgt.js @@ -1,852 +1,11 @@ -/* eslint-disable class-methods-use-this */ -/** Manage npm packages - * - * Copyright (c) 2021-2023 Julian Knight (Totally Information) - * https://it.knightnet.org.uk, https://github.com/TotallyInformation/node-red-contrib-uibuilder - * - * 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. - */ -'use strict' - -/** --- Type Defs --- - * @typedef {import('../../typedefs.js').runtimeRED} runtimeRED - * @typedef {import('../../typedefs.js').uibNode} uibNode - * @typedef {import('../../typedefs.js').uibConfig} uibConfig - * @typedef {import('../../typedefs.js').uibPackageJson} uibPackageJson - */ - -const path = require('path') -// const util = require('util') -const fs = require('fs-extra') -// const tilib = require('./tilib') -const execa = require('execa') - -class UibPackages { - /** PRIVATE Flag to indicate whether setup() has been run (ignore the false eslint error) - * @type {boolean} - */ - #isConfigured = false - - #logUndefinedError = new Error('pkgMgt: this.log is undefined') - #uibUndefinedError = new Error('pkgMgt: this.uib is undefined') - #rootFldrNullError = new Error('pkgMgt: this.uib.rootFolder is null') - - /** @type {Array} Updated by updateMergedPackageList which is called first in setup and then in various updates */ - mergedPkgMasterList = [] - - /** @type {string} The name of the package.json file 'package.json' */ - packageJson = 'package.json' - - /** @type {uibPackageJson|null} The uibRoot package.json contents */ - uibPackageJson - - /** @type {string} Get npm's global install location */ - globalPrefix // set in constructor - - constructor() { - - /** Get npm's global install location */ - this.globalPrefix = this.npmGetGlobalPrefix() - - } // ---- End of constructor ---- // - - /** Gets the global install folder for npm & saves to a class variable - * @returns {string} The npm global install folder name - */ - npmGetGlobalPrefix() { // eslint-disable-line class-methods-use-this - // Does not need setup to have run - - const opts = { - 'all': true, - } - const args = [ - 'config', - 'get', - 'prefix', - ] - - let res - try { - const all = execa.sync('npm', args, opts) - res = all.stdout - } catch (e) { - console.error('>>>>>', e.all) - res = e.all // Do we need to wrap this in a promise? - } - return res - } // ---- End of npmGetGlobalPrefix ---- // - - /** Configure this class with uibuilder module specifics - * @param {uibConfig} uib uibuilder module-level configuration - */ - setup( uib ) { - if ( !uib ) throw new Error('[uibuilder:UibPackages.js:setup] Called without required uib parameter or uib is undefined.') - if ( uib.RED === null ) throw new Error('[uibuilder:UibPackages.js:setup] uib.RED is null') - - // Prevent setup from being called more than once - if ( this.#isConfigured === true ) { - uib.RED.log.warn('[uibuilder:UibPackages:setup] Setup has already been called, it cannot be called again.') - return - } - - this.RED = uib.RED - this.uib = uib - const log = this.log = uib.RED.log - - log.trace('[uibuilder:package-mgt:setup] Package Management setup started') - - // Get the uibuilder root folder's package.json file and save to class var or create minimal version if one doesn't exist - const pj = this.uibPackageJson = this.getUibRootPJ() - - // Update the version string to match uibuilder version - pj.version = this.uib.version - // Make sure there is a dependencies prop - if ( !pj.dependencies ) pj.dependencies = {} - // Make sure there is a uibuilder prop - if ( !pj.uibuilder ) pj.uibuilder = {} - // Make sure there is a uibuilder.packagedetails prop - if ( !pj.uibuilder.packages ) pj.uibuilder.packages = {} - - this.pkgsQuickUpd() - - // At this point we have the refs to uib and RED - this.#isConfigured = true - - // Re-build package.json uibuilder.packages with details & rewrite file [after 3sec] (async) - this.updateInstalledPackageDetails() - - log.trace('[uibuilder:package-mgt:setup] Package Management setup completed') - } // ---- End of setup ---- // - - /** Do a fast update of the min data in pj.uibuilder.packages required for web.serveVendorPackages() - re-saves the package.json file */ - pkgsQuickUpd() { - if ( this.uib === undefined ) throw this.#uibUndefinedError - if ( this.uib.rootFolder === null ) throw this.#rootFldrNullError - - const pj = this.uibPackageJson - - // Make sure no extra package details - for (const pkgName in pj.uibuilder.packages) { - if ( !pj.dependencies[pkgName] ) delete pj.uibuilder.packages[pkgName] - } - // Make sure all dependencies are reflected in uibuilder.packagedetails - for (const depName in pj.dependencies) { - if ( !pj.uibuilder.packages[depName] ) { - pj.uibuilder.packages[depName] = { installedVersion: pj.dependencies[depName] } - } - } - // Get folders for web:startup:serveVendorPackages() - for (const pkgName in pj.uibuilder.packages) { - const pkg = pj.uibuilder.packages[pkgName] - if ( this.uib.rootFolder === null ) throw this.#rootFldrNullError - // The actual location of the package folder - pkg.installFolder = path.join(this.uib.rootFolder, 'node_modules', pkgName) - // The base url used by uib - note this is changed if this is a scoped package - pkg.packageUrl = '/' + pkgName - // this.log.debug(`[uibuilder:package-mgt:pkgsQuickUpd] Updating '${pkgName}'. Fldr: '${pkg.installFolder}', URL: '${pkg.packageUrl}'.`) - } - - // Re-save the updated file - // this.setUibRootPackageJson(pj) - this.writePackageJson(this.uib.rootFolder, pj) - } - - /** Read the contents of a package.json file - * @param {string} folder The folder containing a package.json file - * @returns {object|null} Object representation of JSON if found otherwise null - */ - readPackageJson(folder) { - if ( this.log === undefined ) throw this.#logUndefinedError - - // Does not need setup to have finished running - - let file = null - try { - //! TODO: Replace fs-extra - // const data = fs.readFileSync('./example.json') - // const obj = JSON.parse(data) - file = fs.readJsonSync( path.join(folder, this.packageJson), 'utf8' ) - this.log.trace(`[uibuilder:package-mgt:readPackageJson] package.json file read successfully from ${folder}`) - } catch (err) { - this.log.error(`[uibuilder:package-mgt:readPackageJson] Failed to read package.json file from ${folder}`, this.packageJson, err) - } - return file - } // ---- End of readPackageJson ---- // - - /** Write updated /package.json (async) - * Also makes a backup copy to package.json.bak - * @param {string} folder The folder where to write the file - * @param {object} json The Object data to write to the file - */ - async writePackageJson(folder, json) { - // Does not need setup to have finished running - - const fileName = path.join( folder, this.packageJson ) - - try { // Make a backup copy - await fs.copy(fileName, `${fileName}.bak`) - this.log.trace(`[uibuilder:package-mgt:writePackageJson] package.json file successfully backed up in ${folder}`) - } catch (err) { - this.log.error(`[uibuilder:package-mgt:writePackageJson] Failed to copy package.json to backup. ${folder}`, this.packageJson, err) - } - - try { - await fs.writeJson(fileName, json, { spaces: 2 }) - this.log.trace(`[uibuilder:package-mgt:writePackageJson] package.json file written successfully in ${folder}`) - } catch (err) { - this.log.error(`[uibuilder:package-mgt:writePackageJson] Failed to write package.json. ${folder}`, this.packageJson, err) - } - } - - /** Get the uibRoot package.json and return as object. Or, if not exists, return minimal object - * Note: Does not directly update this.uibPackageJson because of async timing - * @returns {object} uibRoot/package.json contents or a minimal version as an object - */ - getUibRootPJ() { - if ( this.uib === undefined ) throw this.#uibUndefinedError - if ( this.log === undefined ) throw this.#logUndefinedError - - // Does not need setup to have finished running - - if ( this.uib.rootFolder === null ) throw this.#rootFldrNullError - const uibRoot = this.uib.rootFolder - - const fileName = path.join( uibRoot, this.packageJson ) - - // Get it to class var or create minimal class var - let res = this.readPackageJson(uibRoot) - - if (res === null) { - this.log.warn(`[uibuilder:package-mgt:getUibRootPJ] Could not read ${fileName}. Creating minimal version.`) - // Create a minimal pj - res = { - 'name': 'uib_root', - 'version': this.uib.version, - 'description': 'Root configuration and data folder for uibuilder', - 'scripts': {}, - 'dependencies': {}, - 'homepage': '', - 'bugs': '', - 'author': '', - 'license': 'Apache-2.0', - 'repository': '', - 'uibuilder': { - 'packages': {}, - }, - } - } - - return res - } - - async updIndividualPkgDetails(pkgName, lsParsed) { - if ( this.uibPackageJson === null ) throw new Error('[uibuilder:UibPackages.js:updIndividualPkgDetails] this.uibPackageJson is null') - const pj = this.uibPackageJson - - if ( pj.uibuilder === undefined || pj.uibuilder.packages === undefined || pj.dependencies === undefined ) throw new Error('pgkMgt:updIndividualPkgDetails: pj.uibuilder, pj.uibuilder.packages or pj.dependencies is undefined') - - // Make sure only packages in uibRoot/package.json dependencies are processed - if ( !pj.dependencies[pkgName] ) return - - const packages = pj.uibuilder.packages - - packages[pkgName] = {} - const pkg = packages[pkgName] - - const lsp = lsParsed.dependencies[pkgName] - // save the version/location spec from the dependencies prop so everything is together - pkg.spec = pj.dependencies[pkgName] - - if ( lsp.missing ) { - pkg.missing = true - pkg.problems = lsp.problems - } else { - // Get/Update package details - pkg.installFolder = lsp.path - pkg.installedVersion = lsp.version - - /** If we can, lets work out what resource is actually needed - * when using one of these packages in the browser. - * If we can't, leave a ? to make it obvious - * Annoyingly, a few packages have decided to make the `browser` property an object instead of a string. - * (e.g. vgauge) - ignore in that case as it isn't clear what the intent is. - */ - if (lsp.browser && (typeof lsp.browser === 'string') ) pkg.estimatedEntryPoint = lsp.browser - else if (lsp.jsdelivr) pkg.estimatedEntryPoint = lsp.jsdelivr - else if (lsp.unpkg) pkg.estimatedEntryPoint = lsp.unpkg - else if (lsp.main) pkg.estimatedEntryPoint = lsp.main - else pkg.estimatedEntryPoint = '?' - if ( pkg.estimatedEntryPoint === 'none') pkg.estimatedEntryPoint = '?' - - // Homepage - used for a help ref in the Editor - if (lsp.homepage) pkg.homepage = lsp.homepage - else pkg.homepage = `https://www.npmjs.com/search?q=${pkgName}` - - // The base url used by uib - note this is changed if this is a scoped package - pkg.packageUrl = '/' + pkgName - - // As the url may have changed (by removing scope), record the usable url - pkg.url = `../uibuilder/vendor${pkg.packageUrl}/${pkg.estimatedEntryPoint}` - - // If the package name is npm @scoped, remove the scope, add leading / & track scope name - if ( pkgName.startsWith('@') ) { - // pkg.packageUrl = '/' + pkgName.replace(/^@.*\//, '') - pkg.scope = pkgName.replace(pkg.packageUrl, '') - } - } - - if ( pj.dependencies[pkgName] && pj.dependencies[pkgName].includes(':') ) { - // Must be installed from somewhere other than npmjs so don't try to find latest version - pkg.latestVersion = null - pkg.installedFrom = pj.dependencies[pkgName].split(':')[0] - pkg.outdated = {} - } else { - pkg.installedFrom = 'npm' - - // Add current version details - let res = await this.npmOutdated(pkgName) - try { - res = JSON.parse(res) - } catch (e) { /* */ } - if ( res[pkgName] ) { - res = { - current: res[pkgName].current, - wanted: res[pkgName].wanted, - latest: res[pkgName].latest, - } - } - pkg.outdated = res - } - } - - /** Use npm to get detailed pkg info (slow, async) to pj.uibuilder.packages & rewrite the pj file */ - async updateInstalledPackageDetails() { - const pj = this.uibPackageJson - - if ( this.uib === undefined ) throw this.#uibUndefinedError - if ( this.uib.rootFolder === null ) throw this.#rootFldrNullError - const rootFolder = this.uib.rootFolder - - let ls = '' - try { - ls = await this.npmListInstalled(rootFolder) - } catch {} - - let lsParsed = { dependencies: {} } - try { - lsParsed = JSON.parse(ls) - } catch {} - - // Make sure we have package details for all installed packages - NB: don't use await with forEach! - const depPkgNames = Object.keys(lsParsed.dependencies || {}) - // await depPkgNames.forEach( async pkgName => { - // await this.updIndividualPkgDetails(pkgName, lsParsed) - // }) - // EITHER (serial) - // for ( const pkgName of depPkgNames ) { - // await this.updIndividualPkgDetails(pkgName, lsParsed) - // } - // OR (parallel) - await Promise.all( depPkgNames.map(async (pkgName) => { - await this.updIndividualPkgDetails(pkgName, lsParsed) - })) - - // (re)Write package.json - this.writePackageJson(rootFolder, pj) - } - - /** Get /package.json (create it if it doesn't exist), enhance with package details - * Also make version string same as uibuilder version - * @returns {object|null} Parsed version of /package.json with uibuilder specific updates - */ - getUibRootPackageJson() { - if ( this.log === undefined ) throw this.#logUndefinedError - if ( this.uib === undefined ) throw this.#uibUndefinedError - if ( this.uib.rootFolder === null ) throw this.#rootFldrNullError - - const log = this.log - - if ( this.#isConfigured !== true ) { - log.warn('[uibuilder:UibPackages:getUibRootPackageJson] Cannot run. Setup has not been called.') - return null - } - - const uibRoot = this.uib.rootFolder - const fileName = path.join( uibRoot, this.packageJson ) - - // Make sure it exists & contains valid JSON - - if ( !fs.existsSync(fileName) ) { - log.warn('[uibuilder:package-mgt:getUibRootPackageJson] No uibRoot/package.json file, creating minimal file.') - this.setUibRootPackageJson() - } - - // Get it - let pj = {} - try { - pj = this.readPackageJson(uibRoot) - } catch (e) { - log.error(`[uibuilder:package-mgt:getUibRootPackageJson] Error reading ${fileName}. ${e.message}`) - this.uibPackageJson = null - return null - } - - // Make sure there is a dependencies prop - if ( !pj.dependencies ) pj.dependencies = {} - // Make sure there is a uibuilder prop - if ( !pj.uibuilder ) pj.uibuilder = {} - // Reset the packages list, we rebuild it below - pj.uibuilder.packages = {} - - if (this.uibPackageJson.dependencies !== pj.dependencies ) { - log.info(`[uibuilder:package-mgt:getUibRootPackageJson] package.json dependencies changed`) - console.info({'pkg-deps': this.uibPackageJson.dependencies, 'memory-deps': pj.dependencies}) - } - - // Update the version string to match uibuilder version - pj.version = this.uib.version - - // Make sure we have package details for all installed packages - Object.keys(pj.dependencies).forEach( packageName => { - // Get/Update package details - pj.uibuilder.packages[packageName] = this.getPackageDetails2(packageName, uibRoot) - // And save the version/location spec from the dependencies prop so everything is together - pj.uibuilder.packages[packageName].spec = pj.dependencies[packageName] - - // Frig to pick up the version of Bootstrap installed with bootstrap-vue - if (packageName === 'bootstrap-vue' && !pj.dependencies.bootstrap ) { - pj.dependencies.bootstrap = pj.uibuilder.packages[packageName].bootstrap - pj.uibuilder.packages.bootstrap = this.getPackageDetails2('bootstrap', uibRoot) - pj.uibuilder.packages.bootstrap.spec = pj.dependencies.bootstrap - } - }) - - // Update the /package.json file with updated details & Return it - if (this.setUibRootPackageJson(pj) === true) return pj - - // Failed - return null - } // ----- End of getUibRootPackageJson() ----- // - - /** Write updated /package.json - * @param {object} json The Object data to write to the file - * @returns {boolean} True if write was successful - */ - setUibRootPackageJson(json) { - if ( this.log === undefined ) throw this.#logUndefinedError - if ( this.uib === undefined ) throw this.#uibUndefinedError - if ( this.uib.rootFolder === null ) throw this.#rootFldrNullError - - if ( this.#isConfigured !== true ) { - this.log.warn('[uibuilder:UibPackages:setUibRootPackageJson] Cannot run. Setup has not been called.') - return - } - - const uibRoot = this.uib.rootFolder - const fileName = path.join( uibRoot, this.packageJson ) - - if (!json) { - log.warn('[uibuilder:package-mgt:setUibRootPackageJson] Using dummy json') - json = { - 'name': 'uib_root', - 'version': this.uib.version, - 'description': 'Root configuration and data folder for uibuilder', - 'scripts': {}, - 'dependencies': {}, - 'homepage': '', - 'bugs': '', - 'author': '', - 'license': 'Apache-2.0', - 'repository': '', - 'uibuilder': { - 'packages': {}, - } - } - } - - try { - fs.writeJsonSync(fileName, json, { spaces: 2 }) - // Save it for use elsewhere - this.uibPackageJson = json - return true - } catch (e) { - log.error(`[uibuilder:package-mgt:setUibRootPackageJson] Error writing ${fileName}. ${e.message}`) - this.uibPackageJson = null - return false - } - } - - /** Find install folder for a package - allows an array of locations to be given - * NOTE: require.resolve can be a little ODD! - * When run from a linked package, it uses the link root not the linked location, - * this throws out the tree search. That's why we have to try several different locations here. - * Also, it finds the "main" script name which might not be in the package root. - * Also, it won't find ANYTHING if a `main` entry doesn't exist :( - * So we no longer use it, just search for folder names. - * @param {string} packageName - Name of the package who's install folder we are looking for. - * @param {string|Array} installRoot Location to search. Can be an array of locations. - * @returns {null|string} Actual filing system path to the installed package - */ - getPackagePath2(packageName, installRoot) { - if ( this.log === undefined ) throw this.#logUndefinedError - - if ( this.#isConfigured !== true ) { - this.log.warn('[uibuilder:UibPackages:getPackagePath] Cannot run. Setup has not been called.') - return null - } - - // If installRoot = string, make an array - if ( !Array.isArray(installRoot) ) installRoot = [installRoot] - - for (const r of installRoot) { - const loc = path.join(r, 'node_modules', packageName) - if (fs.existsSync( loc )) return loc - } - - this.log.warn(`[uibuilder:package-mgt:getPackagePath2] PACKAGE ${packageName} NOT FOUND`) - return null - } // ---- End of getPackagePath2 ---- // - - /** Get the details for an installed package & update uibuilder specific details before returning it - * @param {string} packageName - Name of the package who's install folder we are looking for. - * @param {string} installRoot A uibuilder node instance - will search in node's root folder first - * @returns {object|null} Details object for an installed package - */ - getPackageDetails2(packageName, installRoot) { - if ( this.log === undefined ) throw this.#logUndefinedError - - if ( this.#isConfigured !== true ) { - this.log.warn('[uibuilder:UibPackages:getPackagePath2] Cannot run. Setup has not been called.') - return null - } - - // Trim the input just in case - packageName = packageName.trim() - - const folder = this.getPackagePath2(packageName, installRoot) - if ( folder === null ) throw new Error('folder is null') - const pkgJson = this.readPackageJson(folder) - - const pkgDetails = { 'installFolder': folder } - // if ( pkgDetails === undefined ) throw new Error('pkgDetails is undefined') - if (pkgJson.version) pkgDetails.installedVersion = pkgJson.version - - /** If we can, lets work out what resource is actually needed - * when using one of these packages in the browser. - * If we can't, leave a ? to make it obvious - * Annoyingly, a few packages have decided to make the `browser` property an object instead of a string. - * (e.g. vgauge) - ignore in that case as it isn't clear what the intent is. - */ - if (pkgJson.browser && (typeof pkgJson.browser === 'string') ) pkgDetails.estimatedEntryPoint = pkgJson.browser - else if (pkgJson.jsdelivr) pkgDetails.estimatedEntryPoint = pkgJson.jsdelivr - else if (pkgJson.unpkg) pkgDetails.estimatedEntryPoint = pkgJson.unpkg - else if (pkgJson.main) pkgDetails.estimatedEntryPoint = pkgJson.main - else pkgDetails.estimatedEntryPoint = '?' - if ( pkgDetails.estimatedEntryPoint === 'none') pkgDetails.estimatedEntryPoint = '?' - - // Homepage - used for a help ref in the Editor - if (pkgJson.homepage) pkgDetails.homepage = pkgJson.homepage - else pkgDetails.homepage = `https://www.npmjs.com/search?q=${packageName}` - - // The base url used by uib - note this is changed if this is a scoped package - pkgDetails.packageUrl = '/' + packageName - - // Work out what kind of package this is - - // If the package name is npm @scoped, remove the scope, add leading / & track scope name - if ( pkgDetails.packageUrl.startsWith('@') ) { - pkgDetails.packageUrl = '/' + packageName.replace(/^@.*\//, '') - pkgDetails.scope = packageName.replace(pkgDetails.packageUrl, '') - } - - // As the url may have changed (by removing scope), record the usable url - pkgDetails.url = `../uibuilder/vendor${pkgDetails.packageUrl}/${pkgDetails.estimatedEntryPoint}` - - // Frig to pick up the version of Bootstrap installed with bootstrap-vue - if (packageName === 'bootstrap-vue') { - pkgDetails.bootstrap = pkgJson.dependencies.bootstrap - } - - // Add current version details - // pkgDetails.outdated = this.npmOutdated(packageName) - // console.log('pkgDetails.outdated', pkgDetails.outdated) - // this.npmOutdated(packageName) - // .then(res => { - // try { - // res = JSON.parse(res) - // } catch(e) { /* */ } - // if ( res[packageName] ) { - // res = { - // current: res[packageName].current, - // wanted: res[packageName].wanted, - // latest: res[packageName].latest, - // } - // } - // pkgDetails.outdated = res - // return true - // }) - // .catch( err => { - // // - // }) - - return pkgDetails - } // ---- End of getPackageDetails2 ---- // - - //#region --- DEPRECATED --- - - /** Update all of the installed packages - */ - updateInstalledPackages() { - this.log.error('[uibuilder:UibPackages:updateInstalledPackages] FUNCTION IS DEPRECATED.') - console.trace() - - console.trace('package-mgt.js:updateInstalledPackages') - } // ---- End of updateInstalledPackages ---- // - - /** !DEPRECATED! Find install folder for a package - */ - getPackagePath() { - this.log.error('[uibuilder:UibPackages:getPackagePath] FUNCTION IS DEPRECATED.') - console.trace() - - console.trace('package-mgt.js:getPackagePath') - } // ---- End of getPackagePath ---- // - - /** Update the master name list of possible packages that could be served to the front-end - */ - updateMergedPackageList() { - this.log.error('[uibuilder:UibPackages:updateMergedPackageList] FUNCTION IS DEPRECATED.') - console.trace() - - console.trace('package-mgt.js:updateMergedPackageList') - } // ---- End of updateMergedPackageList ---- // - - //#endregion --- DEPRECATED --- - - /** Install an npm package - * NOTE: This fn does not update the list of packages - * because that is built from the package.json file - * and that is updated by calling web.serveVendorPackages() - * which can't be done here - The calling admin API's do that - * Editor->API->This fn->API cont.->web.serveVendorPackages->getUibRootPackageJson->API cont2->Editor - * @param {string} url Node instance url - * @param {string} pkgName The npm name of the package (with scope prefix, version, etc if needed) - * @param {string} [tag] Default=''. Specifier for a version, tag, branch, etc. with leading @ for npm and # for GitHub installs - * @param {string} [toLocation] Where to install to. Defaults to uibRoot - * @returns {Promise} [Combined stdout/stderr, updated list of package details] - */ - async npmInstallPackage(url, pkgName, tag = '', toLocation = '') { - if ( this.log === undefined ) throw this.#logUndefinedError - - if ( this.#isConfigured !== true ) { - this.log.warn('[uibuilder:UibPackages:npmInstallPackage] Cannot run. Setup has not been called.') - return '' - } - - if ( this.uib === undefined ) throw this.#uibUndefinedError - if ( this.uib.rootFolder === null ) throw new Error('this.log.rootFolder is null') - if ( toLocation === '' ) toLocation = this.uib.rootFolder - - // https://github.com/sindresorhus/execa#options - const opts = { - 'cwd': toLocation, - 'all': true, - } - const args = [ // `npm install --no-audit --no-update-notifier --save --production --color=false --no-fund --json ${params.package}@latest` - 'install', - '--no-fund', - '--no-audit', - '--no-update-notifier', - '--save', - '--production', - '--color=false', - // '--json', - pkgName + tag, - ] - - // Don't need a try since we don't do any processing on an execa error - if cmd fails, the promise is rejected - const { all } = await execa('npm', args, opts) - this.log.info(`[uibuilder:UibPackages:npmInstallPackage] npm output: \n ${all}\n `) - - return /** @type {string} */ (all) - - } // ---- End of installPackage ---- // - - /** Install an npm package - * NOTE: This fn does not update the list of packages - see install above for reasons. - * @param {string} pkgName The npm name of the package (with scope prefix, version, etc if needed) - * @returns {Promise} Combined stdout/stderr - */ - async npmRemovePackage(pkgName) { - if ( this.log === undefined ) throw this.#logUndefinedError - if ( this.uib === undefined ) throw this.#uibUndefinedError - if ( this.uib.rootFolder === null ) throw this.#rootFldrNullError - - if ( this.#isConfigured !== true ) { - this.log.warn('[uibuilder:UibPackages:npmRemovePackage] Cannot run. Setup has not been called.') - return '' - } - - // https://github.com/sindresorhus/execa#options - const opts = { - 'cwd': this.uib.rootFolder, - 'all': true, - } - const args = [ - 'uninstall', - '--save', - '--color=false', - '--no-fund', - '--no-audit', - '--no-update-notifier', - // '--json', - pkgName, - ] - - // Don't need a try since we don't do any processing on an execa error - if cmd fails, the promise is rejected - const { all } = await execa('npm', args, opts) - this.log.info(`[uibuilder:UibPackages:npmRemovePackage] npm output: \n ${all}\n `) - - return /** @type {string} */ (all) - - } // ---- End of removePackage ---- // - - /** List all npm packages installed at the top-level of a folder - * @param {string=} folder The folder to start the list in - * @returns {Promise} Command output - */ - async npmListInstalled(folder) { - this.log.trace('[uibuilder:package-mgt:npmListInstalled] npm list installed started') - - // if ( this._isConfigured !== true ) { - // this.log.warn('[uibuilder:UibPackages:npmListInstalled] Cannot run. Setup has not been called.') - // return - // } - - // https://github.com/sindresorhus/execa#options - const opts = { - 'cwd': folder, - 'all': true, - } - const args = [ - 'list', - '--long', - '--json', - '--depth=0', - ] - - let res - try { - const { stdout } = await execa('npm', args, opts) - // console.log('>>>>>', stdout) - res = stdout - } catch (e) { - // console.log('>>>>>', e.message) - res = e.stdout - } - - this.log.trace('[uibuilder:package-mgt:npmListInstalled] npm list installed completed') - return res - } // ---- End of npmListInstalled ---- // - - /** Get the latest version string for a package - * @param {string} pkgName The npm name of the package (with scope prefix, version, etc if needed) - * @returns {Promise} Combined stdout/stderr - */ - async npmOutdated(pkgName) { - if ( this.log === undefined ) throw this.#logUndefinedError - if ( this.uib === undefined ) throw this.#uibUndefinedError - if ( this.uib.rootFolder === null ) throw this.#rootFldrNullError - - if ( this.#isConfigured !== true ) { - this.log.warn('[uibuilder:UibPackages:npmOutdated] Cannot run. Setup has not been called.') - return - } - - // https://github.com/sindresorhus/execa#options - const opts = { - 'cwd': this.uib.rootFolder, - 'all': true, - } - const args = [ // `npm remove --no-audit --no-update-notifier --color=false --json ${params.package}` // --save-prefix="~" - 'outdated', - '--json', - pkgName, - ] - - let res - try { - const { stdout } = await execa('npm', args, opts) - // const {stdout} = execa.sync('npm', args, opts) - res = stdout - } catch (err) { - res = err.stdout - } - - this.log.trace(`[uibuilder:UibPackages:npmOutdated] npm output: \n ${res}\n `) - - return res - - } // ---- End of npmOutdated ---- // - - /** Update an npm package (Not yet in use) - * @param {string} pkgName The npm name of the package (with scope prefix, version, etc if needed) - * @returns {Promise} Combined stdout/stderr - */ - async npmUpdate(pkgName) { - if ( this.log === undefined ) throw this.#logUndefinedError - - if ( this.#isConfigured !== true ) { - this.log.warn('[uibuilder:UibPackages:npmUpdate] Cannot run. Setup has not been called.') - return '' - } - - if ( this.uib === undefined ) throw this.#uibUndefinedError - if ( this.uib.rootFolder === null ) throw new Error('this.log.rootFolder is null') - // if ( toLocation === '' ) toLocation = this.uib.rootFolder - const toLocation = this.uib.rootFolder - - // https://github.com/sindresorhus/execa#options - const opts = { - 'cwd': toLocation, - 'all': true, - } - const args = [ - 'update', - '--no-fund', - '--no-audit', - '--no-update-notifier', - '--save', - '--production', - '--color=false', - // '--json', - pkgName, - ] - - // Don't need a try since we don't do any processing on an execa error - if cmd fails, the promise is rejected - const { all } = await execa('npm', args, opts) - this.log.info(`[uibuilder:UibPackages:npmUpdate] npm output: \n ${all}\n `) - - return /** @type {string} */ (all) - - } - -} // ----- End of UibPackages ----- // - -/** Singleton model. Only 1 instance of UibWeb should ever exist. - * Use as: `const packageMgt = require('./package-mgt.js')` - */ -// @ts-ignore -const uibPackages = new UibPackages() -module.exports = uibPackages - -// EOF +"use strict";const r=require("path"),l=require("fs-extra"),u=require("execa");class d{#e=!1;#t=new Error("pkgMgt: this.log is undefined");#i=new Error("pkgMgt: this.uib is undefined");#s=new Error("pkgMgt: this.uib.rootFolder is null");mergedPkgMasterList=[];packageJson="package.json";uibPackageJson;globalPrefix;constructor(){this.globalPrefix=this.npmGetGlobalPrefix()}npmGetGlobalPrefix(){const e={all:!0},n=["config","get","prefix"];let s;try{s=u.sync("npm",n,e).stdout}catch(t){console.error(">>>>>",t.all),s=t.all}return s}setup(e){if(!e)throw new Error("[uibuilder:UibPackages.js:setup] Called without required uib parameter or uib is undefined.");if(e.RED===null)throw new Error("[uibuilder:UibPackages.js:setup] uib.RED is null");if(this.#e===!0){e.RED.log.warn("[uibuilder:UibPackages:setup] Setup has already been called, it cannot be called again.");return}this.RED=e.RED,this.uib=e;const n=this.log=e.RED.log;n.trace("[uibuilder:package-mgt:setup] Package Management setup started");const s=this.uibPackageJson=this.getUibRootPJ();s.version=this.uib.version,s.dependencies||(s.dependencies={}),s.uibuilder||(s.uibuilder={}),s.uibuilder.packages||(s.uibuilder.packages={}),this.pkgsQuickUpd(),this.#e=!0,this.updateInstalledPackageDetails(),n.trace("[uibuilder:package-mgt:setup] Package Management setup completed")}pkgsQuickUpd(){if(this.uib===void 0)throw this.#i;if(this.uib.rootFolder===null)throw this.#s;const e=this.uibPackageJson;for(const n in e.uibuilder.packages)e.dependencies[n]||delete e.uibuilder.packages[n];for(const n in e.dependencies)e.uibuilder.packages[n]||(e.uibuilder.packages[n]={installedVersion:e.dependencies[n]});for(const n in e.uibuilder.packages){const s=e.uibuilder.packages[n];if(this.uib.rootFolder===null)throw this.#s;s.installFolder=r.join(this.uib.rootFolder,"node_modules",n),s.packageUrl="/"+n}this.writePackageJson(this.uib.rootFolder,e)}readPackageJson(e){if(this.log===void 0)throw this.#t;let n=null;try{//! TODO: Replace fs-extra +n=l.readJsonSync(r.join(e,this.packageJson),"utf8"),this.log.trace(`[uibuilder:package-mgt:readPackageJson] package.json file read successfully from ${e}`)}catch(s){this.log.error(`[uibuilder:package-mgt:readPackageJson] Failed to read package.json file from ${e}`,this.packageJson,s)}return n}async writePackageJson(e,n){const s=r.join(e,this.packageJson);try{await l.copy(s,`${s}.bak`),this.log.trace(`[uibuilder:package-mgt:writePackageJson] package.json file successfully backed up in ${e}`)}catch(t){this.log.error(`[uibuilder:package-mgt:writePackageJson] Failed to copy package.json to backup. ${e}`,this.packageJson,t)}try{await l.writeJson(s,n,{spaces:2}),this.log.trace(`[uibuilder:package-mgt:writePackageJson] package.json file written successfully in ${e}`)}catch(t){this.log.error(`[uibuilder:package-mgt:writePackageJson] Failed to write package.json. ${e}`,this.packageJson,t)}}getUibRootPJ(){if(this.uib===void 0)throw this.#i;if(this.log===void 0)throw this.#t;if(this.uib.rootFolder===null)throw this.#s;const e=this.uib.rootFolder,n=r.join(e,this.packageJson);let s=this.readPackageJson(e);return s===null&&(this.log.warn(`[uibuilder:package-mgt:getUibRootPJ] Could not read ${n}. Creating minimal version.`),s={name:"uib_root",version:this.uib.version,description:"Root configuration and data folder for uibuilder",scripts:{},dependencies:{},homepage:"",bugs:"",author:"",license:"Apache-2.0",repository:"",uibuilder:{packages:{}}}),s}async updIndividualPkgDetails(e,n){if(this.uibPackageJson===null)throw new Error("[uibuilder:UibPackages.js:updIndividualPkgDetails] this.uibPackageJson is null");const s=this.uibPackageJson;if(s.uibuilder===void 0||s.uibuilder.packages===void 0||s.dependencies===void 0)throw new Error("pgkMgt:updIndividualPkgDetails: pj.uibuilder, pj.uibuilder.packages or pj.dependencies is undefined");if(!s.dependencies[e])return;const t=s.uibuilder.packages;t[e]={};const i=t[e],a=n.dependencies[e];if(i.spec=s.dependencies[e],a.missing?(i.missing=!0,i.problems=a.problems):(i.installFolder=a.path,i.installedVersion=a.version,a.browser&&typeof a.browser=="string"?i.estimatedEntryPoint=a.browser:a.jsdelivr?i.estimatedEntryPoint=a.jsdelivr:a.unpkg?i.estimatedEntryPoint=a.unpkg:a.main?i.estimatedEntryPoint=a.main:i.estimatedEntryPoint="?",i.estimatedEntryPoint==="none"&&(i.estimatedEntryPoint="?"),a.homepage?i.homepage=a.homepage:i.homepage=`https://www.npmjs.com/search?q=${e}`,i.packageUrl="/"+e,i.url=`../uibuilder/vendor${i.packageUrl}/${i.estimatedEntryPoint}`,e.startsWith("@")&&(i.scope=e.replace(i.packageUrl,""))),s.dependencies[e]&&s.dependencies[e].includes(":"))i.latestVersion=null,i.installedFrom=s.dependencies[e].split(":")[0],i.outdated={};else{i.installedFrom="npm";let o=await this.npmOutdated(e);try{o=JSON.parse(o)}catch{}o[e]&&(o={current:o[e].current,wanted:o[e].wanted,latest:o[e].latest}),i.outdated=o}}async updateInstalledPackageDetails(){const e=this.uibPackageJson;if(this.uib===void 0)throw this.#i;if(this.uib.rootFolder===null)throw this.#s;const n=this.uib.rootFolder;let s="";try{s=await this.npmListInstalled(n)}catch{}let t={dependencies:{}};try{t=JSON.parse(s)}catch{}const i=Object.keys(t.dependencies||{});await Promise.all(i.map(async a=>{await this.updIndividualPkgDetails(a,t)})),this.writePackageJson(n,e)}getUibRootPackageJson(){if(this.log===void 0)throw this.#t;if(this.uib===void 0)throw this.#i;if(this.uib.rootFolder===null)throw this.#s;const e=this.log;if(this.#e!==!0)return e.warn("[uibuilder:UibPackages:getUibRootPackageJson] Cannot run. Setup has not been called."),null;const n=this.uib.rootFolder,s=r.join(n,this.packageJson);l.existsSync(s)||(e.warn("[uibuilder:package-mgt:getUibRootPackageJson] No uibRoot/package.json file, creating minimal file."),this.setUibRootPackageJson());let t={};try{t=this.readPackageJson(n)}catch(i){return e.error(`[uibuilder:package-mgt:getUibRootPackageJson] Error reading ${s}. ${i.message}`),this.uibPackageJson=null,null}return t.dependencies||(t.dependencies={}),t.uibuilder||(t.uibuilder={}),t.uibuilder.packages={},this.uibPackageJson.dependencies!==t.dependencies&&(e.info("[uibuilder:package-mgt:getUibRootPackageJson] package.json dependencies changed"),console.info({"pkg-deps":this.uibPackageJson.dependencies,"memory-deps":t.dependencies})),t.version=this.uib.version,Object.keys(t.dependencies).forEach(i=>{t.uibuilder.packages[i]=this.getPackageDetails2(i,n),t.uibuilder.packages[i].spec=t.dependencies[i],i==="bootstrap-vue"&&!t.dependencies.bootstrap&&(t.dependencies.bootstrap=t.uibuilder.packages[i].bootstrap,t.uibuilder.packages.bootstrap=this.getPackageDetails2("bootstrap",n),t.uibuilder.packages.bootstrap.spec=t.dependencies.bootstrap)}),this.setUibRootPackageJson(t)===!0?t:null}setUibRootPackageJson(e){if(this.log===void 0)throw this.#t;if(this.uib===void 0)throw this.#i;if(this.uib.rootFolder===null)throw this.#s;if(this.#e!==!0){this.log.warn("[uibuilder:UibPackages:setUibRootPackageJson] Cannot run. Setup has not been called.");return}const n=this.uib.rootFolder,s=r.join(n,this.packageJson);e||(log.warn("[uibuilder:package-mgt:setUibRootPackageJson] Using dummy json"),e={name:"uib_root",version:this.uib.version,description:"Root configuration and data folder for uibuilder",scripts:{},dependencies:{},homepage:"",bugs:"",author:"",license:"Apache-2.0",repository:"",uibuilder:{packages:{}}});try{return l.writeJsonSync(s,e,{spaces:2}),this.uibPackageJson=e,!0}catch(t){return log.error(`[uibuilder:package-mgt:setUibRootPackageJson] Error writing ${s}. ${t.message}`),this.uibPackageJson=null,!1}}getPackagePath2(e,n){if(this.log===void 0)throw this.#t;if(this.#e!==!0)return this.log.warn("[uibuilder:UibPackages:getPackagePath] Cannot run. Setup has not been called."),null;Array.isArray(n)||(n=[n]);for(const s of n){const t=r.join(s,"node_modules",e);if(l.existsSync(t))return t}return this.log.warn(`[uibuilder:package-mgt:getPackagePath2] PACKAGE ${e} NOT FOUND`),null}getPackageDetails2(e,n){if(this.log===void 0)throw this.#t;if(this.#e!==!0)return this.log.warn("[uibuilder:UibPackages:getPackagePath2] Cannot run. Setup has not been called."),null;e=e.trim();const s=this.getPackagePath2(e,n);if(s===null)throw new Error("folder is null");const t=this.readPackageJson(s),i={installFolder:s};return t.version&&(i.installedVersion=t.version),t.browser&&typeof t.browser=="string"?i.estimatedEntryPoint=t.browser:t.jsdelivr?i.estimatedEntryPoint=t.jsdelivr:t.unpkg?i.estimatedEntryPoint=t.unpkg:t.main?i.estimatedEntryPoint=t.main:i.estimatedEntryPoint="?",i.estimatedEntryPoint==="none"&&(i.estimatedEntryPoint="?"),t.homepage?i.homepage=t.homepage:i.homepage=`https://www.npmjs.com/search?q=${e}`,i.packageUrl="/"+e,i.packageUrl.startsWith("@")&&(i.packageUrl="/"+e.replace(/^@.*\//,""),i.scope=e.replace(i.packageUrl,"")),i.url=`../uibuilder/vendor${i.packageUrl}/${i.estimatedEntryPoint}`,e==="bootstrap-vue"&&(i.bootstrap=t.dependencies.bootstrap),i}updateInstalledPackages(){this.log.error("[uibuilder:UibPackages:updateInstalledPackages] FUNCTION IS DEPRECATED."),console.trace(),console.trace("package-mgt.js:updateInstalledPackages")}getPackagePath(){this.log.error("[uibuilder:UibPackages:getPackagePath] FUNCTION IS DEPRECATED."),console.trace(),console.trace("package-mgt.js:getPackagePath")}updateMergedPackageList(){this.log.error("[uibuilder:UibPackages:updateMergedPackageList] FUNCTION IS DEPRECATED."),console.trace(),console.trace("package-mgt.js:updateMergedPackageList")}async npmInstallPackage(e,n,s="",t=""){if(this.log===void 0)throw this.#t;if(this.#e!==!0)return this.log.warn("[uibuilder:UibPackages:npmInstallPackage] Cannot run. Setup has not been called."),"";if(this.uib===void 0)throw this.#i;if(this.uib.rootFolder===null)throw new Error("this.log.rootFolder is null");t===""&&(t=this.uib.rootFolder);const i={cwd:t,all:!0},a=["install","--no-fund","--no-audit","--no-update-notifier","--save","--production","--color=false",n+s],{all:o}=await u("npm",a,i);return this.log.info(`[uibuilder:UibPackages:npmInstallPackage] npm output: + ${o} + `),o}async npmRemovePackage(e){if(this.log===void 0)throw this.#t;if(this.uib===void 0)throw this.#i;if(this.uib.rootFolder===null)throw this.#s;if(this.#e!==!0)return this.log.warn("[uibuilder:UibPackages:npmRemovePackage] Cannot run. Setup has not been called."),"";const n={cwd:this.uib.rootFolder,all:!0},s=["uninstall","--save","--color=false","--no-fund","--no-audit","--no-update-notifier",e],{all:t}=await u("npm",s,n);return this.log.info(`[uibuilder:UibPackages:npmRemovePackage] npm output: + ${t} + `),t}async npmListInstalled(e){this.log.trace("[uibuilder:package-mgt:npmListInstalled] npm list installed started");const n={cwd:e,all:!0},s=["list","--long","--json","--depth=0"];let t;try{const{stdout:i}=await u("npm",s,n);t=i}catch(i){t=i.stdout}return this.log.trace("[uibuilder:package-mgt:npmListInstalled] npm list installed completed"),t}async npmOutdated(e){if(this.log===void 0)throw this.#t;if(this.uib===void 0)throw this.#i;if(this.uib.rootFolder===null)throw this.#s;if(this.#e!==!0){this.log.warn("[uibuilder:UibPackages:npmOutdated] Cannot run. Setup has not been called.");return}const n={cwd:this.uib.rootFolder,all:!0},s=["outdated","--json",e];let t;try{const{stdout:i}=await u("npm",s,n);t=i}catch(i){t=i.stdout}return this.log.trace(`[uibuilder:UibPackages:npmOutdated] npm output: + ${t} + `),t}async npmUpdate(e){if(this.log===void 0)throw this.#t;if(this.#e!==!0)return this.log.warn("[uibuilder:UibPackages:npmUpdate] Cannot run. Setup has not been called."),"";if(this.uib===void 0)throw this.#i;if(this.uib.rootFolder===null)throw new Error("this.log.rootFolder is null");const s={cwd:this.uib.rootFolder,all:!0},t=["update","--no-fund","--no-audit","--no-update-notifier","--save","--production","--color=false",e],{all:i}=await u("npm",t,s);return this.log.info(`[uibuilder:UibPackages:npmUpdate] npm output: + ${i} + `),i}}const c=new d;module.exports=c; +//# sourceMappingURL=package-mgt.js.map diff --git a/nodes/libs/package-mgt.js.map b/nodes/libs/package-mgt.js.map new file mode 100644 index 00000000..39e462d0 --- /dev/null +++ b/nodes/libs/package-mgt.js.map @@ -0,0 +1,7 @@ +{ + "version": 3, + "sources": ["src/libs/package-mgt.js"], + "sourcesContent": ["/* eslint-disable class-methods-use-this */\n/** Manage npm packages\n *\n * Copyright (c) 2021-2023 Julian Knight (Totally Information)\n * https://it.knightnet.org.uk, https://github.com/TotallyInformation/node-red-contrib-uibuilder\n *\n * Licensed under the Apache License, Version 2.0 (the 'License');\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an 'AS IS' BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use strict'\n\n/** --- Type Defs ---\n * @typedef {import('../../typedefs.js').runtimeRED} runtimeRED\n * @typedef {import('../../typedefs.js').uibNode} uibNode\n * @typedef {import('../../typedefs.js').uibConfig} uibConfig\n * @typedef {import('../../typedefs.js').uibPackageJson} uibPackageJson\n */\n\nconst path = require('path')\n// const util = require('util')\nconst fs = require('fs-extra')\n// const tilib = require('./tilib')\nconst execa = require('execa')\n\nclass UibPackages {\n /** PRIVATE Flag to indicate whether setup() has been run (ignore the false eslint error)\n * @type {boolean}\n */\n #isConfigured = false\n\n #logUndefinedError = new Error('pkgMgt: this.log is undefined')\n #uibUndefinedError = new Error('pkgMgt: this.uib is undefined')\n #rootFldrNullError = new Error('pkgMgt: this.uib.rootFolder is null')\n\n /** @type {Array} Updated by updateMergedPackageList which is called first in setup and then in various updates */\n mergedPkgMasterList = []\n\n /** @type {string} The name of the package.json file 'package.json' */\n packageJson = 'package.json'\n\n /** @type {uibPackageJson|null} The uibRoot package.json contents */\n uibPackageJson\n\n /** @type {string} Get npm's global install location */\n globalPrefix // set in constructor\n\n constructor() {\n\n /** Get npm's global install location */\n this.globalPrefix = this.npmGetGlobalPrefix()\n\n } // ---- End of constructor ---- //\n\n /** Gets the global install folder for npm & saves to a class variable\n * @returns {string} The npm global install folder name\n */\n npmGetGlobalPrefix() { // eslint-disable-line class-methods-use-this\n // Does not need setup to have run\n\n const opts = {\n 'all': true,\n }\n const args = [\n 'config',\n 'get',\n 'prefix',\n ]\n\n let res\n try {\n const all = execa.sync('npm', args, opts)\n res = all.stdout\n } catch (e) {\n console.error('>>>>>', e.all)\n res = e.all // Do we need to wrap this in a promise?\n }\n return res\n } // ---- End of npmGetGlobalPrefix ---- //\n\n /** Configure this class with uibuilder module specifics\n * @param {uibConfig} uib uibuilder module-level configuration\n */\n setup( uib ) {\n if ( !uib ) throw new Error('[uibuilder:UibPackages.js:setup] Called without required uib parameter or uib is undefined.')\n if ( uib.RED === null ) throw new Error('[uibuilder:UibPackages.js:setup] uib.RED is null')\n\n // Prevent setup from being called more than once\n if ( this.#isConfigured === true ) {\n uib.RED.log.warn('[uibuilder:UibPackages:setup] Setup has already been called, it cannot be called again.')\n return\n }\n\n this.RED = uib.RED\n this.uib = uib\n const log = this.log = uib.RED.log\n\n log.trace('[uibuilder:package-mgt:setup] Package Management setup started')\n\n // Get the uibuilder root folder's package.json file and save to class var or create minimal version if one doesn't exist\n const pj = this.uibPackageJson = this.getUibRootPJ()\n\n // Update the version string to match uibuilder version\n pj.version = this.uib.version\n // Make sure there is a dependencies prop\n if ( !pj.dependencies ) pj.dependencies = {}\n // Make sure there is a uibuilder prop\n if ( !pj.uibuilder ) pj.uibuilder = {}\n // Make sure there is a uibuilder.packagedetails prop\n if ( !pj.uibuilder.packages ) pj.uibuilder.packages = {}\n\n this.pkgsQuickUpd()\n\n // At this point we have the refs to uib and RED\n this.#isConfigured = true\n\n // Re-build package.json uibuilder.packages with details & rewrite file [after 3sec] (async)\n this.updateInstalledPackageDetails()\n\n log.trace('[uibuilder:package-mgt:setup] Package Management setup completed')\n } // ---- End of setup ---- //\n\n /** Do a fast update of the min data in pj.uibuilder.packages required for web.serveVendorPackages() - re-saves the package.json file */\n pkgsQuickUpd() {\n if ( this.uib === undefined ) throw this.#uibUndefinedError\n if ( this.uib.rootFolder === null ) throw this.#rootFldrNullError\n\n const pj = this.uibPackageJson\n\n // Make sure no extra package details\n for (const pkgName in pj.uibuilder.packages) {\n if ( !pj.dependencies[pkgName] ) delete pj.uibuilder.packages[pkgName]\n }\n // Make sure all dependencies are reflected in uibuilder.packagedetails\n for (const depName in pj.dependencies) {\n if ( !pj.uibuilder.packages[depName] ) {\n pj.uibuilder.packages[depName] = { installedVersion: pj.dependencies[depName] }\n }\n }\n // Get folders for web:startup:serveVendorPackages()\n for (const pkgName in pj.uibuilder.packages) {\n const pkg = pj.uibuilder.packages[pkgName]\n if ( this.uib.rootFolder === null ) throw this.#rootFldrNullError\n // The actual location of the package folder\n pkg.installFolder = path.join(this.uib.rootFolder, 'node_modules', pkgName)\n // The base url used by uib - note this is changed if this is a scoped package\n pkg.packageUrl = '/' + pkgName\n // this.log.debug(`[uibuilder:package-mgt:pkgsQuickUpd] Updating '${pkgName}'. Fldr: '${pkg.installFolder}', URL: '${pkg.packageUrl}'.`)\n }\n\n // Re-save the updated file\n // this.setUibRootPackageJson(pj)\n this.writePackageJson(this.uib.rootFolder, pj)\n }\n\n /** Read the contents of a package.json file\n * @param {string} folder The folder containing a package.json file\n * @returns {object|null} Object representation of JSON if found otherwise null\n */\n readPackageJson(folder) {\n if ( this.log === undefined ) throw this.#logUndefinedError\n\n // Does not need setup to have finished running\n\n let file = null\n try {\n //! TODO: Replace fs-extra\n // const data = fs.readFileSync('./example.json')\n // const obj = JSON.parse(data)\n file = fs.readJsonSync( path.join(folder, this.packageJson), 'utf8' )\n this.log.trace(`[uibuilder:package-mgt:readPackageJson] package.json file read successfully from ${folder}`)\n } catch (err) {\n this.log.error(`[uibuilder:package-mgt:readPackageJson] Failed to read package.json file from ${folder}`, this.packageJson, err)\n }\n return file\n } // ---- End of readPackageJson ---- //\n\n /** Write updated /package.json (async)\n * Also makes a backup copy to package.json.bak\n * @param {string} folder The folder where to write the file\n * @param {object} json The Object data to write to the file\n */\n async writePackageJson(folder, json) {\n // Does not need setup to have finished running\n\n const fileName = path.join( folder, this.packageJson )\n\n try { // Make a backup copy\n await fs.copy(fileName, `${fileName}.bak`)\n this.log.trace(`[uibuilder:package-mgt:writePackageJson] package.json file successfully backed up in ${folder}`)\n } catch (err) {\n this.log.error(`[uibuilder:package-mgt:writePackageJson] Failed to copy package.json to backup. ${folder}`, this.packageJson, err)\n }\n\n try {\n await fs.writeJson(fileName, json, { spaces: 2 })\n this.log.trace(`[uibuilder:package-mgt:writePackageJson] package.json file written successfully in ${folder}`)\n } catch (err) {\n this.log.error(`[uibuilder:package-mgt:writePackageJson] Failed to write package.json. ${folder}`, this.packageJson, err)\n }\n }\n\n /** Get the uibRoot package.json and return as object. Or, if not exists, return minimal object\n * Note: Does not directly update this.uibPackageJson because of async timing\n * @returns {object} uibRoot/package.json contents or a minimal version as an object\n */\n getUibRootPJ() {\n if ( this.uib === undefined ) throw this.#uibUndefinedError\n if ( this.log === undefined ) throw this.#logUndefinedError\n\n // Does not need setup to have finished running\n\n if ( this.uib.rootFolder === null ) throw this.#rootFldrNullError\n const uibRoot = this.uib.rootFolder\n\n const fileName = path.join( uibRoot, this.packageJson )\n\n // Get it to class var or create minimal class var\n let res = this.readPackageJson(uibRoot)\n\n if (res === null) {\n this.log.warn(`[uibuilder:package-mgt:getUibRootPJ] Could not read ${fileName}. Creating minimal version.`)\n // Create a minimal pj\n res = {\n 'name': 'uib_root',\n 'version': this.uib.version,\n 'description': 'Root configuration and data folder for uibuilder',\n 'scripts': {},\n 'dependencies': {},\n 'homepage': '',\n 'bugs': '',\n 'author': '',\n 'license': 'Apache-2.0',\n 'repository': '',\n 'uibuilder': {\n 'packages': {},\n },\n }\n }\n\n return res\n }\n\n async updIndividualPkgDetails(pkgName, lsParsed) {\n if ( this.uibPackageJson === null ) throw new Error('[uibuilder:UibPackages.js:updIndividualPkgDetails] this.uibPackageJson is null')\n const pj = this.uibPackageJson\n\n if ( pj.uibuilder === undefined || pj.uibuilder.packages === undefined || pj.dependencies === undefined ) throw new Error('pgkMgt:updIndividualPkgDetails: pj.uibuilder, pj.uibuilder.packages or pj.dependencies is undefined')\n\n // Make sure only packages in uibRoot/package.json dependencies are processed\n if ( !pj.dependencies[pkgName] ) return\n\n const packages = pj.uibuilder.packages\n\n packages[pkgName] = {}\n const pkg = packages[pkgName]\n\n const lsp = lsParsed.dependencies[pkgName]\n // save the version/location spec from the dependencies prop so everything is together\n pkg.spec = pj.dependencies[pkgName]\n\n if ( lsp.missing ) {\n pkg.missing = true\n pkg.problems = lsp.problems\n } else {\n // Get/Update package details\n pkg.installFolder = lsp.path\n pkg.installedVersion = lsp.version\n\n /** If we can, lets work out what resource is actually needed\n * when using one of these packages in the browser.\n * If we can't, leave a ? to make it obvious\n * Annoyingly, a few packages have decided to make the `browser` property an object instead of a string.\n * (e.g. vgauge) - ignore in that case as it isn't clear what the intent is.\n */\n if (lsp.browser && (typeof lsp.browser === 'string') ) pkg.estimatedEntryPoint = lsp.browser\n else if (lsp.jsdelivr) pkg.estimatedEntryPoint = lsp.jsdelivr\n else if (lsp.unpkg) pkg.estimatedEntryPoint = lsp.unpkg\n else if (lsp.main) pkg.estimatedEntryPoint = lsp.main\n else pkg.estimatedEntryPoint = '?'\n if ( pkg.estimatedEntryPoint === 'none') pkg.estimatedEntryPoint = '?'\n\n // Homepage - used for a help ref in the Editor\n if (lsp.homepage) pkg.homepage = lsp.homepage\n else pkg.homepage = `https://www.npmjs.com/search?q=${pkgName}`\n\n // The base url used by uib - note this is changed if this is a scoped package\n pkg.packageUrl = '/' + pkgName\n\n // As the url may have changed (by removing scope), record the usable url\n pkg.url = `../uibuilder/vendor${pkg.packageUrl}/${pkg.estimatedEntryPoint}`\n\n // If the package name is npm @scoped, remove the scope, add leading / & track scope name\n if ( pkgName.startsWith('@') ) {\n // pkg.packageUrl = '/' + pkgName.replace(/^@.*\\//, '')\n pkg.scope = pkgName.replace(pkg.packageUrl, '')\n }\n }\n\n if ( pj.dependencies[pkgName] && pj.dependencies[pkgName].includes(':') ) {\n // Must be installed from somewhere other than npmjs so don't try to find latest version\n pkg.latestVersion = null\n pkg.installedFrom = pj.dependencies[pkgName].split(':')[0]\n pkg.outdated = {}\n } else {\n pkg.installedFrom = 'npm'\n\n // Add current version details\n let res = await this.npmOutdated(pkgName)\n try {\n res = JSON.parse(res)\n } catch (e) { /* */ }\n if ( res[pkgName] ) {\n res = {\n current: res[pkgName].current,\n wanted: res[pkgName].wanted,\n latest: res[pkgName].latest,\n }\n }\n pkg.outdated = res\n }\n }\n\n /** Use npm to get detailed pkg info (slow, async) to pj.uibuilder.packages & rewrite the pj file */\n async updateInstalledPackageDetails() {\n const pj = this.uibPackageJson\n\n if ( this.uib === undefined ) throw this.#uibUndefinedError\n if ( this.uib.rootFolder === null ) throw this.#rootFldrNullError\n const rootFolder = this.uib.rootFolder\n\n let ls = ''\n try {\n ls = await this.npmListInstalled(rootFolder)\n } catch {}\n\n let lsParsed = { dependencies: {} }\n try {\n lsParsed = JSON.parse(ls)\n } catch {}\n\n // Make sure we have package details for all installed packages - NB: don't use await with forEach!\n const depPkgNames = Object.keys(lsParsed.dependencies || {})\n // await depPkgNames.forEach( async pkgName => {\n // await this.updIndividualPkgDetails(pkgName, lsParsed)\n // })\n // EITHER (serial)\n // for ( const pkgName of depPkgNames ) {\n // await this.updIndividualPkgDetails(pkgName, lsParsed)\n // }\n // OR (parallel)\n await Promise.all( depPkgNames.map(async (pkgName) => {\n await this.updIndividualPkgDetails(pkgName, lsParsed)\n }))\n\n // (re)Write package.json\n this.writePackageJson(rootFolder, pj)\n }\n\n /** Get /package.json (create it if it doesn't exist), enhance with package details\n * Also make version string same as uibuilder version\n * @returns {object|null} Parsed version of /package.json with uibuilder specific updates\n */\n getUibRootPackageJson() {\n if ( this.log === undefined ) throw this.#logUndefinedError\n if ( this.uib === undefined ) throw this.#uibUndefinedError\n if ( this.uib.rootFolder === null ) throw this.#rootFldrNullError\n\n const log = this.log\n\n if ( this.#isConfigured !== true ) {\n log.warn('[uibuilder:UibPackages:getUibRootPackageJson] Cannot run. Setup has not been called.')\n return null\n }\n\n const uibRoot = this.uib.rootFolder\n const fileName = path.join( uibRoot, this.packageJson )\n\n // Make sure it exists & contains valid JSON - \n if ( !fs.existsSync(fileName) ) {\n log.warn('[uibuilder:package-mgt:getUibRootPackageJson] No uibRoot/package.json file, creating minimal file.')\n this.setUibRootPackageJson()\n }\n\n // Get it\n let pj = {}\n try {\n pj = this.readPackageJson(uibRoot)\n } catch (e) {\n log.error(`[uibuilder:package-mgt:getUibRootPackageJson] Error reading ${fileName}. ${e.message}`)\n this.uibPackageJson = null\n return null\n }\n\n // Make sure there is a dependencies prop\n if ( !pj.dependencies ) pj.dependencies = {}\n // Make sure there is a uibuilder prop\n if ( !pj.uibuilder ) pj.uibuilder = {}\n // Reset the packages list, we rebuild it below\n pj.uibuilder.packages = {}\n\n if (this.uibPackageJson.dependencies !== pj.dependencies ) {\n log.info(`[uibuilder:package-mgt:getUibRootPackageJson] package.json dependencies changed`)\n console.info({'pkg-deps': this.uibPackageJson.dependencies, 'memory-deps': pj.dependencies})\n }\n\n // Update the version string to match uibuilder version\n pj.version = this.uib.version\n\n // Make sure we have package details for all installed packages\n Object.keys(pj.dependencies).forEach( packageName => {\n // Get/Update package details\n pj.uibuilder.packages[packageName] = this.getPackageDetails2(packageName, uibRoot)\n // And save the version/location spec from the dependencies prop so everything is together\n pj.uibuilder.packages[packageName].spec = pj.dependencies[packageName]\n\n // Frig to pick up the version of Bootstrap installed with bootstrap-vue\n if (packageName === 'bootstrap-vue' && !pj.dependencies.bootstrap ) {\n pj.dependencies.bootstrap = pj.uibuilder.packages[packageName].bootstrap\n pj.uibuilder.packages.bootstrap = this.getPackageDetails2('bootstrap', uibRoot)\n pj.uibuilder.packages.bootstrap.spec = pj.dependencies.bootstrap\n }\n })\n\n // Update the /package.json file with updated details & Return it\n if (this.setUibRootPackageJson(pj) === true) return pj\n \n // Failed\n return null\n } // ----- End of getUibRootPackageJson() ----- //\n\n /** Write updated /package.json\n * @param {object} json The Object data to write to the file\n * @returns {boolean} True if write was successful\n */\n setUibRootPackageJson(json) {\n if ( this.log === undefined ) throw this.#logUndefinedError\n if ( this.uib === undefined ) throw this.#uibUndefinedError\n if ( this.uib.rootFolder === null ) throw this.#rootFldrNullError\n\n if ( this.#isConfigured !== true ) {\n this.log.warn('[uibuilder:UibPackages:setUibRootPackageJson] Cannot run. Setup has not been called.')\n return\n }\n\n const uibRoot = this.uib.rootFolder\n const fileName = path.join( uibRoot, this.packageJson )\n\n if (!json) {\n log.warn('[uibuilder:package-mgt:setUibRootPackageJson] Using dummy json')\n json = {\n 'name': 'uib_root',\n 'version': this.uib.version,\n 'description': 'Root configuration and data folder for uibuilder',\n 'scripts': {},\n 'dependencies': {},\n 'homepage': '',\n 'bugs': '',\n 'author': '',\n 'license': 'Apache-2.0',\n 'repository': '',\n 'uibuilder': {\n 'packages': {},\n }\n }\n }\n\n try {\n fs.writeJsonSync(fileName, json, { spaces: 2 })\n // Save it for use elsewhere\n this.uibPackageJson = json\n return true\n } catch (e) {\n log.error(`[uibuilder:package-mgt:setUibRootPackageJson] Error writing ${fileName}. ${e.message}`)\n this.uibPackageJson = null\n return false\n }\n }\n\n /** Find install folder for a package - allows an array of locations to be given\n * NOTE: require.resolve can be a little ODD!\n * When run from a linked package, it uses the link root not the linked location,\n * this throws out the tree search. That's why we have to try several different locations here.\n * Also, it finds the \"main\" script name which might not be in the package root.\n * Also, it won't find ANYTHING if a `main` entry doesn't exist :(\n * So we no longer use it, just search for folder names.\n * @param {string} packageName - Name of the package who's install folder we are looking for.\n * @param {string|Array} installRoot Location to search. Can be an array of locations.\n * @returns {null|string} Actual filing system path to the installed package\n */\n getPackagePath2(packageName, installRoot) {\n if ( this.log === undefined ) throw this.#logUndefinedError\n\n if ( this.#isConfigured !== true ) {\n this.log.warn('[uibuilder:UibPackages:getPackagePath] Cannot run. Setup has not been called.')\n return null\n }\n\n // If installRoot = string, make an array\n if ( !Array.isArray(installRoot) ) installRoot = [installRoot]\n\n for (const r of installRoot) {\n const loc = path.join(r, 'node_modules', packageName)\n if (fs.existsSync( loc )) return loc\n }\n\n this.log.warn(`[uibuilder:package-mgt:getPackagePath2] PACKAGE ${packageName} NOT FOUND`)\n return null\n } // ---- End of getPackagePath2 ---- //\n\n /** Get the details for an installed package & update uibuilder specific details before returning it\n * @param {string} packageName - Name of the package who's install folder we are looking for.\n * @param {string} installRoot A uibuilder node instance - will search in node's root folder first\n * @returns {object|null} Details object for an installed package\n */\n getPackageDetails2(packageName, installRoot) {\n if ( this.log === undefined ) throw this.#logUndefinedError\n\n if ( this.#isConfigured !== true ) {\n this.log.warn('[uibuilder:UibPackages:getPackagePath2] Cannot run. Setup has not been called.')\n return null\n }\n\n // Trim the input just in case\n packageName = packageName.trim()\n\n const folder = this.getPackagePath2(packageName, installRoot)\n if ( folder === null ) throw new Error('folder is null')\n const pkgJson = this.readPackageJson(folder)\n\n const pkgDetails = { 'installFolder': folder }\n // if ( pkgDetails === undefined ) throw new Error('pkgDetails is undefined')\n if (pkgJson.version) pkgDetails.installedVersion = pkgJson.version\n\n /** If we can, lets work out what resource is actually needed\n * when using one of these packages in the browser.\n * If we can't, leave a ? to make it obvious\n * Annoyingly, a few packages have decided to make the `browser` property an object instead of a string.\n * (e.g. vgauge) - ignore in that case as it isn't clear what the intent is.\n */\n if (pkgJson.browser && (typeof pkgJson.browser === 'string') ) pkgDetails.estimatedEntryPoint = pkgJson.browser\n else if (pkgJson.jsdelivr) pkgDetails.estimatedEntryPoint = pkgJson.jsdelivr\n else if (pkgJson.unpkg) pkgDetails.estimatedEntryPoint = pkgJson.unpkg\n else if (pkgJson.main) pkgDetails.estimatedEntryPoint = pkgJson.main\n else pkgDetails.estimatedEntryPoint = '?'\n if ( pkgDetails.estimatedEntryPoint === 'none') pkgDetails.estimatedEntryPoint = '?'\n\n // Homepage - used for a help ref in the Editor\n if (pkgJson.homepage) pkgDetails.homepage = pkgJson.homepage\n else pkgDetails.homepage = `https://www.npmjs.com/search?q=${packageName}`\n\n // The base url used by uib - note this is changed if this is a scoped package\n pkgDetails.packageUrl = '/' + packageName\n\n // Work out what kind of package this is\n\n // If the package name is npm @scoped, remove the scope, add leading / & track scope name\n if ( pkgDetails.packageUrl.startsWith('@') ) {\n pkgDetails.packageUrl = '/' + packageName.replace(/^@.*\\//, '')\n pkgDetails.scope = packageName.replace(pkgDetails.packageUrl, '')\n }\n\n // As the url may have changed (by removing scope), record the usable url\n pkgDetails.url = `../uibuilder/vendor${pkgDetails.packageUrl}/${pkgDetails.estimatedEntryPoint}`\n\n // Frig to pick up the version of Bootstrap installed with bootstrap-vue\n if (packageName === 'bootstrap-vue') {\n pkgDetails.bootstrap = pkgJson.dependencies.bootstrap\n }\n\n // Add current version details\n // pkgDetails.outdated = this.npmOutdated(packageName)\n // console.log('pkgDetails.outdated', pkgDetails.outdated)\n // this.npmOutdated(packageName)\n // .then(res => {\n // try {\n // res = JSON.parse(res)\n // } catch(e) { /* */ }\n // if ( res[packageName] ) {\n // res = {\n // current: res[packageName].current,\n // wanted: res[packageName].wanted,\n // latest: res[packageName].latest,\n // }\n // }\n // pkgDetails.outdated = res\n // return true\n // })\n // .catch( err => {\n // //\n // })\n\n return pkgDetails\n } // ---- End of getPackageDetails2 ---- //\n\n //#region --- DEPRECATED ---\n\n /** Update all of the installed packages\n */\n updateInstalledPackages() {\n this.log.error('[uibuilder:UibPackages:updateInstalledPackages] FUNCTION IS DEPRECATED.')\n console.trace()\n\n console.trace('package-mgt.js:updateInstalledPackages')\n } // ---- End of updateInstalledPackages ---- //\n\n /** !DEPRECATED! Find install folder for a package\n */\n getPackagePath() {\n this.log.error('[uibuilder:UibPackages:getPackagePath] FUNCTION IS DEPRECATED.')\n console.trace()\n\n console.trace('package-mgt.js:getPackagePath')\n } // ---- End of getPackagePath ---- //\n\n /** Update the master name list of possible packages that could be served to the front-end\n */\n updateMergedPackageList() {\n this.log.error('[uibuilder:UibPackages:updateMergedPackageList] FUNCTION IS DEPRECATED.')\n console.trace()\n\n console.trace('package-mgt.js:updateMergedPackageList')\n } // ---- End of updateMergedPackageList ---- //\n\n //#endregion --- DEPRECATED ---\n\n /** Install an npm package\n * NOTE: This fn does not update the list of packages\n * because that is built from the package.json file\n * and that is updated by calling web.serveVendorPackages()\n * which can't be done here - The calling admin API's do that\n * Editor->API->This fn->API cont.->web.serveVendorPackages->getUibRootPackageJson->API cont2->Editor\n * @param {string} url Node instance url\n * @param {string} pkgName The npm name of the package (with scope prefix, version, etc if needed)\n * @param {string} [tag] Default=''. Specifier for a version, tag, branch, etc. with leading @ for npm and # for GitHub installs\n * @param {string} [toLocation] Where to install to. Defaults to uibRoot\n * @returns {Promise} [Combined stdout/stderr, updated list of package details]\n */\n async npmInstallPackage(url, pkgName, tag = '', toLocation = '') {\n if ( this.log === undefined ) throw this.#logUndefinedError\n\n if ( this.#isConfigured !== true ) {\n this.log.warn('[uibuilder:UibPackages:npmInstallPackage] Cannot run. Setup has not been called.')\n return ''\n }\n\n if ( this.uib === undefined ) throw this.#uibUndefinedError\n if ( this.uib.rootFolder === null ) throw new Error('this.log.rootFolder is null')\n if ( toLocation === '' ) toLocation = this.uib.rootFolder\n\n // https://github.com/sindresorhus/execa#options\n const opts = {\n 'cwd': toLocation,\n 'all': true,\n }\n const args = [ // `npm install --no-audit --no-update-notifier --save --production --color=false --no-fund --json ${params.package}@latest`\n 'install',\n '--no-fund',\n '--no-audit',\n '--no-update-notifier',\n '--save',\n '--production',\n '--color=false',\n // '--json',\n pkgName + tag,\n ]\n\n // Don't need a try since we don't do any processing on an execa error - if cmd fails, the promise is rejected\n const { all } = await execa('npm', args, opts)\n this.log.info(`[uibuilder:UibPackages:npmInstallPackage] npm output: \\n ${all}\\n `)\n\n return /** @type {string} */ (all)\n\n } // ---- End of installPackage ---- //\n\n /** Install an npm package\n * NOTE: This fn does not update the list of packages - see install above for reasons.\n * @param {string} pkgName The npm name of the package (with scope prefix, version, etc if needed)\n * @returns {Promise} Combined stdout/stderr\n */\n async npmRemovePackage(pkgName) {\n if ( this.log === undefined ) throw this.#logUndefinedError\n if ( this.uib === undefined ) throw this.#uibUndefinedError\n if ( this.uib.rootFolder === null ) throw this.#rootFldrNullError\n\n if ( this.#isConfigured !== true ) {\n this.log.warn('[uibuilder:UibPackages:npmRemovePackage] Cannot run. Setup has not been called.')\n return ''\n }\n\n // https://github.com/sindresorhus/execa#options\n const opts = {\n 'cwd': this.uib.rootFolder,\n 'all': true,\n }\n const args = [\n 'uninstall',\n '--save',\n '--color=false',\n '--no-fund',\n '--no-audit',\n '--no-update-notifier',\n // '--json',\n pkgName,\n ]\n\n // Don't need a try since we don't do any processing on an execa error - if cmd fails, the promise is rejected\n const { all } = await execa('npm', args, opts)\n this.log.info(`[uibuilder:UibPackages:npmRemovePackage] npm output: \\n ${all}\\n `)\n\n return /** @type {string} */ (all)\n\n } // ---- End of removePackage ---- //\n\n /** List all npm packages installed at the top-level of a folder\n * @param {string=} folder The folder to start the list in\n * @returns {Promise} Command output\n */\n async npmListInstalled(folder) {\n this.log.trace('[uibuilder:package-mgt:npmListInstalled] npm list installed started')\n\n // if ( this._isConfigured !== true ) {\n // this.log.warn('[uibuilder:UibPackages:npmListInstalled] Cannot run. Setup has not been called.')\n // return\n // }\n\n // https://github.com/sindresorhus/execa#options\n const opts = {\n 'cwd': folder,\n 'all': true,\n }\n const args = [\n 'list',\n '--long',\n '--json',\n '--depth=0',\n ]\n\n let res\n try {\n const { stdout } = await execa('npm', args, opts)\n // console.log('>>>>>', stdout)\n res = stdout\n } catch (e) {\n // console.log('>>>>>', e.message)\n res = e.stdout\n }\n\n this.log.trace('[uibuilder:package-mgt:npmListInstalled] npm list installed completed')\n return res\n } // ---- End of npmListInstalled ---- //\n\n /** Get the latest version string for a package\n * @param {string} pkgName The npm name of the package (with scope prefix, version, etc if needed)\n * @returns {Promise} Combined stdout/stderr\n */\n async npmOutdated(pkgName) {\n if ( this.log === undefined ) throw this.#logUndefinedError\n if ( this.uib === undefined ) throw this.#uibUndefinedError\n if ( this.uib.rootFolder === null ) throw this.#rootFldrNullError\n\n if ( this.#isConfigured !== true ) {\n this.log.warn('[uibuilder:UibPackages:npmOutdated] Cannot run. Setup has not been called.')\n return\n }\n\n // https://github.com/sindresorhus/execa#options\n const opts = {\n 'cwd': this.uib.rootFolder,\n 'all': true,\n }\n const args = [ // `npm remove --no-audit --no-update-notifier --color=false --json ${params.package}` // --save-prefix=\"~\"\n 'outdated',\n '--json',\n pkgName,\n ]\n\n let res\n try {\n const { stdout } = await execa('npm', args, opts)\n // const {stdout} = execa.sync('npm', args, opts)\n res = stdout\n } catch (err) {\n res = err.stdout\n }\n\n this.log.trace(`[uibuilder:UibPackages:npmOutdated] npm output: \\n ${res}\\n `)\n\n return res\n\n } // ---- End of npmOutdated ---- //\n\n /** Update an npm package (Not yet in use)\n * @param {string} pkgName The npm name of the package (with scope prefix, version, etc if needed)\n * @returns {Promise} Combined stdout/stderr\n */\n async npmUpdate(pkgName) {\n if ( this.log === undefined ) throw this.#logUndefinedError\n\n if ( this.#isConfigured !== true ) {\n this.log.warn('[uibuilder:UibPackages:npmUpdate] Cannot run. Setup has not been called.')\n return ''\n }\n\n if ( this.uib === undefined ) throw this.#uibUndefinedError\n if ( this.uib.rootFolder === null ) throw new Error('this.log.rootFolder is null')\n // if ( toLocation === '' ) toLocation = this.uib.rootFolder\n const toLocation = this.uib.rootFolder\n\n // https://github.com/sindresorhus/execa#options\n const opts = {\n 'cwd': toLocation,\n 'all': true,\n }\n const args = [\n 'update',\n '--no-fund',\n '--no-audit',\n '--no-update-notifier',\n '--save',\n '--production',\n '--color=false',\n // '--json',\n pkgName,\n ]\n\n // Don't need a try since we don't do any processing on an execa error - if cmd fails, the promise is rejected\n const { all } = await execa('npm', args, opts)\n this.log.info(`[uibuilder:UibPackages:npmUpdate] npm output: \\n ${all}\\n `)\n\n return /** @type {string} */ (all)\n\n }\n\n} // ----- End of UibPackages ----- //\n\n/** Singleton model. Only 1 instance of UibWeb should ever exist.\n * Use as: `const packageMgt = require('./package-mgt.js')`\n */\n// @ts-ignore\nconst uibPackages = new UibPackages()\nmodule.exports = uibPackages\n\n// EOF\n"], + "mappings": "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", + "names": ["path", "fs", "execa", "UibPackages", "#isConfigured", "#logUndefinedError", "#uibUndefinedError", "#rootFldrNullError", "opts", "args", "res", "e", "uib", "log", "pj", "pkgName", "depName", "pkg", "folder", "file", "err", "json", "fileName", "uibRoot", "lsParsed", "packages", "lsp", "rootFolder", "ls", "depPkgNames", "packageName", "installRoot", "r", "loc", "pkgJson", "pkgDetails", "url", "tag", "toLocation", "all", "stdout", "uibPackages"] +} diff --git a/nodes/libs/socket.js b/nodes/libs/socket.js index 5be86f29..103c831d 100644 --- a/nodes/libs/socket.js +++ b/nodes/libs/socket.js @@ -1,686 +1,2 @@ -/** Manage Socket.IO on behalf of uibuilder - * Singleton. only 1 instance of this class will ever exist. So it can be used in other modules within Node-RED. - * - * Copyright (c) 2017-2023 Julian Knight (Totally Information) - * https://it.knightnet.org.uk, https://github.com/TotallyInformation/node-red-contrib-uibuilder - * - * 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. - */ -/* eslint-disable class-methods-use-this, sonarjs/no-duplicate-string, max-params */ -'use strict' - -/** --- Type Defs --- - * @typedef {import('../../typedefs.js').runtimeRED} runtimeRED - * @typedef {import('../../typedefs.js').MsgAuth} MsgAuth - * @typedef {import('../../typedefs.js').uibNode} uibNode - * @typedef {import('../../typedefs.js').uibConfig} uibConfig - * @typedef {import('Express')} Express - */ - -const path = require('path') -const fs = require('fs-extra') -const socketio = require('socket.io') -const tilib = require('./tilib') // General purpose library (by Totally Information) -const uiblib = require('./uiblib') // Utility library for uibuilder -// const security = require('./sec-lib') // uibuilder security module -const tiEventManager = require('@totallyinformation/ti-common-event-handler') - -/** Get client real ip address - NB: Optional chaining (?.) is node.js v14 not v12 - * @param {socketio.Socket} socket Socket.IO socket object - * @returns {string | string[] | undefined} Best estimate of the client's real IP address - */ -function getClientRealIpAddress(socket) { - let clientRealIpAddress - if ( 'headers' in socket.request && 'x-real-ip' in socket.request.headers) { - // get ip from behind a nginx proxy or proxy using nginx's 'x-real-ip header - clientRealIpAddress = socket.request.headers['x-real-ip'] - } else if ( 'headers' in socket.request && 'x-forwarded-for' in socket.request.headers) { - // else get ip from behind a general proxy - if (socket.request.headers['x-forwarded-for'] === undefined) throw new Error('socket.request.headers["x-forwarded-for"] is undefined') - if (!Array.isArray(socket.request.headers['x-forwarded-for'])) socket.request.headers['x-forwarded-for'] = [socket.request.headers['x-forwarded-for']] - clientRealIpAddress = socket.request.headers['x-forwarded-for'][0].split(',').shift() - } else if ( 'connection' in socket.request && 'remoteAddress' in socket.request.connection ) { - // else get ip from socket.request that returns the reference to the request that originated the underlying engine.io Client - clientRealIpAddress = socket.request.connection.remoteAddress - } else { - // else get ip from socket.handshake that is a object that contains handshake details - clientRealIpAddress = socket.handshake.address - } - - // socket.client.conn.remoteAddress - - // Switch to this code when node.js v14 becomes the baseline version - // const clientRealIpAddress = - // //get ip from behind a nginx proxy or proxy using nginx's 'x-real-ip header - // socket.request?.headers['x-real-ip'] - // //get ip from behind a general proxy - // || socket.request?.headers['x-forwarded-for']?.split(',').shift() //if more thatn one x-fowared-for the left-most is the original client. Others after are successive proxys that passed the request adding to the IP addres list all the way back to the first proxy. - // //get ip from socket.request that returns the reference to the request that originated the underlying engine.io Client - // || socket.request?.connection?.remoteAddress - // // get ip from socket.handshake that is a object that contains handshake details - // || socket.handshake?.address - - return clientRealIpAddress -} // --- End of getClientRealIpAddress --- // - -/** Get client real ip address - NB: Optional chaining (?.) is node.js v14 not v12 - * @param {socketio.Socket} socket Socket.IO socket object - * @param {uibNode} node Reference to the uibuilder node instance - * @returns {string | string[] | undefined} Best estimate of the client's real IP address - */ -function getClientPageName(socket, node) { - let pageName = socket.handshake.auth.pathName.replace(`/${node.url}/`, '') - if ( pageName.endsWith('/') ) pageName += 'index.html' - if ( pageName === '' ) pageName = 'index.html' - - return pageName -} // --- End of getClientPageName --- // - -class UibSockets { - // TODO: Replace _XXX with #XXX once node.js v14 is the minimum supported version - /** Flag to indicate whether setup() has been run - * @type {boolean} - * @protected - */ - // _isConfigured = false - - /** Called when class is instantiated */ - constructor() { - // setup() has not yet been run - this._isConfigured = false - - //#region ---- References to core Node-RED & uibuilder objects ---- // - /** @type {runtimeRED|undefined} */ - this.RED = undefined - /** @type {uibConfig|undefined} Reference link to uibuilder.js global configuration object */ - this.uib = undefined - /** Reference to uibuilder's global log functions */ - this.log = undefined - /** Reference to ExpressJS server instance being used by uibuilder - * Used to enable the Socket.IO client code to be served to the front-end - */ - this.server = undefined - //#endregion ---- References to core Node-RED & uibuilder objects ---- // - - //#region ---- Common variables ---- // - - /** URI path for accessing the socket.io client from FE code. Based on the uib node instance URL. - * @constant {string} uib_socketPath */ - this.uib_socketPath = undefined - - /** An instance of Socket.IO Server */ - this.io = undefined - - /** Collection of Socket.IO namespaces - * Each namespace correstponds to a uibuilder node instance and must have a unique namespace name that matches the unique URL parameter for the node. - * The namespace is stored in the this.ioNamespaces object against a property name matching the URL so that it can be referenced later. - * Because this is a Singleton object, any reference to this module can access all of the namespaces (by url). - * The namespace has some uib extensions that track the originating node id (searchable in Node-RED), the number of connected clients - * and the number of messages recieved. - * @type {!Object} - */ - this.ioNamespaces = {} - - //#endregion ---- ---- // - - } // --- End of constructor() --- // - - /** Assign uibuilder and Node-RED core vars to Class static vars. - * This makes them available wherever this MODULE is require'd. - * Because JS passess objects by REFERENCE, updates to the original - * variables means that these are updated as well. - * @param {uibConfig} uib reference to uibuilder 'global' configuration object - * @param {Express} server reference to ExpressJS server being used by uibuilder - */ - setup( uib, server ) { - if ( !uib || !server ) throw new Error('[uibuilder:socket.js:setup] Called without required parameters or uib and/or server are undefined.') - if (uib.RED === null) throw new Error('[uibuilder:socket.js:setup] uib.RED is null') - - // Prevent setup from being called more than once - if ( this._isConfigured === true ) { - uib.RED.log.warn('[uibuilder:web:setup] Setup has already been called, it cannot be called again.') - return - } - - /** reference to Core Node-RED runtime object */ - this.RED = uib.RED - - this.uib = uib - this.log = uib.RED.log - this.server = server - - // TODO: Replace _XXX with #XXX once node.js v14 is the minimum supported version - this._socketIoSetup() - - if (uib.configFolder === null) throw new Error('[uibuilder:socket.js:setup] uib.configFolder is null') - - // If available, set up optional outbound msg middleware - this.outboundMsgMiddleware = function outboundMsgMiddleware( msg, url, channel ) { return null } - // Try to load the sioMsgOut middleware function - sioMsgOut applies to all outgoing msgs - const mwfile = path.join(uib.configFolder, uib.sioMsgOutMwName) - if ( fs.existsSync(mwfile) ) { // not interested if the file doesn't exist - try { - const sioMsgOut = require( mwfile ) - if ( typeof sioMsgOut === 'function' ) { // if exported, has to be a function - this.outboundMsgMiddleware = sioMsgOut - this.log.trace('[uibuilder:socket:setup] sioMsgOut Middleware loaded successfully.') - } else { - this.log.warn('[uibuilder:socket:setup] sioMsgOut Middleware failed to load - check that uibRoot/.config/sioMsgOut.js has a valid exported fn.') - } - } catch (e) { - this.log.warn(`[uibuilder:socket:setup] sioMsgOut middleware Failed to load. Reason: ${e.message}`) - } - } - - this._isConfigured = true - - } // --- End of setup() --- // - - /** Holder for Socket.IO - we want this to survive redeployments of each node instance - * so that existing clients can be reconnected. - * Start Socket.IO - make sure the right version of SIO is used so keeping this separate from other - * modules that might also use it (path). This is only needed ONCE for ALL uib.instances of this node. - * Must only be run once and so is made an ECMA2018 private class method - * @private - */ - _socketIoSetup() { - // Reference static vars - const uib = this.uib - const RED = this.RED - const log = this.log - const server = this.server - - if (uib === undefined) throw new Error('uib is undefined') - if (RED === undefined) throw new Error('RED is undefined') - if (log === undefined) throw new Error('log is undefined') - - const uibSocketPath = this.uib_socketPath = tilib.urlJoin(uib.nodeRoot, uib.moduleName, 'vendor', 'socket.io') - - log.trace(`[uibuilder:socket:socketIoSetup] Socket.IO initialisation - Socket Path=${uibSocketPath}, CORS Origin=*` ) - // Socket.Io server options, see https://socket.io/docs/v4/server-options/ - let ioOptions = { - 'path': uibSocketPath, - serveClient: true, // Needed for backwards compatibility - connectionStateRecovery: { - // the backup duration of the sessions and the packets - maxDisconnectionDuration: 120000, // Default = 2 * 60 * 1000 = 120000, - // whether to skip middlewares upon successful recovery - skipMiddlewares: true, // Default = true - }, - // https://github.com/expressjs/cors#configuration-options, https://socket.io/docs/v3/handling-cors/ - cors: { - origin: '*', - // allowedHeaders: ['x-clientid'], - }, - /* // Socket.Io 3+ CORS is disabled by default, also options have changed. - // for CORS need to handle preflight request explicitly 'cause there's an - // Allow-Headers:X-ClientId in there. see https://socket.io/docs/v4/handling-cors/ - handlePreflightRequest: (req, res) => { - res.writeHead(204, { - 'Access-Control-Allow-Origin': req.headers['origin'], // eslint-disable-line dot-notation - 'Access-Control-Allow-Methods': 'GET,POST', - 'Access-Control-Allow-Headers': 'X-ClientId', - 'Access-Control-Allow-Credentials': true, - }) - res.end() - }, */ - } - - // Merge in overrides from settings.js if given. NB: settings.uibuilder.socketOptions will override the above defaults. - if ( RED.settings.uibuilder && RED.settings.uibuilder.socketOptions ) { - ioOptions = Object.assign( {}, ioOptions, RED.settings.uibuilder.socketOptions ) - } - - // @ts-ignore ts(2769) - this.io = new socketio.Server(server, ioOptions) // listen === attach - - } // --- End of socketIoSetup() --- // - - /** Allow the isConfigured flag to be read (not written) externally - * @returns {boolean} True if this class as been configured - */ - get isConfigured() { - return this._isConfigured - } - - // ? Consider adding isConfigered checks on each method? - - /** Output a msg to the front-end. - * @param {object} msg The message to output, include msg._socketId to send to a single client - * @param {string} url THe uibuilder id - * @param {string=} channel Optional. Which channel to send to (see uib.ioChannels) - defaults to client - */ - sendToFe( msg, url, channel ) { - const uib = this.uib - const log = this.log - - if (uib === undefined) throw new Error('uib is undefined') - if (log === undefined) throw new Error('log is undefined') - - if ( channel === undefined ) channel = uib.ioChannels.client - - const ioNs = this.ioNamespaces[url] - - const socketId = msg._socketId || undefined - - // Control msgs should say where they came from - if ( channel === uib.ioChannels.control && !msg.from ) msg.from = 'server' - - // Process outbound middleware (middleware is loaded in this.setup) - try { - this.outboundMsgMiddleware( msg, url, channel, ioNs ) - } catch (e) { - log.warn(`[uibuilder:socket:sendToFe] outboundMsgMiddleware middleware failed to run. Reason: ${e.message}`) - } - - // TODO: Sending should have some safety validation on it. Is msg an object? Is channel valid? - - // pass the complete msg object to the uibuilder client - if (socketId !== undefined) { // Send to specific client - log.trace(`[uibuilder:socket.js:sendToFe:${url}] msg sent on to client ${socketId}. Channel: ${channel}. ${JSON.stringify(msg)}`) - ioNs.to(socketId).emit(channel, msg) - } else { // Broadcast - log.trace(`[uibuilder:socket.js:sendToFe:${url}] msg sent on to ALL clients. Channel: ${channel}. ${JSON.stringify(msg)}`) - ioNs.emit(channel, msg) - } - - } // ---- End of sendToFe ---- // - - /** Output a normal msg to the front-end. Can override socketid - * Currently only used for the auto-reload on edit in admin-api-v2.js - * @param {object} msg The message to output - * @param {object} url The uibuilder instance url - will be unique. Used to lookup the correct Socket.IO namespace for sending. - * @param {string=} socketId Optional. If included, only send to specific client id (mostly expecting this to be on msg._socketID so not often required) - */ - sendToFe2(msg, url, socketId) { // eslint-disable-line class-methods-use-this - const uib = this.uib - const ioNs = this.ioNamespaces[url] - - if (uib === undefined) throw new Error('uib is undefined') - if (this.log === undefined) throw new Error('this.log is undefined') - - if (socketId) msg._socketId = socketId - - // TODO: This should have some safety validation on it - if (msg._socketId) { - this.log.trace(`[uibuilder:socket:sendToFe2:${url}] msg sent on to client ${msg._socketId}. Channel: ${uib.ioChannels.server}. ${JSON.stringify(msg)}`) - ioNs.to(msg._socketId).emit(uib.ioChannels.server, msg) - } else { - this.log.trace(`[uibuilder:socket:sendToFe2:${url}] msg sent on to ALL clients. Channel: ${uib.ioChannels.server}. ${JSON.stringify(msg)}`) - ioNs.emit(uib.ioChannels.server, msg) - } - } // ---- End of sendToFe2 ---- // - - /** Send a uibuilder control message out of port #2 - * Note: this.getClientDetails is used before calling this if client details needed - * @param {object} msg The message to output - * @param {uibNode} node Reference to the uibuilder node instance - */ - sendCtrlMsg(msg, node) { - node.send( [null, msg]) - } - - /** Get client details for including in Node-RED messages - * @param {socketio.Socket} socket Reference to client socket connection - * @param {uibNode} node Reference to the uibuilder node instance - * @returns {object} Extracted key information - */ - getClientDetails(socket, node) { - - // Add page name meta to allow caches and other flows to send back to specific page - // Note, could use socket.handshake.auth.pageName instead - let pageName - if ( socket.handshake.auth.pathName ) { - pageName = getClientPageName(socket, node) - } - - return { - '_socketId': socket.id, - // Let the flow know what v of uib client is in use - 'version': socket.handshake.auth.clientVersion, - /** Do our best to get the actual IP addr of client despite any Proxies */ - 'ip': getClientRealIpAddress(socket), - /** What is the stable client id (set by uibuilder, retained till browser restart) */ - 'clientId': socket.handshake.auth.clientId, - /** What is the client tab identifier (set by uibuilder modern client) */ - 'tabId': socket.handshake.auth.tabId, - /** What was the originating uibuilder URL */ - 'url': node.url, - /** What was the originating page name (for SPA's) */ - 'pageName': pageName, - /** The browser's URL parameters */ - 'urlParams': socket.handshake.auth.urlParams, - /** How many times has this client reconnected (e.g. after sleep) */ - 'connections': socket.handshake.auth.connectedNum, - /** What type of client nav happened previously */ - 'lastNavType': socket.handshake.auth.lastNavType, - /** True if https/wss */ - 'tls': socket.handshake.secure, - /** When the client connected to the server */ - 'connectedTimestamp': (new Date(socket.handshake.issued)).toISOString(), - /** THe referring webpage, should be the full URL of the uibuilder page */ - 'referer': socket.request.headers.referer, - /** Is this client reconnected after temp loss? */ - 'recovered': socket.recovered, - - // ? client time offset ? - } - } - - /** Get a uib node instance namespace - * @param {string} url The uibuilder node instance's url (identifier) - * @returns {socketio.Namespace} Return a reference to the namespace of the specified uib instance for convenience in core code - */ - getNs(url) { - return this.ioNamespaces[url] - } - - /** Send a node-red msg either directly out of the node instance OR via return event name - * @param {object} msg Message object received from a client - * @param {uibNode} node Reference to the uibuilder node instance - */ - sendIt(msg, node) { - if ( msg._uib && msg._uib.originator && (typeof msg._uib.originator === 'string') ) { - // const eventName = `node-red-contrib-uibuilder/return/${msg._uib.originator}` - tiEventManager.emit(`node-red-contrib-uibuilder/return/${msg._uib.originator}`, msg) - } else { - node.send(msg) - } - } - - /** Socket listener fn for msgs from clients - NOTE that the optional sioUse middleware is also applied before this - * @param {object} msg Message object received from a client - * @param {socketio.Socket} socket Reference to the socket for this node - * @param {uibNode} node Reference to the uibuilder node instance - */ - listenFromClient(msg, socket, node) { - const log = this.log - if (log === undefined) throw new Error('log is undefined') - - node.rcvMsgCount++ - log.trace(`[uibuilder:socket:${node.url}] Data received from client, ID: ${socket.id}, Msg: ${JSON.stringify(msg)}`) - - // Make sure the incoming msg is a correctly formed Node-RED msg - switch ( typeof msg ) { - case 'string': - case 'number': - case 'boolean': - msg = { 'topic': node.topic, 'payload': msg } - } - - // If the sender hasn't added msg._socketId, add the Socket.id now - if ( !Object.prototype.hasOwnProperty.call(msg, '_socketId') ) msg._socketId = socket.id - - // If required, add/merge the client details to the msg using msg._uib - if (node.showMsgUib) { - if (!msg._uib) msg._uib = this.getClientDetails(socket, node) - else { - msg._uib = { - ...msg._uib, - ...this.getClientDetails(socket, node) - } - } - } - - // Send out the message for downstream flows - // TODO: This should probably have safety validations! - this.sendIt(msg, node) - - } // ---- End of listenFromClient ---- // - - /** Add a new Socket.IO NAMESPACE - * Each namespace correstponds to a uibuilder node instance and must have a unique namespace name that matches the unique URL parameter for the node. - * The namespace is stored in the this.ioNamespaces object against a property name matching the URL so that it can be referenced later. - * Because this is a Singleton object, any reference to this module can access all of the namespaces (by url). - * The namespace has some uib extensions that track the originating node id (searchable in Node-RED), the number of connected clients - * and the number of messages received. - * @param {uibNode} node Reference to the uibuilder node instance - */ - addNS(node) { - const log = this.log - const uib = this.uib - - if (log === undefined) throw new Error('log is undefined') - if (uib === undefined) throw new Error('uib is undefined') - if (this.io === undefined) throw new Error('this.io is undefined') - - const ioNs = this.ioNamespaces[node.url] = this.io.of(node.url) - - // @ts-expect-error Add some additional metadata to NS - const url = ioNs.url = node.url - // @ts-expect-error Allows us to track back to the actual node in Node-RED - ioNs.nodeId = node.id - // @ts-expect-error ioNs.useSecurity = node.useSecurity // Is security on for this node instance? - ioNs.rcvMsgCount = 0 - // @ts-expect-error Make Node-RED's log available to middleware via custom ns property - ioNs.log = log - // ioNs.clientLog = {} - - if (uib.configFolder === null) throw new Error('uib.configFolder is undefined') - - /** Check for /.config/sioMiddleware.js, use it if present. - * Applies ONCE on a new client connection. - * Had to move to addNS since MW no longer globally loadable since sio v3 - */ - const sioMwPath = path.join(uib.configFolder, 'sioMiddleware.js') - if ( fs.existsSync(sioMwPath) ) { // not interested if the file doesn't exist - try { - const sioMiddleware = require(sioMwPath) - if ( typeof sioMiddleware === 'function' ) { - ioNs.use(sioMiddleware) - log.trace(`[uibuilder:socket:addNs:${url}] Socket.IO sioMiddleware.js middleware loaded successfully for NS.`) - } else { - log.warn(`[uibuilder:socket:addNs:${url}] Socket.IO middleware failed to load for NS - check that uibRoot/.config/sioMiddleware.js has a valid exported fn.`) - } - } catch (e) { - log.warn(`[uibuilder:socket:addNs:${url}] Socket.IO middleware failed to load for NS. Reason: ${e.message}`) - } - } - - const that = this - - ioNs.on('connection', (socket) => { - - //#region ----- Event Handlers ----- // - - // NOTE: as of sio v4, disconnect seems to be fired AFTER a connect when a client reconnects - socket.on('disconnect', (reason, description) => { - - // ioNs.clientLog[socket.handshake.auth.clientId].connected = false - - node.ioClientsCount = ioNs.sockets.size - log.trace( - `[uibuilder:socket:${url}:disconnect] Client disconnected, clientCount: ${ioNs.sockets.size}, Reason: ${reason}, ID: ${socket.id}, IP Addr: ${getClientRealIpAddress(socket)}, Client ID: ${socket.handshake.auth.clientId}. For node ${node.id}` - ) - node.statusDisplay.text = 'connected ' + ioNs.sockets.size - uiblib.setNodeStatus( node ) - - // Let the control output port know a client has disconnected - const ctrlMsg = { - ...{ - 'uibuilderCtrl': 'client disconnect', - 'reason': reason, - 'topic': node.topic || undefined, - 'from': 'server', - 'description': description, - }, - ...that.getClientDetails(socket, node), - } - - that.sendToFe(ctrlMsg, node.url, uib.ioChannels.control) - - // Copy to port#2 for reference - that.sendCtrlMsg(ctrlMsg, node) - - // Let other nodes know a client is disconnecting (via custom event manager) - tiEventManager.emit(`node-red-contrib-uibuilder/${this.url}/clientDisconnect`, ctrlMsg) - - }) // --- End of on-connection::on-disconnect() --- // - - // Listen for msgs from clients on standard channel - socket.on(uib.ioChannels.client, function(msg) { - that.listenFromClient(msg, socket, node ) - }) // --- End of on-connection::on-incoming-client-msg() --- // - - // Listen for msgs from clients on control channel - socket.on(uib.ioChannels.control, function(msg) { - node.rcvMsgCount++ - log.trace(`[uibuilder:socket:${url}] Control Msg from client, ID: ${socket.id}, Msg: ${JSON.stringify(msg)}`) - - // Make sure the incoming msg is a correctly formed Node-RED msg - switch ( typeof msg ) { - case 'string': - case 'number': - case 'boolean': - msg = { 'uibuilderCtrl': msg } - } - - // Apply standard client details to the control msg - msg = { ...msg, ...that.getClientDetails(socket, node) } - - // Control msgs should say where they came from - msg.from = 'client' - - if ( !msg.topic ) msg.topic = node.topic - - that.sendCtrlMsg(msg, node) - - }) // --- End of on-connection::on-incoming-control-msg() --- // - - // Listen for socket.io errors - output a control msg - socket.on('error', function(err) { - - log.error(`[uibuilder:socket:addNs:${url}] ERROR received, ID: ${socket.id}, Reason: ${err.message}`) - - // Let the control output port (port #2) know there has been an error - const ctrlMsg = { - ...{ - uibuilderCtrl: 'socket error', - error: err.message, - from: 'server', - }, - ...that.getClientDetails(socket, node), - } - - that.sendCtrlMsg(ctrlMsg, node) - - }) // --- End of on-connection::on-error() --- // - - //#endregion ----- Event Handlers ----- // - - //#region ---- run when client connects ---- // - - // How many client connections are there? - node.ioClientsCount = ioNs.sockets.size - - log.trace( - `[uibuilder:socket:addNS:${url}:connect] Client connected. ClientCount: ${ioNs.sockets.size}, Socket ID: ${socket.id}, IP Addr: ${getClientRealIpAddress(socket)}, Client ID: ${socket.handshake.auth.clientId}, Recovered?: ${socket.recovered}, Client Version: ${socket.handshake.auth.clientVersion}. For node ${node.id}` - ) - - if (uib.configFolder === null) throw new Error('uib.configFolder is undefined') - - // Try to load the sioUse middleware function - sioUse applies to all incoming msgs - const mwfile = path.join(uib.configFolder, uib.sioUseMwName) - if ( fs.existsSync(mwfile) ) { // not interested if the file doesn't exist - try { - const sioUseMw = require( mwfile ) - if ( typeof sioUseMw === 'function' ) { // if exported, has to be a function - socket.use(sioUseMw) - log.trace(`[uibuilder:socket:onConnect:${url}] sioUse sioUse.js middleware loaded successfully for NS ${url}.`) - } else { - log.warn(`[uibuilder:socket:onConnect:${url}] sioUse middleware failed to load for NS ${url} - check that uibRoot/.config/sioUse.js has a valid exported fn.`) - } - } catch (e) { - log.warn(`[uibuilder:socket:addNS:${url}] sioUse failed to load Use middleware. Reason: ${e.message}`) - } - } - - node.statusDisplay.text = `connected ${ioNs.sockets.size}` - uiblib.setNodeStatus( node ) - - // Initial connect message to client - const msgClient = { - 'uibuilderCtrl': 'client connect', - 'serverTimestamp': (new Date()), - 'topic': node.topic || undefined, - 'version': uib.version, // Let the front-end know what v of uib is in use - '_socketId': socket.id, - } - // msgClient.ip = getClientRealIpAddress(socket) - // msgClient.clientId = socket.handshake.auth.clientId - // msgClient.connections = socket.handshake.auth.connectedNum - // msgClient.pageName = socket.handshake.auth.pageName - - // ioNs.clientLog[msg.clientId] = { - // ip: msg.ip, - // connections: msg.connections, - // connected: true, - // } - - // Let the clients know we are connecting - that.sendToFe(msgClient, node.url, uib.ioChannels.control) - - // Send client connect control msg (via port #2) - const ctrlMsg = { - ...{ - uibuilderCtrl: 'client connect', - topic: node.topic || undefined, - from: 'server', - }, - ...that.getClientDetails(socket, node), - } - - that.sendCtrlMsg(ctrlMsg, node) - - // Let other nodes know a client is connecting (via custom event manager) - tiEventManager.emit(`node-red-contrib-uibuilder/${this.url}/clientConnect`, ctrlMsg) - - //#endregion ---- run when client connects ---- // - - }) // --- End of on-connection() --- // - - } // --- End of addNS() --- // - - /** Remove the current clients and namespace for this node. - * Called from uiblib.processClose. - * @param {uibNode} node Reference to the uibuilder node instance - */ - removeNS(node) { - - const ioNs = this.ioNamespaces[node.url] - - // Disconnect all connected sockets for this Namespace (Socket.io v4+) - ioNs.disconnectSockets(true) - - ioNs.removeAllListeners() // Remove all Listeners for the event emitter - - // No longer works from socket.io v3+ //delete this.io.nsps[`/${node.url}`] // Remove from the server namespaces - - } // --- End of removeNS() --- // - -} // ==== End of UibSockets Class Definition ==== // - -/** Singleton model. Only 1 instance of UibSockets should ever exist. - * Use as: `const sockets = require('./libs/socket.js')` - * Wrap in try/catch to force out better error logging if there is a problem - * Downside of this approach is that you cannot directly pass in parameters. Use the startup(...) method instead. - */ - -try { // Wrap in a try in case any errors creep into the class - const uibsockets = new UibSockets() - module.exports = uibsockets -} catch (e) { - console.error(`[uibuilder:socket.js] Unable to create class instance. Error: ${e.message}`) -} - -// EOF +"use strict";const f=require("path"),h=require("fs-extra"),N=require("socket.io"),M=require("./tilib"),C=require("./uiblib"),b=require("@totallyinformation/ti-common-event-handler");function w(d){let e;if("headers"in d.request&&"x-real-ip"in d.request.headers)e=d.request.headers["x-real-ip"];else if("headers"in d.request&&"x-forwarded-for"in d.request.headers){if(d.request.headers["x-forwarded-for"]===void 0)throw new Error('socket.request.headers["x-forwarded-for"] is undefined');Array.isArray(d.request.headers["x-forwarded-for"])||(d.request.headers["x-forwarded-for"]=[d.request.headers["x-forwarded-for"]]),e=d.request.headers["x-forwarded-for"][0].split(",").shift()}else"connection"in d.request&&"remoteAddress"in d.request.connection?e=d.request.connection.remoteAddress:e=d.handshake.address;return e}function S(d,e){let i=d.handshake.auth.pathName.replace(`/${e.url}/`,"");return i.endsWith("/")&&(i+="index.html"),i===""&&(i="index.html"),i}class v{constructor(){this._isConfigured=!1,this.RED=void 0,this.uib=void 0,this.log=void 0,this.server=void 0,this.uib_socketPath=void 0,this.io=void 0,this.ioNamespaces={}}setup(e,i){if(!e||!i)throw new Error("[uibuilder:socket.js:setup] Called without required parameters or uib and/or server are undefined.");if(e.RED===null)throw new Error("[uibuilder:socket.js:setup] uib.RED is null");if(this._isConfigured===!0){e.RED.log.warn("[uibuilder:web:setup] Setup has already been called, it cannot be called again.");return}if(this.RED=e.RED,this.uib=e,this.log=e.RED.log,this.server=i,this._socketIoSetup(),e.configFolder===null)throw new Error("[uibuilder:socket.js:setup] uib.configFolder is null");this.outboundMsgMiddleware=function(n,a,u){return null};const t=f.join(e.configFolder,e.sioMsgOutMwName);if(h.existsSync(t))try{const r=require(t);typeof r=="function"?(this.outboundMsgMiddleware=r,this.log.trace("[uibuilder:socket:setup] sioMsgOut Middleware loaded successfully.")):this.log.warn("[uibuilder:socket:setup] sioMsgOut Middleware failed to load - check that uibRoot/.config/sioMsgOut.js has a valid exported fn.")}catch(r){this.log.warn(`[uibuilder:socket:setup] sioMsgOut middleware Failed to load. Reason: ${r.message}`)}this._isConfigured=!0}_socketIoSetup(){const e=this.uib,i=this.RED,t=this.log,r=this.server;if(e===void 0)throw new Error("uib is undefined");if(i===void 0)throw new Error("RED is undefined");if(t===void 0)throw new Error("log is undefined");const n=this.uib_socketPath=M.urlJoin(e.nodeRoot,e.moduleName,"vendor","socket.io");t.trace(`[uibuilder:socket:socketIoSetup] Socket.IO initialisation - Socket Path=${n}, CORS Origin=*`);let a={path:n,serveClient:!0,connectionStateRecovery:{maxDisconnectionDuration:12e4,skipMiddlewares:!0},cors:{origin:"*"}};i.settings.uibuilder&&i.settings.uibuilder.socketOptions&&(a=Object.assign({},a,i.settings.uibuilder.socketOptions)),this.io=new N.Server(r,a)}get isConfigured(){return this._isConfigured}sendToFe(e,i,t){const r=this.uib,n=this.log;if(r===void 0)throw new Error("uib is undefined");if(n===void 0)throw new Error("log is undefined");t===void 0&&(t=r.ioChannels.client);const a=this.ioNamespaces[i],u=e._socketId||void 0;t===r.ioChannels.control&&!e.from&&(e.from="server");try{this.outboundMsgMiddleware(e,i,t,a)}catch(s){n.warn(`[uibuilder:socket:sendToFe] outboundMsgMiddleware middleware failed to run. Reason: ${s.message}`)}u!==void 0?(n.trace(`[uibuilder:socket.js:sendToFe:${i}] msg sent on to client ${u}. Channel: ${t}. ${JSON.stringify(e)}`),a.to(u).emit(t,e)):(n.trace(`[uibuilder:socket.js:sendToFe:${i}] msg sent on to ALL clients. Channel: ${t}. ${JSON.stringify(e)}`),a.emit(t,e))}sendToFe2(e,i,t){const r=this.uib,n=this.ioNamespaces[i];if(r===void 0)throw new Error("uib is undefined");if(this.log===void 0)throw new Error("this.log is undefined");t&&(e._socketId=t),e._socketId?(this.log.trace(`[uibuilder:socket:sendToFe2:${i}] msg sent on to client ${e._socketId}. Channel: ${r.ioChannels.server}. ${JSON.stringify(e)}`),n.to(e._socketId).emit(r.ioChannels.server,e)):(this.log.trace(`[uibuilder:socket:sendToFe2:${i}] msg sent on to ALL clients. Channel: ${r.ioChannels.server}. ${JSON.stringify(e)}`),n.emit(r.ioChannels.server,e))}sendCtrlMsg(e,i){i.send([null,e])}getClientDetails(e,i){let t;return e.handshake.auth.pathName&&(t=S(e,i)),{_socketId:e.id,version:e.handshake.auth.clientVersion,ip:w(e),clientId:e.handshake.auth.clientId,tabId:e.handshake.auth.tabId,url:i.url,pageName:t,urlParams:e.handshake.auth.urlParams,connections:e.handshake.auth.connectedNum,lastNavType:e.handshake.auth.lastNavType,tls:e.handshake.secure,connectedTimestamp:new Date(e.handshake.issued).toISOString(),referer:e.request.headers.referer,recovered:e.recovered}}getNs(e){return this.ioNamespaces[e]}sendIt(e,i){e._uib&&e._uib.originator&&typeof e._uib.originator=="string"?b.emit(`node-red-contrib-uibuilder/return/${e._uib.originator}`,e):i.send(e)}listenFromClient(e,i,t){const r=this.log;if(r===void 0)throw new Error("log is undefined");switch(t.rcvMsgCount++,r.trace(`[uibuilder:socket:${t.url}] Data received from client, ID: ${i.id}, Msg: ${JSON.stringify(e)}`),typeof e){case"string":case"number":case"boolean":e={topic:t.topic,payload:e}}Object.prototype.hasOwnProperty.call(e,"_socketId")||(e._socketId=i.id),t.showMsgUib&&(e._uib?e._uib={...e._uib,...this.getClientDetails(i,t)}:e._uib=this.getClientDetails(i,t)),this.sendIt(e,t)}addNS(e){const i=this.log,t=this.uib;if(i===void 0)throw new Error("log is undefined");if(t===void 0)throw new Error("uib is undefined");if(this.io===void 0)throw new Error("this.io is undefined");const r=this.ioNamespaces[e.url]=this.io.of(e.url),n=r.url=e.url;if(r.nodeId=e.id,r.rcvMsgCount=0,r.log=i,t.configFolder===null)throw new Error("uib.configFolder is undefined");const a=f.join(t.configFolder,"sioMiddleware.js");if(h.existsSync(a))try{const s=require(a);typeof s=="function"?(r.use(s),i.trace(`[uibuilder:socket:addNs:${n}] Socket.IO sioMiddleware.js middleware loaded successfully for NS.`)):i.warn(`[uibuilder:socket:addNs:${n}] Socket.IO middleware failed to load for NS - check that uibRoot/.config/sioMiddleware.js has a valid exported fn.`)}catch(s){i.warn(`[uibuilder:socket:addNs:${n}] Socket.IO middleware failed to load for NS. Reason: ${s.message}`)}const u=this;r.on("connection",s=>{if(s.on("disconnect",(o,l)=>{e.ioClientsCount=r.sockets.size,i.trace(`[uibuilder:socket:${n}:disconnect] Client disconnected, clientCount: ${r.sockets.size}, Reason: ${o}, ID: ${s.id}, IP Addr: ${w(s)}, Client ID: ${s.handshake.auth.clientId}. For node ${e.id}`),e.statusDisplay.text="connected "+r.sockets.size,C.setNodeStatus(e);const c={uibuilderCtrl:"client disconnect",reason:o,topic:e.topic||void 0,from:"server",description:l,...u.getClientDetails(s,e)};u.sendToFe(c,e.url,t.ioChannels.control),u.sendCtrlMsg(c,e),b.emit(`node-red-contrib-uibuilder/${this.url}/clientDisconnect`,c)}),s.on(t.ioChannels.client,function(o){u.listenFromClient(o,s,e)}),s.on(t.ioChannels.control,function(o){switch(e.rcvMsgCount++,i.trace(`[uibuilder:socket:${n}] Control Msg from client, ID: ${s.id}, Msg: ${JSON.stringify(o)}`),typeof o){case"string":case"number":case"boolean":o={uibuilderCtrl:o}}o={...o,...u.getClientDetails(s,e)},o.from="client",o.topic||(o.topic=e.topic),u.sendCtrlMsg(o,e)}),s.on("error",function(o){i.error(`[uibuilder:socket:addNs:${n}] ERROR received, ID: ${s.id}, Reason: ${o.message}`);const l={uibuilderCtrl:"socket error",error:o.message,from:"server",...u.getClientDetails(s,e)};u.sendCtrlMsg(l,e)}),e.ioClientsCount=r.sockets.size,i.trace(`[uibuilder:socket:addNS:${n}:connect] Client connected. ClientCount: ${r.sockets.size}, Socket ID: ${s.id}, IP Addr: ${w(s)}, Client ID: ${s.handshake.auth.clientId}, Recovered?: ${s.recovered}, Client Version: ${s.handshake.auth.clientVersion}. For node ${e.id}`),t.configFolder===null)throw new Error("uib.configFolder is undefined");const g=f.join(t.configFolder,t.sioUseMwName);if(h.existsSync(g))try{const o=require(g);typeof o=="function"?(s.use(o),i.trace(`[uibuilder:socket:onConnect:${n}] sioUse sioUse.js middleware loaded successfully for NS ${n}.`)):i.warn(`[uibuilder:socket:onConnect:${n}] sioUse middleware failed to load for NS ${n} - check that uibRoot/.config/sioUse.js has a valid exported fn.`)}catch(o){i.warn(`[uibuilder:socket:addNS:${n}] sioUse failed to load Use middleware. Reason: ${o.message}`)}e.statusDisplay.text=`connected ${r.sockets.size}`,C.setNodeStatus(e);const $={uibuilderCtrl:"client connect",serverTimestamp:new Date,topic:e.topic||void 0,version:t.version,_socketId:s.id};u.sendToFe($,e.url,t.ioChannels.control);const p={uibuilderCtrl:"client connect",topic:e.topic||void 0,from:"server",...u.getClientDetails(s,e)};u.sendCtrlMsg(p,e),b.emit(`node-red-contrib-uibuilder/${this.url}/clientConnect`,p)})}removeNS(e){const i=this.ioNamespaces[e.url];i.disconnectSockets(!0),i.removeAllListeners()}}try{const d=new v;module.exports=d}catch(d){console.error(`[uibuilder:socket.js] Unable to create class instance. Error: ${d.message}`)} +//# sourceMappingURL=socket.js.map diff --git a/nodes/libs/socket.js.map b/nodes/libs/socket.js.map new file mode 100644 index 00000000..f41fc0f7 --- /dev/null +++ b/nodes/libs/socket.js.map @@ -0,0 +1,7 @@ +{ + "version": 3, + "sources": ["src/libs/socket.js"], + "sourcesContent": ["/** Manage Socket.IO on behalf of uibuilder\n * Singleton. only 1 instance of this class will ever exist. So it can be used in other modules within Node-RED.\n *\n * Copyright (c) 2017-2023 Julian Knight (Totally Information)\n * https://it.knightnet.org.uk, https://github.com/TotallyInformation/node-red-contrib-uibuilder\n *\n * Licensed under the Apache License, Version 2.0 (the 'License');\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an 'AS IS' BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n/* eslint-disable class-methods-use-this, sonarjs/no-duplicate-string, max-params */\n'use strict'\n\n/** --- Type Defs ---\n * @typedef {import('../../typedefs.js').runtimeRED} runtimeRED\n * @typedef {import('../../typedefs.js').MsgAuth} MsgAuth\n * @typedef {import('../../typedefs.js').uibNode} uibNode\n * @typedef {import('../../typedefs.js').uibConfig} uibConfig\n * @typedef {import('Express')} Express\n */\n\nconst path = require('path')\nconst fs = require('fs-extra')\nconst socketio = require('socket.io')\nconst tilib = require('./tilib') // General purpose library (by Totally Information)\nconst uiblib = require('./uiblib') // Utility library for uibuilder\n// const security = require('./sec-lib') // uibuilder security module\nconst tiEventManager = require('@totallyinformation/ti-common-event-handler')\n\n/** Get client real ip address - NB: Optional chaining (?.) is node.js v14 not v12\n * @param {socketio.Socket} socket Socket.IO socket object\n * @returns {string | string[] | undefined} Best estimate of the client's real IP address\n */\nfunction getClientRealIpAddress(socket) {\n let clientRealIpAddress\n if ( 'headers' in socket.request && 'x-real-ip' in socket.request.headers) {\n // get ip from behind a nginx proxy or proxy using nginx's 'x-real-ip header\n clientRealIpAddress = socket.request.headers['x-real-ip']\n } else if ( 'headers' in socket.request && 'x-forwarded-for' in socket.request.headers) {\n // else get ip from behind a general proxy\n if (socket.request.headers['x-forwarded-for'] === undefined) throw new Error('socket.request.headers[\"x-forwarded-for\"] is undefined')\n if (!Array.isArray(socket.request.headers['x-forwarded-for'])) socket.request.headers['x-forwarded-for'] = [socket.request.headers['x-forwarded-for']]\n clientRealIpAddress = socket.request.headers['x-forwarded-for'][0].split(',').shift()\n } else if ( 'connection' in socket.request && 'remoteAddress' in socket.request.connection ) {\n // else get ip from socket.request that returns the reference to the request that originated the underlying engine.io Client\n clientRealIpAddress = socket.request.connection.remoteAddress\n } else {\n // else get ip from socket.handshake that is a object that contains handshake details\n clientRealIpAddress = socket.handshake.address\n }\n\n // socket.client.conn.remoteAddress\n\n // Switch to this code when node.js v14 becomes the baseline version\n // const clientRealIpAddress =\n // //get ip from behind a nginx proxy or proxy using nginx's 'x-real-ip header\n // socket.request?.headers['x-real-ip']\n // //get ip from behind a general proxy\n // || socket.request?.headers['x-forwarded-for']?.split(',').shift() //if more thatn one x-fowared-for the left-most is the original client. Others after are successive proxys that passed the request adding to the IP addres list all the way back to the first proxy.\n // //get ip from socket.request that returns the reference to the request that originated the underlying engine.io Client\n // || socket.request?.connection?.remoteAddress\n // // get ip from socket.handshake that is a object that contains handshake details\n // || socket.handshake?.address\n\n return clientRealIpAddress\n} // --- End of getClientRealIpAddress --- //\n\n/** Get client real ip address - NB: Optional chaining (?.) is node.js v14 not v12\n * @param {socketio.Socket} socket Socket.IO socket object\n * @param {uibNode} node Reference to the uibuilder node instance\n * @returns {string | string[] | undefined} Best estimate of the client's real IP address\n */\nfunction getClientPageName(socket, node) {\n let pageName = socket.handshake.auth.pathName.replace(`/${node.url}/`, '')\n if ( pageName.endsWith('/') ) pageName += 'index.html'\n if ( pageName === '' ) pageName = 'index.html'\n\n return pageName\n} // --- End of getClientPageName --- //\n\nclass UibSockets {\n // TODO: Replace _XXX with #XXX once node.js v14 is the minimum supported version\n /** Flag to indicate whether setup() has been run\n * @type {boolean}\n * @protected\n */\n // _isConfigured = false\n\n /** Called when class is instantiated */\n constructor() {\n // setup() has not yet been run\n this._isConfigured = false\n\n //#region ---- References to core Node-RED & uibuilder objects ---- //\n /** @type {runtimeRED|undefined} */\n this.RED = undefined\n /** @type {uibConfig|undefined} Reference link to uibuilder.js global configuration object */\n this.uib = undefined\n /** Reference to uibuilder's global log functions */\n this.log = undefined\n /** Reference to ExpressJS server instance being used by uibuilder\n * Used to enable the Socket.IO client code to be served to the front-end\n */\n this.server = undefined\n //#endregion ---- References to core Node-RED & uibuilder objects ---- //\n\n //#region ---- Common variables ---- //\n\n /** URI path for accessing the socket.io client from FE code. Based on the uib node instance URL.\n * @constant {string} uib_socketPath */\n this.uib_socketPath = undefined\n\n /** An instance of Socket.IO Server */\n this.io = undefined\n\n /** Collection of Socket.IO namespaces\n * Each namespace correstponds to a uibuilder node instance and must have a unique namespace name that matches the unique URL parameter for the node.\n * The namespace is stored in the this.ioNamespaces object against a property name matching the URL so that it can be referenced later.\n * Because this is a Singleton object, any reference to this module can access all of the namespaces (by url).\n * The namespace has some uib extensions that track the originating node id (searchable in Node-RED), the number of connected clients\n * and the number of messages recieved.\n * @type {!Object}\n */\n this.ioNamespaces = {}\n\n //#endregion ---- ---- //\n\n } // --- End of constructor() --- //\n\n /** Assign uibuilder and Node-RED core vars to Class static vars.\n * This makes them available wherever this MODULE is require'd.\n * Because JS passess objects by REFERENCE, updates to the original\n * variables means that these are updated as well.\n * @param {uibConfig} uib reference to uibuilder 'global' configuration object\n * @param {Express} server reference to ExpressJS server being used by uibuilder\n */\n setup( uib, server ) {\n if ( !uib || !server ) throw new Error('[uibuilder:socket.js:setup] Called without required parameters or uib and/or server are undefined.')\n if (uib.RED === null) throw new Error('[uibuilder:socket.js:setup] uib.RED is null')\n\n // Prevent setup from being called more than once\n if ( this._isConfigured === true ) {\n uib.RED.log.warn('[uibuilder:web:setup] Setup has already been called, it cannot be called again.')\n return\n }\n\n /** reference to Core Node-RED runtime object */\n this.RED = uib.RED\n\n this.uib = uib\n this.log = uib.RED.log\n this.server = server\n\n // TODO: Replace _XXX with #XXX once node.js v14 is the minimum supported version\n this._socketIoSetup()\n\n if (uib.configFolder === null) throw new Error('[uibuilder:socket.js:setup] uib.configFolder is null')\n\n // If available, set up optional outbound msg middleware\n this.outboundMsgMiddleware = function outboundMsgMiddleware( msg, url, channel ) { return null }\n // Try to load the sioMsgOut middleware function - sioMsgOut applies to all outgoing msgs\n const mwfile = path.join(uib.configFolder, uib.sioMsgOutMwName)\n if ( fs.existsSync(mwfile) ) { // not interested if the file doesn't exist\n try {\n const sioMsgOut = require( mwfile )\n if ( typeof sioMsgOut === 'function' ) { // if exported, has to be a function\n this.outboundMsgMiddleware = sioMsgOut\n this.log.trace('[uibuilder:socket:setup] sioMsgOut Middleware loaded successfully.')\n } else {\n this.log.warn('[uibuilder:socket:setup] sioMsgOut Middleware failed to load - check that uibRoot/.config/sioMsgOut.js has a valid exported fn.')\n }\n } catch (e) {\n this.log.warn(`[uibuilder:socket:setup] sioMsgOut middleware Failed to load. Reason: ${e.message}`)\n }\n }\n\n this._isConfigured = true\n\n } // --- End of setup() --- //\n\n /** Holder for Socket.IO - we want this to survive redeployments of each node instance\n * so that existing clients can be reconnected.\n * Start Socket.IO - make sure the right version of SIO is used so keeping this separate from other\n * modules that might also use it (path). This is only needed ONCE for ALL uib.instances of this node.\n * Must only be run once and so is made an ECMA2018 private class method\n * @private\n */\n _socketIoSetup() {\n // Reference static vars\n const uib = this.uib\n const RED = this.RED\n const log = this.log\n const server = this.server\n\n if (uib === undefined) throw new Error('uib is undefined')\n if (RED === undefined) throw new Error('RED is undefined')\n if (log === undefined) throw new Error('log is undefined')\n\n const uibSocketPath = this.uib_socketPath = tilib.urlJoin(uib.nodeRoot, uib.moduleName, 'vendor', 'socket.io')\n\n log.trace(`[uibuilder:socket:socketIoSetup] Socket.IO initialisation - Socket Path=${uibSocketPath}, CORS Origin=*` )\n // Socket.Io server options, see https://socket.io/docs/v4/server-options/\n let ioOptions = {\n 'path': uibSocketPath,\n serveClient: true, // Needed for backwards compatibility\n connectionStateRecovery: {\n // the backup duration of the sessions and the packets\n maxDisconnectionDuration: 120000, // Default = 2 * 60 * 1000 = 120000,\n // whether to skip middlewares upon successful recovery\n skipMiddlewares: true, // Default = true\n },\n // https://github.com/expressjs/cors#configuration-options, https://socket.io/docs/v3/handling-cors/\n cors: {\n origin: '*',\n // allowedHeaders: ['x-clientid'],\n },\n /* // Socket.Io 3+ CORS is disabled by default, also options have changed.\n // for CORS need to handle preflight request explicitly 'cause there's an\n // Allow-Headers:X-ClientId in there. see https://socket.io/docs/v4/handling-cors/\n handlePreflightRequest: (req, res) => {\n res.writeHead(204, {\n 'Access-Control-Allow-Origin': req.headers['origin'], // eslint-disable-line dot-notation\n 'Access-Control-Allow-Methods': 'GET,POST',\n 'Access-Control-Allow-Headers': 'X-ClientId',\n 'Access-Control-Allow-Credentials': true,\n })\n res.end()\n }, */\n }\n\n // Merge in overrides from settings.js if given. NB: settings.uibuilder.socketOptions will override the above defaults.\n if ( RED.settings.uibuilder && RED.settings.uibuilder.socketOptions ) {\n ioOptions = Object.assign( {}, ioOptions, RED.settings.uibuilder.socketOptions )\n }\n\n // @ts-ignore ts(2769)\n this.io = new socketio.Server(server, ioOptions) // listen === attach\n\n } // --- End of socketIoSetup() --- //\n\n /** Allow the isConfigured flag to be read (not written) externally\n * @returns {boolean} True if this class as been configured\n */\n get isConfigured() {\n return this._isConfigured\n }\n\n // ? Consider adding isConfigered checks on each method?\n\n /** Output a msg to the front-end.\n * @param {object} msg The message to output, include msg._socketId to send to a single client\n * @param {string} url THe uibuilder id\n * @param {string=} channel Optional. Which channel to send to (see uib.ioChannels) - defaults to client\n */\n sendToFe( msg, url, channel ) {\n const uib = this.uib\n const log = this.log\n\n if (uib === undefined) throw new Error('uib is undefined')\n if (log === undefined) throw new Error('log is undefined')\n\n if ( channel === undefined ) channel = uib.ioChannels.client\n\n const ioNs = this.ioNamespaces[url]\n\n const socketId = msg._socketId || undefined\n\n // Control msgs should say where they came from\n if ( channel === uib.ioChannels.control && !msg.from ) msg.from = 'server'\n\n // Process outbound middleware (middleware is loaded in this.setup)\n try {\n this.outboundMsgMiddleware( msg, url, channel, ioNs )\n } catch (e) {\n log.warn(`[uibuilder:socket:sendToFe] outboundMsgMiddleware middleware failed to run. Reason: ${e.message}`)\n }\n\n // TODO: Sending should have some safety validation on it. Is msg an object? Is channel valid?\n\n // pass the complete msg object to the uibuilder client\n if (socketId !== undefined) { // Send to specific client\n log.trace(`[uibuilder:socket.js:sendToFe:${url}] msg sent on to client ${socketId}. Channel: ${channel}. ${JSON.stringify(msg)}`)\n ioNs.to(socketId).emit(channel, msg)\n } else { // Broadcast\n log.trace(`[uibuilder:socket.js:sendToFe:${url}] msg sent on to ALL clients. Channel: ${channel}. ${JSON.stringify(msg)}`)\n ioNs.emit(channel, msg)\n }\n\n } // ---- End of sendToFe ---- //\n\n /** Output a normal msg to the front-end. Can override socketid\n * Currently only used for the auto-reload on edit in admin-api-v2.js\n * @param {object} msg The message to output\n * @param {object} url The uibuilder instance url - will be unique. Used to lookup the correct Socket.IO namespace for sending.\n * @param {string=} socketId Optional. If included, only send to specific client id (mostly expecting this to be on msg._socketID so not often required)\n */\n sendToFe2(msg, url, socketId) { // eslint-disable-line class-methods-use-this\n const uib = this.uib\n const ioNs = this.ioNamespaces[url]\n\n if (uib === undefined) throw new Error('uib is undefined')\n if (this.log === undefined) throw new Error('this.log is undefined')\n\n if (socketId) msg._socketId = socketId\n\n // TODO: This should have some safety validation on it\n if (msg._socketId) {\n this.log.trace(`[uibuilder:socket:sendToFe2:${url}] msg sent on to client ${msg._socketId}. Channel: ${uib.ioChannels.server}. ${JSON.stringify(msg)}`)\n ioNs.to(msg._socketId).emit(uib.ioChannels.server, msg)\n } else {\n this.log.trace(`[uibuilder:socket:sendToFe2:${url}] msg sent on to ALL clients. Channel: ${uib.ioChannels.server}. ${JSON.stringify(msg)}`)\n ioNs.emit(uib.ioChannels.server, msg)\n }\n } // ---- End of sendToFe2 ---- //\n\n /** Send a uibuilder control message out of port #2\n * Note: this.getClientDetails is used before calling this if client details needed\n * @param {object} msg The message to output\n * @param {uibNode} node Reference to the uibuilder node instance\n */\n sendCtrlMsg(msg, node) {\n node.send( [null, msg])\n }\n\n /** Get client details for including in Node-RED messages\n * @param {socketio.Socket} socket Reference to client socket connection\n * @param {uibNode} node Reference to the uibuilder node instance\n * @returns {object} Extracted key information\n */\n getClientDetails(socket, node) {\n\n // Add page name meta to allow caches and other flows to send back to specific page\n // Note, could use socket.handshake.auth.pageName instead\n let pageName\n if ( socket.handshake.auth.pathName ) {\n pageName = getClientPageName(socket, node)\n }\n\n return {\n '_socketId': socket.id,\n // Let the flow know what v of uib client is in use\n 'version': socket.handshake.auth.clientVersion,\n /** Do our best to get the actual IP addr of client despite any Proxies */\n 'ip': getClientRealIpAddress(socket),\n /** What is the stable client id (set by uibuilder, retained till browser restart) */\n 'clientId': socket.handshake.auth.clientId,\n /** What is the client tab identifier (set by uibuilder modern client) */\n 'tabId': socket.handshake.auth.tabId,\n /** What was the originating uibuilder URL */\n 'url': node.url,\n /** What was the originating page name (for SPA's) */\n 'pageName': pageName,\n /** The browser's URL parameters */\n 'urlParams': socket.handshake.auth.urlParams,\n /** How many times has this client reconnected (e.g. after sleep) */\n 'connections': socket.handshake.auth.connectedNum,\n /** What type of client nav happened previously */\n 'lastNavType': socket.handshake.auth.lastNavType,\n /** True if https/wss */\n 'tls': socket.handshake.secure,\n /** When the client connected to the server */\n 'connectedTimestamp': (new Date(socket.handshake.issued)).toISOString(),\n /** THe referring webpage, should be the full URL of the uibuilder page */\n 'referer': socket.request.headers.referer,\n /** Is this client reconnected after temp loss? */\n 'recovered': socket.recovered,\n\n // ? client time offset ?\n }\n }\n\n /** Get a uib node instance namespace\n * @param {string} url The uibuilder node instance's url (identifier)\n * @returns {socketio.Namespace} Return a reference to the namespace of the specified uib instance for convenience in core code\n */\n getNs(url) {\n return this.ioNamespaces[url]\n }\n\n /** Send a node-red msg either directly out of the node instance OR via return event name\n * @param {object} msg Message object received from a client\n * @param {uibNode} node Reference to the uibuilder node instance\n */\n sendIt(msg, node) {\n if ( msg._uib && msg._uib.originator && (typeof msg._uib.originator === 'string') ) {\n // const eventName = `node-red-contrib-uibuilder/return/${msg._uib.originator}`\n tiEventManager.emit(`node-red-contrib-uibuilder/return/${msg._uib.originator}`, msg)\n } else {\n node.send(msg)\n }\n }\n\n /** Socket listener fn for msgs from clients - NOTE that the optional sioUse middleware is also applied before this\n * @param {object} msg Message object received from a client\n * @param {socketio.Socket} socket Reference to the socket for this node\n * @param {uibNode} node Reference to the uibuilder node instance\n */\n listenFromClient(msg, socket, node) {\n const log = this.log\n if (log === undefined) throw new Error('log is undefined')\n\n node.rcvMsgCount++\n log.trace(`[uibuilder:socket:${node.url}] Data received from client, ID: ${socket.id}, Msg: ${JSON.stringify(msg)}`)\n\n // Make sure the incoming msg is a correctly formed Node-RED msg\n switch ( typeof msg ) {\n case 'string':\n case 'number':\n case 'boolean':\n msg = { 'topic': node.topic, 'payload': msg }\n }\n\n // If the sender hasn't added msg._socketId, add the Socket.id now\n if ( !Object.prototype.hasOwnProperty.call(msg, '_socketId') ) msg._socketId = socket.id\n\n // If required, add/merge the client details to the msg using msg._uib\n if (node.showMsgUib) {\n if (!msg._uib) msg._uib = this.getClientDetails(socket, node)\n else {\n msg._uib = {\n ...msg._uib,\n ...this.getClientDetails(socket, node)\n }\n }\n }\n\n // Send out the message for downstream flows\n // TODO: This should probably have safety validations!\n this.sendIt(msg, node)\n\n } // ---- End of listenFromClient ---- //\n\n /** Add a new Socket.IO NAMESPACE\n * Each namespace correstponds to a uibuilder node instance and must have a unique namespace name that matches the unique URL parameter for the node.\n * The namespace is stored in the this.ioNamespaces object against a property name matching the URL so that it can be referenced later.\n * Because this is a Singleton object, any reference to this module can access all of the namespaces (by url).\n * The namespace has some uib extensions that track the originating node id (searchable in Node-RED), the number of connected clients\n * and the number of messages received.\n * @param {uibNode} node Reference to the uibuilder node instance\n */\n addNS(node) {\n const log = this.log\n const uib = this.uib\n\n if (log === undefined) throw new Error('log is undefined')\n if (uib === undefined) throw new Error('uib is undefined')\n if (this.io === undefined) throw new Error('this.io is undefined')\n\n const ioNs = this.ioNamespaces[node.url] = this.io.of(node.url)\n\n // @ts-expect-error Add some additional metadata to NS\n const url = ioNs.url = node.url\n // @ts-expect-error Allows us to track back to the actual node in Node-RED\n ioNs.nodeId = node.id\n // @ts-expect-error ioNs.useSecurity = node.useSecurity // Is security on for this node instance?\n ioNs.rcvMsgCount = 0\n // @ts-expect-error Make Node-RED's log available to middleware via custom ns property\n ioNs.log = log\n // ioNs.clientLog = {}\n\n if (uib.configFolder === null) throw new Error('uib.configFolder is undefined')\n\n /** Check for /.config/sioMiddleware.js, use it if present.\n * Applies ONCE on a new client connection.\n * Had to move to addNS since MW no longer globally loadable since sio v3\n */\n const sioMwPath = path.join(uib.configFolder, 'sioMiddleware.js')\n if ( fs.existsSync(sioMwPath) ) { // not interested if the file doesn't exist\n try {\n const sioMiddleware = require(sioMwPath)\n if ( typeof sioMiddleware === 'function' ) {\n ioNs.use(sioMiddleware)\n log.trace(`[uibuilder:socket:addNs:${url}] Socket.IO sioMiddleware.js middleware loaded successfully for NS.`)\n } else {\n log.warn(`[uibuilder:socket:addNs:${url}] Socket.IO middleware failed to load for NS - check that uibRoot/.config/sioMiddleware.js has a valid exported fn.`)\n }\n } catch (e) {\n log.warn(`[uibuilder:socket:addNs:${url}] Socket.IO middleware failed to load for NS. Reason: ${e.message}`)\n }\n }\n\n const that = this\n\n ioNs.on('connection', (socket) => {\n\n //#region ----- Event Handlers ----- //\n\n // NOTE: as of sio v4, disconnect seems to be fired AFTER a connect when a client reconnects\n socket.on('disconnect', (reason, description) => {\n\n // ioNs.clientLog[socket.handshake.auth.clientId].connected = false\n\n node.ioClientsCount = ioNs.sockets.size\n log.trace(\n `[uibuilder:socket:${url}:disconnect] Client disconnected, clientCount: ${ioNs.sockets.size}, Reason: ${reason}, ID: ${socket.id}, IP Addr: ${getClientRealIpAddress(socket)}, Client ID: ${socket.handshake.auth.clientId}. For node ${node.id}`\n )\n node.statusDisplay.text = 'connected ' + ioNs.sockets.size\n uiblib.setNodeStatus( node )\n\n // Let the control output port know a client has disconnected\n const ctrlMsg = {\n ...{\n 'uibuilderCtrl': 'client disconnect',\n 'reason': reason,\n 'topic': node.topic || undefined,\n 'from': 'server',\n 'description': description,\n },\n ...that.getClientDetails(socket, node),\n }\n\n that.sendToFe(ctrlMsg, node.url, uib.ioChannels.control)\n\n // Copy to port#2 for reference\n that.sendCtrlMsg(ctrlMsg, node)\n\n // Let other nodes know a client is disconnecting (via custom event manager)\n tiEventManager.emit(`node-red-contrib-uibuilder/${this.url}/clientDisconnect`, ctrlMsg)\n\n }) // --- End of on-connection::on-disconnect() --- //\n\n // Listen for msgs from clients on standard channel\n socket.on(uib.ioChannels.client, function(msg) {\n that.listenFromClient(msg, socket, node )\n }) // --- End of on-connection::on-incoming-client-msg() --- //\n\n // Listen for msgs from clients on control channel\n socket.on(uib.ioChannels.control, function(msg) {\n node.rcvMsgCount++\n log.trace(`[uibuilder:socket:${url}] Control Msg from client, ID: ${socket.id}, Msg: ${JSON.stringify(msg)}`)\n\n // Make sure the incoming msg is a correctly formed Node-RED msg\n switch ( typeof msg ) {\n case 'string':\n case 'number':\n case 'boolean':\n msg = { 'uibuilderCtrl': msg }\n }\n\n // Apply standard client details to the control msg\n msg = { ...msg, ...that.getClientDetails(socket, node) }\n\n // Control msgs should say where they came from\n msg.from = 'client'\n\n if ( !msg.topic ) msg.topic = node.topic\n\n that.sendCtrlMsg(msg, node)\n\n }) // --- End of on-connection::on-incoming-control-msg() --- //\n\n // Listen for socket.io errors - output a control msg\n socket.on('error', function(err) {\n\n log.error(`[uibuilder:socket:addNs:${url}] ERROR received, ID: ${socket.id}, Reason: ${err.message}`)\n\n // Let the control output port (port #2) know there has been an error\n const ctrlMsg = {\n ...{\n uibuilderCtrl: 'socket error',\n error: err.message,\n from: 'server',\n },\n ...that.getClientDetails(socket, node),\n }\n\n that.sendCtrlMsg(ctrlMsg, node)\n\n }) // --- End of on-connection::on-error() --- //\n\n //#endregion ----- Event Handlers ----- //\n\n //#region ---- run when client connects ---- //\n\n // How many client connections are there?\n node.ioClientsCount = ioNs.sockets.size\n\n log.trace(\n `[uibuilder:socket:addNS:${url}:connect] Client connected. ClientCount: ${ioNs.sockets.size}, Socket ID: ${socket.id}, IP Addr: ${getClientRealIpAddress(socket)}, Client ID: ${socket.handshake.auth.clientId}, Recovered?: ${socket.recovered}, Client Version: ${socket.handshake.auth.clientVersion}. For node ${node.id}`\n )\n\n if (uib.configFolder === null) throw new Error('uib.configFolder is undefined')\n\n // Try to load the sioUse middleware function - sioUse applies to all incoming msgs\n const mwfile = path.join(uib.configFolder, uib.sioUseMwName)\n if ( fs.existsSync(mwfile) ) { // not interested if the file doesn't exist\n try {\n const sioUseMw = require( mwfile )\n if ( typeof sioUseMw === 'function' ) { // if exported, has to be a function\n socket.use(sioUseMw)\n log.trace(`[uibuilder:socket:onConnect:${url}] sioUse sioUse.js middleware loaded successfully for NS ${url}.`)\n } else {\n log.warn(`[uibuilder:socket:onConnect:${url}] sioUse middleware failed to load for NS ${url} - check that uibRoot/.config/sioUse.js has a valid exported fn.`)\n }\n } catch (e) {\n log.warn(`[uibuilder:socket:addNS:${url}] sioUse failed to load Use middleware. Reason: ${e.message}`)\n }\n }\n\n node.statusDisplay.text = `connected ${ioNs.sockets.size}`\n uiblib.setNodeStatus( node )\n\n // Initial connect message to client\n const msgClient = {\n 'uibuilderCtrl': 'client connect',\n 'serverTimestamp': (new Date()),\n 'topic': node.topic || undefined,\n 'version': uib.version, // Let the front-end know what v of uib is in use\n '_socketId': socket.id,\n }\n // msgClient.ip = getClientRealIpAddress(socket)\n // msgClient.clientId = socket.handshake.auth.clientId\n // msgClient.connections = socket.handshake.auth.connectedNum\n // msgClient.pageName = socket.handshake.auth.pageName\n\n // ioNs.clientLog[msg.clientId] = {\n // ip: msg.ip,\n // connections: msg.connections,\n // connected: true,\n // }\n\n // Let the clients know we are connecting\n that.sendToFe(msgClient, node.url, uib.ioChannels.control)\n\n // Send client connect control msg (via port #2)\n const ctrlMsg = {\n ...{\n uibuilderCtrl: 'client connect',\n topic: node.topic || undefined,\n from: 'server',\n },\n ...that.getClientDetails(socket, node),\n }\n\n that.sendCtrlMsg(ctrlMsg, node)\n\n // Let other nodes know a client is connecting (via custom event manager)\n tiEventManager.emit(`node-red-contrib-uibuilder/${this.url}/clientConnect`, ctrlMsg)\n\n //#endregion ---- run when client connects ---- //\n\n }) // --- End of on-connection() --- //\n\n } // --- End of addNS() --- //\n\n /** Remove the current clients and namespace for this node.\n * Called from uiblib.processClose.\n * @param {uibNode} node Reference to the uibuilder node instance\n */\n removeNS(node) {\n\n const ioNs = this.ioNamespaces[node.url]\n\n // Disconnect all connected sockets for this Namespace (Socket.io v4+)\n ioNs.disconnectSockets(true)\n\n ioNs.removeAllListeners() // Remove all Listeners for the event emitter\n\n // No longer works from socket.io v3+ //delete this.io.nsps[`/${node.url}`] // Remove from the server namespaces\n\n } // --- End of removeNS() --- //\n\n} // ==== End of UibSockets Class Definition ==== //\n\n/** Singleton model. Only 1 instance of UibSockets should ever exist.\n * Use as: `const sockets = require('./libs/socket.js')`\n * Wrap in try/catch to force out better error logging if there is a problem\n * Downside of this approach is that you cannot directly pass in parameters. Use the startup(...) method instead.\n */\n\ntry { // Wrap in a try in case any errors creep into the class\n const uibsockets = new UibSockets()\n module.exports = uibsockets\n} catch (e) {\n console.error(`[uibuilder:socket.js] Unable to create class instance. Error: ${e.message}`)\n}\n\n// EOF\n"], + "mappings": "aA6BA,MAAMA,EAAW,QAAQ,MAAM,EACzBC,EAAK,QAAQ,UAAU,EACvBC,EAAW,QAAQ,WAAW,EAC9BC,EAAW,QAAQ,SAAS,EAC5BC,EAAW,QAAQ,UAAU,EAE7BC,EAAiB,QAAQ,6CAA6C,EAM5E,SAASC,EAAuBC,EAAQ,CACpC,IAAIC,EACJ,GAAK,YAAaD,EAAO,SAAW,cAAeA,EAAO,QAAQ,QAE9DC,EAAsBD,EAAO,QAAQ,QAAQ,WAAW,UAChD,YAAaA,EAAO,SAAW,oBAAqBA,EAAO,QAAQ,QAAS,CAEpF,GAAIA,EAAO,QAAQ,QAAQ,iBAAiB,IAAM,OAAW,MAAM,IAAI,MAAM,wDAAwD,EAChI,MAAM,QAAQA,EAAO,QAAQ,QAAQ,iBAAiB,CAAC,IAAGA,EAAO,QAAQ,QAAQ,iBAAiB,EAAI,CAACA,EAAO,QAAQ,QAAQ,iBAAiB,CAAC,GACrJC,EAAsBD,EAAO,QAAQ,QAAQ,iBAAiB,EAAE,CAAC,EAAE,MAAM,GAAG,EAAE,MAAM,CACxF,KAAY,eAAgBA,EAAO,SAAW,kBAAmBA,EAAO,QAAQ,WAE5EC,EAAsBD,EAAO,QAAQ,WAAW,cAGhDC,EAAsBD,EAAO,UAAU,QAgB3C,OAAOC,CACX,CAOA,SAASC,EAAkBF,EAAQG,EAAM,CACrC,IAAIC,EAAWJ,EAAO,UAAU,KAAK,SAAS,QAAQ,IAAIG,EAAK,GAAG,IAAK,EAAE,EACzE,OAAKC,EAAS,SAAS,GAAG,IAAIA,GAAY,cACrCA,IAAa,KAAKA,EAAW,cAE3BA,CACX,CAEA,MAAMC,CAAW,CASb,aAAc,CAEV,KAAK,cAAgB,GAIrB,KAAK,IAAM,OAEX,KAAK,IAAM,OAEX,KAAK,IAAM,OAIX,KAAK,OAAS,OAOd,KAAK,eAAiB,OAGtB,KAAK,GAAK,OAUV,KAAK,aAAe,CAAC,CAIzB,CASA,MAAOC,EAAKC,EAAS,CACjB,GAAK,CAACD,GAAO,CAACC,EAAS,MAAM,IAAI,MAAM,oGAAoG,EAC3I,GAAID,EAAI,MAAQ,KAAM,MAAM,IAAI,MAAM,6CAA6C,EAGnF,GAAK,KAAK,gBAAkB,GAAO,CAC/BA,EAAI,IAAI,IAAI,KAAK,iFAAiF,EAClG,MACJ,CAYA,GATA,KAAK,IAAMA,EAAI,IAEf,KAAK,IAAMA,EACX,KAAK,IAAMA,EAAI,IAAI,IACnB,KAAK,OAASC,EAGd,KAAK,eAAe,EAEhBD,EAAI,eAAiB,KAAM,MAAM,IAAI,MAAM,sDAAsD,EAGrG,KAAK,sBAAwB,SAAgCE,EAAKC,EAAKC,EAAU,CAAE,OAAO,IAAK,EAE/F,MAAMC,EAASlB,EAAK,KAAKa,EAAI,aAAcA,EAAI,eAAe,EAC9D,GAAKZ,EAAG,WAAWiB,CAAM,EACrB,GAAI,CACA,MAAMC,EAAY,QAASD,CAAO,EAC7B,OAAOC,GAAc,YACtB,KAAK,sBAAwBA,EAC7B,KAAK,IAAI,MAAM,oEAAoE,GAEnF,KAAK,IAAI,KAAK,iIAAiI,CAEvJ,OAASC,EAAG,CACR,KAAK,IAAI,KAAK,yEAAyEA,EAAE,OAAO,EAAE,CACtG,CAGJ,KAAK,cAAgB,EAEzB,CASA,gBAAiB,CAEb,MAAMP,EAAM,KAAK,IACXQ,EAAM,KAAK,IACXC,EAAM,KAAK,IACXR,EAAS,KAAK,OAEpB,GAAID,IAAQ,OAAW,MAAM,IAAI,MAAM,kBAAkB,EACzD,GAAIQ,IAAQ,OAAW,MAAM,IAAI,MAAM,kBAAkB,EACzD,GAAIC,IAAQ,OAAW,MAAM,IAAI,MAAM,kBAAkB,EAEzD,MAAMC,EAAgB,KAAK,eAAiBpB,EAAM,QAAQU,EAAI,SAAUA,EAAI,WAAY,SAAU,WAAW,EAE7GS,EAAI,MAAM,2EAA2EC,CAAa,iBAAkB,EAEpH,IAAIC,EAAY,CACZ,KAAQD,EACR,YAAa,GACb,wBAAyB,CAErB,yBAA0B,KAE1B,gBAAiB,EACrB,EAEA,KAAM,CACF,OAAQ,GAEZ,CAaJ,EAGKF,EAAI,SAAS,WAAaA,EAAI,SAAS,UAAU,gBAClDG,EAAY,OAAO,OAAQ,CAAC,EAAGA,EAAWH,EAAI,SAAS,UAAU,aAAc,GAInF,KAAK,GAAK,IAAInB,EAAS,OAAOY,EAAQU,CAAS,CAEnD,CAKA,IAAI,cAAe,CACf,OAAO,KAAK,aAChB,CASA,SAAUT,EAAKC,EAAKC,EAAU,CAC1B,MAAMJ,EAAM,KAAK,IACXS,EAAM,KAAK,IAEjB,GAAIT,IAAQ,OAAW,MAAM,IAAI,MAAM,kBAAkB,EACzD,GAAIS,IAAQ,OAAW,MAAM,IAAI,MAAM,kBAAkB,EAEpDL,IAAY,SAAYA,EAAUJ,EAAI,WAAW,QAEtD,MAAMY,EAAO,KAAK,aAAaT,CAAG,EAE5BU,EAAWX,EAAI,WAAa,OAG7BE,IAAYJ,EAAI,WAAW,SAAW,CAACE,EAAI,OAAOA,EAAI,KAAO,UAGlE,GAAI,CACA,KAAK,sBAAuBA,EAAKC,EAAKC,EAASQ,CAAK,CACxD,OAASL,EAAG,CACRE,EAAI,KAAK,uFAAuFF,EAAE,OAAO,EAAE,CAC/G,CAKIM,IAAa,QACbJ,EAAI,MAAM,iCAAiCN,CAAG,2BAA2BU,CAAQ,cAAcT,CAAO,KAAK,KAAK,UAAUF,CAAG,CAAC,EAAE,EAChIU,EAAK,GAAGC,CAAQ,EAAE,KAAKT,EAASF,CAAG,IAEnCO,EAAI,MAAM,iCAAiCN,CAAG,0CAA0CC,CAAO,KAAK,KAAK,UAAUF,CAAG,CAAC,EAAE,EACzHU,EAAK,KAAKR,EAASF,CAAG,EAG9B,CAQA,UAAUA,EAAKC,EAAKU,EAAU,CAC1B,MAAMb,EAAM,KAAK,IACXY,EAAO,KAAK,aAAaT,CAAG,EAElC,GAAIH,IAAQ,OAAW,MAAM,IAAI,MAAM,kBAAkB,EACzD,GAAI,KAAK,MAAQ,OAAW,MAAM,IAAI,MAAM,uBAAuB,EAE/Da,IAAUX,EAAI,UAAYW,GAG1BX,EAAI,WACJ,KAAK,IAAI,MAAM,+BAA+BC,CAAG,2BAA2BD,EAAI,SAAS,cAAcF,EAAI,WAAW,MAAM,KAAK,KAAK,UAAUE,CAAG,CAAC,EAAE,EACtJU,EAAK,GAAGV,EAAI,SAAS,EAAE,KAAKF,EAAI,WAAW,OAAQE,CAAG,IAEtD,KAAK,IAAI,MAAM,+BAA+BC,CAAG,0CAA0CH,EAAI,WAAW,MAAM,KAAK,KAAK,UAAUE,CAAG,CAAC,EAAE,EAC1IU,EAAK,KAAKZ,EAAI,WAAW,OAAQE,CAAG,EAE5C,CAOA,YAAYA,EAAKL,EAAM,CACnBA,EAAK,KAAM,CAAC,KAAMK,CAAG,CAAC,CAC1B,CAOA,iBAAiBR,EAAQG,EAAM,CAI3B,IAAIC,EACJ,OAAKJ,EAAO,UAAU,KAAK,WACvBI,EAAWF,EAAkBF,EAAQG,CAAI,GAGtC,CACH,UAAaH,EAAO,GAEpB,QAAWA,EAAO,UAAU,KAAK,cAEjC,GAAMD,EAAuBC,CAAM,EAEnC,SAAYA,EAAO,UAAU,KAAK,SAElC,MAASA,EAAO,UAAU,KAAK,MAE/B,IAAOG,EAAK,IAEZ,SAAYC,EAEZ,UAAaJ,EAAO,UAAU,KAAK,UAEnC,YAAeA,EAAO,UAAU,KAAK,aAErC,YAAeA,EAAO,UAAU,KAAK,YAErC,IAAOA,EAAO,UAAU,OAExB,mBAAuB,IAAI,KAAKA,EAAO,UAAU,MAAM,EAAG,YAAY,EAEtE,QAAWA,EAAO,QAAQ,QAAQ,QAElC,UAAaA,EAAO,SAGxB,CACJ,CAMA,MAAMS,EAAK,CACP,OAAO,KAAK,aAAaA,CAAG,CAChC,CAMA,OAAOD,EAAKL,EAAM,CACTK,EAAI,MAAQA,EAAI,KAAK,YAAe,OAAOA,EAAI,KAAK,YAAe,SAEpEV,EAAe,KAAK,qCAAqCU,EAAI,KAAK,UAAU,GAAIA,CAAG,EAEnFL,EAAK,KAAKK,CAAG,CAErB,CAOA,iBAAiBA,EAAKR,EAAQG,EAAM,CAChC,MAAMY,EAAM,KAAK,IACjB,GAAIA,IAAQ,OAAW,MAAM,IAAI,MAAM,kBAAkB,EAMzD,OAJAZ,EAAK,cACLY,EAAI,MAAM,qBAAqBZ,EAAK,GAAG,oCAAoCH,EAAO,EAAE,UAAU,KAAK,UAAUQ,CAAG,CAAC,EAAE,EAG1G,OAAOA,EAAM,CAClB,IAAK,SACL,IAAK,SACL,IAAK,UACDA,EAAM,CAAE,MAASL,EAAK,MAAO,QAAWK,CAAI,CACpD,CAGM,OAAO,UAAU,eAAe,KAAKA,EAAK,WAAW,IAAIA,EAAI,UAAYR,EAAO,IAGlFG,EAAK,aACAK,EAAI,KAELA,EAAI,KAAO,CACP,GAAGA,EAAI,KACP,GAAG,KAAK,iBAAiBR,EAAQG,CAAI,CACzC,EALWK,EAAI,KAAO,KAAK,iBAAiBR,EAAQG,CAAI,GAWhE,KAAK,OAAOK,EAAKL,CAAI,CAEzB,CAUA,MAAMA,EAAM,CACR,MAAMY,EAAM,KAAK,IACXT,EAAM,KAAK,IAEjB,GAAIS,IAAQ,OAAW,MAAM,IAAI,MAAM,kBAAkB,EACzD,GAAIT,IAAQ,OAAW,MAAM,IAAI,MAAM,kBAAkB,EACzD,GAAI,KAAK,KAAO,OAAW,MAAM,IAAI,MAAM,sBAAsB,EAEjE,MAAMY,EAAO,KAAK,aAAaf,EAAK,GAAG,EAAI,KAAK,GAAG,GAAGA,EAAK,GAAG,EAGxDM,EAAMS,EAAK,IAAMf,EAAK,IAS5B,GAPAe,EAAK,OAASf,EAAK,GAEnBe,EAAK,YAAc,EAEnBA,EAAK,IAAMH,EAGPT,EAAI,eAAiB,KAAM,MAAM,IAAI,MAAM,+BAA+B,EAM9E,MAAMc,EAAY3B,EAAK,KAAKa,EAAI,aAAc,kBAAkB,EAChE,GAAKZ,EAAG,WAAW0B,CAAS,EACxB,GAAI,CACA,MAAMC,EAAgB,QAAQD,CAAS,EAClC,OAAOC,GAAkB,YAC1BH,EAAK,IAAIG,CAAa,EACtBN,EAAI,MAAM,2BAA2BN,CAAG,qEAAqE,GAE7GM,EAAI,KAAK,2BAA2BN,CAAG,qHAAqH,CAEpK,OAASI,EAAG,CACRE,EAAI,KAAK,2BAA2BN,CAAG,yDAAyDI,EAAE,OAAO,EAAE,CAC/G,CAGJ,MAAMS,EAAO,KAEbJ,EAAK,GAAG,aAAelB,GAAW,CAkG9B,GA7FAA,EAAO,GAAG,aAAc,CAACuB,EAAQC,IAAgB,CAI7CrB,EAAK,eAAiBe,EAAK,QAAQ,KACnCH,EAAI,MACA,qBAAqBN,CAAG,kDAAkDS,EAAK,QAAQ,IAAI,aAAaK,CAAM,SAASvB,EAAO,EAAE,cAAcD,EAAuBC,CAAM,CAAC,gBAAgBA,EAAO,UAAU,KAAK,QAAQ,cAAcG,EAAK,EAAE,EACnP,EACAA,EAAK,cAAc,KAAO,aAAee,EAAK,QAAQ,KACtDrB,EAAO,cAAeM,CAAK,EAG3B,MAAMsB,EAAU,CAER,cAAiB,oBACjB,OAAUF,EACV,MAASpB,EAAK,OAAS,OACvB,KAAQ,SACR,YAAeqB,EAEnB,GAAGF,EAAK,iBAAiBtB,EAAQG,CAAI,CACzC,EAEAmB,EAAK,SAASG,EAAStB,EAAK,IAAKG,EAAI,WAAW,OAAO,EAGvDgB,EAAK,YAAYG,EAAStB,CAAI,EAG9BL,EAAe,KAAK,8BAA8B,KAAK,GAAG,oBAAqB2B,CAAO,CAE1F,CAAC,EAGDzB,EAAO,GAAGM,EAAI,WAAW,OAAQ,SAASE,EAAK,CAC3Cc,EAAK,iBAAiBd,EAAKR,EAAQG,CAAK,CAC5C,CAAC,EAGDH,EAAO,GAAGM,EAAI,WAAW,QAAS,SAASE,EAAK,CAK5C,OAJAL,EAAK,cACLY,EAAI,MAAM,qBAAqBN,CAAG,kCAAkCT,EAAO,EAAE,UAAU,KAAK,UAAUQ,CAAG,CAAC,EAAE,EAGnG,OAAOA,EAAM,CAClB,IAAK,SACL,IAAK,SACL,IAAK,UACDA,EAAM,CAAE,cAAiBA,CAAI,CACrC,CAGAA,EAAM,CAAE,GAAGA,EAAK,GAAGc,EAAK,iBAAiBtB,EAAQG,CAAI,CAAE,EAGvDK,EAAI,KAAO,SAELA,EAAI,QAAQA,EAAI,MAAQL,EAAK,OAEnCmB,EAAK,YAAYd,EAAKL,CAAI,CAE9B,CAAC,EAGDH,EAAO,GAAG,QAAS,SAAS0B,EAAK,CAE7BX,EAAI,MAAM,2BAA2BN,CAAG,yBAAyBT,EAAO,EAAE,aAAa0B,EAAI,OAAO,EAAE,EAGpG,MAAMD,EAAU,CAER,cAAe,eACf,MAAOC,EAAI,QACX,KAAM,SAEV,GAAGJ,EAAK,iBAAiBtB,EAAQG,CAAI,CACzC,EAEAmB,EAAK,YAAYG,EAAStB,CAAI,CAElC,CAAC,EAODA,EAAK,eAAiBe,EAAK,QAAQ,KAEnCH,EAAI,MACA,2BAA2BN,CAAG,4CAA4CS,EAAK,QAAQ,IAAI,gBAAgBlB,EAAO,EAAE,cAAcD,EAAuBC,CAAM,CAAC,gBAAgBA,EAAO,UAAU,KAAK,QAAQ,iBAAiBA,EAAO,SAAS,qBAAqBA,EAAO,UAAU,KAAK,aAAa,cAAcG,EAAK,EAAE,EAChU,EAEIG,EAAI,eAAiB,KAAM,MAAM,IAAI,MAAM,+BAA+B,EAG9E,MAAMK,EAASlB,EAAK,KAAKa,EAAI,aAAcA,EAAI,YAAY,EAC3D,GAAKZ,EAAG,WAAWiB,CAAM,EACrB,GAAI,CACA,MAAMgB,EAAW,QAAShB,CAAO,EAC5B,OAAOgB,GAAa,YACrB3B,EAAO,IAAI2B,CAAQ,EACnBZ,EAAI,MAAM,+BAA+BN,CAAG,4DAA4DA,CAAG,GAAG,GAE9GM,EAAI,KAAK,+BAA+BN,CAAG,6CAA6CA,CAAG,kEAAkE,CAErK,OAASI,EAAG,CACRE,EAAI,KAAK,2BAA2BN,CAAG,mDAAmDI,EAAE,OAAO,EAAE,CACzG,CAGJV,EAAK,cAAc,KAAO,aAAae,EAAK,QAAQ,IAAI,GACxDrB,EAAO,cAAeM,CAAK,EAG3B,MAAMyB,EAAY,CACd,cAAiB,iBACjB,gBAAoB,IAAI,KACxB,MAASzB,EAAK,OAAS,OACvB,QAAWG,EAAI,QACf,UAAaN,EAAO,EACxB,EAaAsB,EAAK,SAASM,EAAWzB,EAAK,IAAKG,EAAI,WAAW,OAAO,EAGzD,MAAMmB,EAAU,CAER,cAAe,iBACf,MAAOtB,EAAK,OAAS,OACrB,KAAM,SAEV,GAAGmB,EAAK,iBAAiBtB,EAAQG,CAAI,CACzC,EAEAmB,EAAK,YAAYG,EAAStB,CAAI,EAG9BL,EAAe,KAAK,8BAA8B,KAAK,GAAG,iBAAkB2B,CAAO,CAIvF,CAAC,CAEL,CAMA,SAAStB,EAAM,CAEX,MAAMe,EAAO,KAAK,aAAaf,EAAK,GAAG,EAGvCe,EAAK,kBAAkB,EAAI,EAE3BA,EAAK,mBAAmB,CAI5B,CAEJ,CAQA,GAAI,CACA,MAAMW,EAAa,IAAIxB,EACvB,OAAO,QAAUwB,CACrB,OAAShB,EAAG,CACR,QAAQ,MAAM,iEAAiEA,EAAE,OAAO,EAAE,CAC9F", + "names": ["path", "fs", "socketio", "tilib", "uiblib", "tiEventManager", "getClientRealIpAddress", "socket", "clientRealIpAddress", "getClientPageName", "node", "pageName", "UibSockets", "uib", "server", "msg", "url", "channel", "mwfile", "sioMsgOut", "e", "RED", "log", "uibSocketPath", "ioOptions", "ioNs", "socketId", "sioMwPath", "sioMiddleware", "that", "reason", "description", "ctrlMsg", "err", "sioUseMw", "msgClient", "uibsockets"] +} diff --git a/nodes/libs/tilib.js b/nodes/libs/tilib.js index ab04e06f..536c2c47 100644 --- a/nodes/libs/tilib.js +++ b/nodes/libs/tilib.js @@ -1,214 +1,2 @@ -/* eslint-disable prefer-named-capture-group */ -/** - * General utility library for Node.JS - * - * Copyright (c) 2019-2023 Julian Knight (Totally Information) - * https://it.knightnet.org.uk - * - * 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. - **/ -'use strict' - -const path = require('path') -// const fs = require('fs-extra') - -const mylog = (process.env.TI_ENV === 'debug') ? console.log : function() {} - -module.exports = { - /** The name of the package.json file 'package.json' */ - packageJson: 'package.json', - - /** Remove leading/trailing slashes from a string - * @param {string} str String to trim - * @returns {string} Trimmed string - */ - trimSlashes: function(str) { - return str.replace(/(^\/*)|(\/*$)/g, '') - }, // ---- End of trimSlashes ---- // - - /** Joins all arguments as a URL string - * @see http://stackoverflow.com/a/28592528/3016654 - * param {...string} [path] URL fragments (picked up via the arguments var) - * @returns {string} Joined path - */ - urlJoin: function() { - /** @type {Array} */ - const paths = Array.prototype.slice.call(arguments) - const url = '/' + paths.map(function(e) { - return e !== undefined ? e.replace(/^\/|\/$/g, '') : '' - }) - .filter(function(e) { - return e - }) - .join('/') - return url.replace('//', '/') - }, // ---- End of urlJoin ---- // - - /** Escape a user input string to use in a regular expression - * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions - * @param {string} string String to escape - * @returns {string} Input string escaped to use in a re - */ - escapeRegExp: function(string) { - return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') // $& means the whole matched string - }, // ---- End of escapeRegExp ---- // - - /** Get a list of all of the npm run scripts in /package.json OR - * Check if a specific script exists in /package.json - * Used to check that restart and build scripts are available. - * @param {string} chkPath - The path that should contain a package.json - * @param {string} chkScript - OPTIONAL. If present return the script text if present - * @returns {object|string|undefined|null} undefined if file not found or list of script names/commands. If chkScript, null if not found or script text. - */ - getNpmRunScripts: function(chkPath, chkScript = '') { - let pj - try { - pj = require( path.join( chkPath, this.packageJson ) ).scripts - } catch (e) { - pj = undefined - } - if ( (pj !== undefined) && (chkScript !== '') ) { - if (pj[chkScript] === undefined) pj = null - else pj = pj[chkScript] - } - return pj - }, // ---- End of getRedUserRunScripts ---- // - - /** Merge and deduplicate multiple arrays - * @see https://stackoverflow.com/a/27664971/1309986 - * @param {any[]} arr One or more arrays - * @returns {any[]} Deduplicated, merged single array - */ - mergeDedupe: function(...arr) { - return [...new Set([].concat(...arr))] - }, // ---- ---- // - - /** Utility function to html pretty-print JSON - * @param {*} json JSON to pretty-print - * @returns {string} HTML - */ - syntaxHighlight: function(json) { - /* - pre .string { color: orange; } - .number { color: white; } - .boolean { color: rgb(20, 99, 163); } - .null { color: magenta; } - .key { color: #069fb3;} - */ - json = JSON.stringify(json, undefined, 4) - json = json - .replace(/&/g, '&') - .replace(//g, '>') - json = '
' +
-            json.replace(/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+-]?\d+)?)/g, function (match) {
-                let cls = 'number'; let style = 'style="color:white"'
-                if ((/^"/).test(match)) {
-                    if ((/:$/).test(match)) {
-                        cls = 'key'
-                        style = 'style="color:#069fb3"'
-                    } else {
-                        cls = 'string'
-                        style = 'style="color:orange"'
-                    }
-                } else if ((/true|false/).test(match)) {
-                    cls = 'boolean'
-                    style = 'style="color:rgb(20,99,163)"'
-                } else if ((/null/).test(match)) {
-                    cls = 'null'
-                    style = 'style="color:magenta"'
-                }
-                return `${match}`
-            }) +
-            '
' - return json - }, // ---- ---- // - - /** Compare 2 simple arrays, return array of arrays - additions and deletions - * @param {Array} a1 First array - * @param {Array} a2 Second array - * @returns {[string[],string[]]} Array of 2 arrays. Inner array 1: Additions, 2: Deletions - */ - compareArrays: function(a1, a2) { - const temp = [[], []] - - // for each a1 entry, if not in a2 then push to temp[0] - for (let i = 0, len = a1.length; i < len; ++i) { - if (a2.indexOf(a1[i]) === -1) temp[0].push(a1[i]) - } - - // for each a2 entry, if not in a1 then push to temp[1] - for (let i = 0, len = a2.length; i < len; ++i) { - if (a1.indexOf(a2[i]) === -1) temp[1].push(a2[i]) - } - - // @ts-ignore - return temp - }, // ---- ---- // - - /** Compare 2 simple arrays, return false as soon as a difference is found - * @param {Array} a1 First array - * @param {Array} a2 Second array - * @returns {boolean} False if arrays are differnt, else True - */ - quickCompareArrays: function(a1, a2) { - // for each a1 entry, if not in a2 then push to temp[0] - for (let i = 0, len = a1.length; i < len; ++i) { - if (a2.indexOf(a1[i]) === -1) return false - } - - // for each a2 entry, if not in a1 then push to temp[1] - for (let i = 0, len = a2.length; i < len; ++i) { - if (a1.indexOf(a2[i]) === -1) return false - } - - return true - }, // ---- ---- // - - /** Return only the most important parts of an ExpressJS `req` object - * @param {object} req express.Request - * @returns {object} importantReq - */ - dumpReq: function(req) { - return { - 'headers': { - 'host': req.headers.host, - 'referer': req.headers.referer, - }, - 'url': req.url, - 'method': req.method, - 'baseUrl': req.baseUrl, - 'hostname': req.hostname, - 'originalUrl': req.originalUrl, - 'path': req.path, - 'protocol': req.protocol, - 'secure': req.secure, - 'subdomains': req.subdomains, - } - }, // ---- ---- // - - /** Debugging output that only executes if an env variable is set before Node-RED is run */ - mylog: mylog, - - /** Dump process memory use to console - * @param {string} prefix Text to output before the memory info - */ - dumpMem: (prefix) => { - const mem = process.memoryUsage() - const formatMem = (m) => ( m / 1048576 ).toFixed(2) - mylog(`${prefix} Memory Use (MB): RSS=${formatMem(mem.rss)}. Heap: Used=${formatMem(mem.heapUsed)}, Tot=${formatMem(mem.heapTotal)}. Ext C++=${formatMem(mem.external)}`) - }, - -} // ---- End of module.exports ---- // - -// EOF +"use strict";const s=require("path"),o=process.env.TI_ENV==="debug"?console.log:function(){};module.exports={packageJson:"package.json",trimSlashes:function(e){return e.replace(/(^\/*)|(\/*$)/g,"")},urlJoin:function(){return("/"+Array.prototype.slice.call(arguments).map(function(t){return t!==void 0?t.replace(/^\/|\/$/g,""):""}).filter(function(t){return t}).join("/")).replace("//","/")},escapeRegExp:function(e){return e.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")},getNpmRunScripts:function(e,r=""){let t;try{t=require(s.join(e,this.packageJson)).scripts}catch{t=void 0}return t!==void 0&&r!==""&&(t[r]===void 0?t=null:t=t[r]),t},mergeDedupe:function(...e){return[...new Set([].concat(...e))]},syntaxHighlight:function(e){return e=JSON.stringify(e,void 0,4),e=e.replace(/&/g,"&").replace(//g,">"),e='
'+e.replace(/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+-]?\d+)?)/g,function(r){let t="number",n='style="color:white"';return/^"/.test(r)?/:$/.test(r)?(t="key",n='style="color:#069fb3"'):(t="string",n='style="color:orange"'):/true|false/.test(r)?(t="boolean",n='style="color:rgb(20,99,163)"'):/null/.test(r)&&(t="null",n='style="color:magenta"'),`${r}`})+"
",e},compareArrays:function(e,r){const t=[[],[]];for(let n=0,l=e.length;n{const r=process.memoryUsage(),t=n=>(n/1048576).toFixed(2);o(`${e} Memory Use (MB): RSS=${t(r.rss)}. Heap: Used=${t(r.heapUsed)}, Tot=${t(r.heapTotal)}. Ext C++=${t(r.external)}`)}}; +//# sourceMappingURL=tilib.js.map diff --git a/nodes/libs/tilib.js.map b/nodes/libs/tilib.js.map new file mode 100644 index 00000000..f56d7951 --- /dev/null +++ b/nodes/libs/tilib.js.map @@ -0,0 +1,7 @@ +{ + "version": 3, + "sources": ["src/libs/tilib.js"], + "sourcesContent": ["/* eslint-disable prefer-named-capture-group */\n/**\n * General utility library for Node.JS\n *\n * Copyright (c) 2019-2023 Julian Knight (Totally Information)\n * https://it.knightnet.org.uk\n *\n * Licensed under the Apache License, Version 2.0 (the 'License');\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an 'AS IS' BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n **/\n'use strict'\n\nconst path = require('path')\n// const fs = require('fs-extra')\n\nconst mylog = (process.env.TI_ENV === 'debug') ? console.log : function() {}\n\nmodule.exports = {\n /** The name of the package.json file 'package.json' */\n packageJson: 'package.json',\n\n /** Remove leading/trailing slashes from a string\n * @param {string} str String to trim\n * @returns {string} Trimmed string\n */\n trimSlashes: function(str) {\n return str.replace(/(^\\/*)|(\\/*$)/g, '')\n }, // ---- End of trimSlashes ---- //\n\n /** Joins all arguments as a URL string\n * @see http://stackoverflow.com/a/28592528/3016654\n * param {...string} [path] URL fragments (picked up via the arguments var)\n * @returns {string} Joined path\n */\n urlJoin: function() {\n /** @type {Array} */\n const paths = Array.prototype.slice.call(arguments)\n const url = '/' + paths.map(function(e) {\n return e !== undefined ? e.replace(/^\\/|\\/$/g, '') : ''\n })\n .filter(function(e) {\n return e\n })\n .join('/')\n return url.replace('//', '/')\n }, // ---- End of urlJoin ---- //\n\n /** Escape a user input string to use in a regular expression\n * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions\n * @param {string} string String to escape\n * @returns {string} Input string escaped to use in a re\n */\n escapeRegExp: function(string) {\n return string.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&') // $& means the whole matched string\n }, // ---- End of escapeRegExp ---- //\n\n /** Get a list of all of the npm run scripts in /package.json OR\n * Check if a specific script exists in /package.json\n * Used to check that restart and build scripts are available.\n * @param {string} chkPath - The path that should contain a package.json\n * @param {string} chkScript - OPTIONAL. If present return the script text if present\n * @returns {object|string|undefined|null} undefined if file not found or list of script names/commands. If chkScript, null if not found or script text.\n */\n getNpmRunScripts: function(chkPath, chkScript = '') {\n let pj\n try {\n pj = require( path.join( chkPath, this.packageJson ) ).scripts\n } catch (e) {\n pj = undefined\n }\n if ( (pj !== undefined) && (chkScript !== '') ) {\n if (pj[chkScript] === undefined) pj = null\n else pj = pj[chkScript]\n }\n return pj\n }, // ---- End of getRedUserRunScripts ---- //\n\n /** Merge and deduplicate multiple arrays\n * @see https://stackoverflow.com/a/27664971/1309986\n * @param {any[]} arr One or more arrays\n * @returns {any[]} Deduplicated, merged single array\n */\n mergeDedupe: function(...arr) {\n return [...new Set([].concat(...arr))]\n }, // ---- ---- //\n\n /** Utility function to html pretty-print JSON\n * @param {*} json JSON to pretty-print\n * @returns {string} HTML\n */\n syntaxHighlight: function(json) {\n /*\n pre .string { color: orange; }\n .number { color: white; }\n .boolean { color: rgb(20, 99, 163); }\n .null { color: magenta; }\n .key { color: #069fb3;}\n */\n json = JSON.stringify(json, undefined, 4)\n json = json\n .replace(/&/g, '&')\n .replace(//g, '>')\n json = '
' +\n            json.replace(/(\"(\\\\u[a-zA-Z0-9]{4}|\\\\[^u]|[^\\\\\"])*\"(\\s*:)?|\\b(true|false|null)\\b|-?\\d+(?:\\.\\d*)?(?:[eE][+-]?\\d+)?)/g, function (match) {\n                let cls = 'number'; let style = 'style=\"color:white\"'\n                if ((/^\"/).test(match)) {\n                    if ((/:$/).test(match)) {\n                        cls = 'key'\n                        style = 'style=\"color:#069fb3\"'\n                    } else {\n                        cls = 'string'\n                        style = 'style=\"color:orange\"'\n                    }\n                } else if ((/true|false/).test(match)) {\n                    cls = 'boolean'\n                    style = 'style=\"color:rgb(20,99,163)\"'\n                } else if ((/null/).test(match)) {\n                    cls = 'null'\n                    style = 'style=\"color:magenta\"'\n                }\n                return `${match}`\n            }) +\n            '
'\n return json\n }, // ---- ---- //\n\n /** Compare 2 simple arrays, return array of arrays - additions and deletions\n * @param {Array} a1 First array\n * @param {Array} a2 Second array\n * @returns {[string[],string[]]} Array of 2 arrays. Inner array 1: Additions, 2: Deletions\n */\n compareArrays: function(a1, a2) {\n const temp = [[], []]\n\n // for each a1 entry, if not in a2 then push to temp[0]\n for (let i = 0, len = a1.length; i < len; ++i) {\n if (a2.indexOf(a1[i]) === -1) temp[0].push(a1[i])\n }\n\n // for each a2 entry, if not in a1 then push to temp[1]\n for (let i = 0, len = a2.length; i < len; ++i) {\n if (a1.indexOf(a2[i]) === -1) temp[1].push(a2[i])\n }\n\n // @ts-ignore\n return temp\n }, // ---- ---- //\n\n /** Compare 2 simple arrays, return false as soon as a difference is found\n * @param {Array} a1 First array\n * @param {Array} a2 Second array\n * @returns {boolean} False if arrays are differnt, else True\n */\n quickCompareArrays: function(a1, a2) {\n // for each a1 entry, if not in a2 then push to temp[0]\n for (let i = 0, len = a1.length; i < len; ++i) {\n if (a2.indexOf(a1[i]) === -1) return false\n }\n\n // for each a2 entry, if not in a1 then push to temp[1]\n for (let i = 0, len = a2.length; i < len; ++i) {\n if (a1.indexOf(a2[i]) === -1) return false\n }\n\n return true\n }, // ---- ---- //\n\n /** Return only the most important parts of an ExpressJS `req` object\n * @param {object} req express.Request\n * @returns {object} importantReq\n */\n dumpReq: function(req) {\n return {\n 'headers': {\n 'host': req.headers.host,\n 'referer': req.headers.referer,\n },\n 'url': req.url,\n 'method': req.method,\n 'baseUrl': req.baseUrl,\n 'hostname': req.hostname,\n 'originalUrl': req.originalUrl,\n 'path': req.path,\n 'protocol': req.protocol,\n 'secure': req.secure,\n 'subdomains': req.subdomains,\n }\n }, // ---- ---- //\n\n /** Debugging output that only executes if an env variable is set before Node-RED is run */\n mylog: mylog,\n\n /** Dump process memory use to console\n * @param {string} prefix Text to output before the memory info\n */\n dumpMem: (prefix) => {\n const mem = process.memoryUsage()\n const formatMem = (m) => ( m / 1048576 ).toFixed(2)\n mylog(`${prefix} Memory Use (MB): RSS=${formatMem(mem.rss)}. Heap: Used=${formatMem(mem.heapUsed)}, Tot=${formatMem(mem.heapTotal)}. Ext C++=${formatMem(mem.external)}`)\n },\n\n} // ---- End of module.exports ---- //\n\n// EOF\n"], + "mappings": "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", + "names": ["path", "mylog", "str", "e", "string", "chkPath", "chkScript", "pj", "arr", "json", "match", "cls", "style", "a1", "a2", "temp", "i", "len", "req", "prefix", "mem", "formatMem", "m"] +} diff --git a/nodes/libs/tilogger.js b/nodes/libs/tilogger.js index 5dcf07ce..f753be6e 100644 --- a/nodes/libs/tilogger.js +++ b/nodes/libs/tilogger.js @@ -1,246 +1,2 @@ -/** Custom logging module using just the Console object - * Use as: - * const logger = require('./tilogger.js') - * var console = logger.console() // output to the current processes stdout/stderr - * console.verbose('This is verbose output - it will be prefixed with date/time and output level') - * Or, create your own output log: - * var console = logger.console('/some/path/mylog.log') // add 2nd param if you want errors to go to a different location - * - * DEPENDENCIES: - * Node.js v8+ - * - * Copyright (c) 2019 Julian Knight (Totally Information) - * https://it.knightnet.org.uk - * - * 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. -*/ -'use strict' - -const fs = require('fs') - -/** Return a formatted date/time - * @param {string|Date} aDT A JavaScript Date object or a date-like string - * @returns {string} Formatted date string: 'mm-dd HH:MM:SS.SSSS' - */ -function fmtDT(aDT) { - if (typeof aDT === 'string') aDT = new Date(aDT) - - var month = String(aDT.getMonth() + 1).padStart(2,'0') // months are zero indexed - var day = String(aDT.getDate()).padStart(2,'0') - var hours = String(aDT.getHours()).padStart(2,'0') - var minutes = String(aDT.getMinutes()).padStart(2,'0') - var seconds = String(aDT.getSeconds()).padStart(2,'0') - var ms = String(aDT.getMilliseconds()).padStart(4,'0') - - return `${month}-${day} ${hours}:${minutes}:${seconds}.${ms}` -} - -/** Return a structured V8 stack trace */ -function prepStack(error, stack) { - const myStack = [] - var j = 0 - for (let i = 0; i < stack.length; i++) { - const frame = stack[i] - const fName = frame.getFileName() - if (!fName.includes('tilogger') && !fName.startsWith('internal/')) { - myStack.push({ - 'Position': ++j, - 'Function':frame.getFunctionName(), - 'File':fName, - 'Line':frame.getLineNumber(), - 'Col':frame.getColumnNumber(), - 'Method':frame.getMethodName(), - 'EvalOrigin':frame.getEvalOrigin(), - 'isToplevel':frame.isToplevel(), - 'isEval':frame.isEval(), - 'isConstructor':frame.isConstructor(), - 'isNative':frame.isNative(), - 'TypeName':frame.getTypeName(), - }) - } - } - return myStack -} -function fullStack() { - const origST = Error.prepareStackTrace - - Error.prepareStackTrace = prepStack - const stack = (new Error('')).stack - Error.prepareStackTrace = origST - - return stack -} - -module.exports = { - - /** Create a better console logger - * @param {string} [stdout=process.stdout] Optional. Filename to use for standard output. If blank or not provided, uses process.stdout. - * @param {string} [errout=process.stderr] Optional. Filename to use for error output. If blank, uses stdout if that is provided or process.stderr. - * @returns {console & tiConsole} Clone of console with additions. - */ - console: function(stdout='', errout='', options={}) { - // TODO Add log rotation - @see https://github.com/iccicci/rotating-file-stream or https://www.npmjs.com/package/file-stream-rotator - let output, errOutput - if (stdout === '') output = process.stdout - else { output = fs.createWriteStream(stdout) } - if (errout === '') { - if (stdout !== '') errOutput = output - else errOutput = process.stderr - } else { errOutput = fs.createWriteStream(errout) } - - /** Create extended logger from console - * @typedef {Object} tiConsole - * @property {string} [stdout] Location of standard output - * @property {string} [errout] Location of error output - * @property {function} [prefix] Function that adds optional text to the beginning of each output - * @property {function} [verbose] Additional, detailed logging level - * @property {function} [stack] Structured current stack output (without internal and logger) - * @property {function} [settings] Current logger settings - * @property {boolean} [debugging] myLogger.debug will only output if this is true - * @property {function} [debug] Independently controlled by the .debugging property, clone of log - * @property {array} [levels] Array of output levels available: ['none','error','warn','info','log','verbose','all'] - * @property {string} [level="log"] One of: none/error/warn/info/log/verbose/all. all=verbose - */ - /** @type {console & tiConsole} */ - const myLogger = new console.Console(output, errOutput) - - // What levels of logging are available? Keep in the order least to most. - myLogger.levels = ['none','error','warn','info','log','verbose','all'] - // What types of logging are there? As above plus debug & stack - let types = ['error','warn','info','log','verbose','debug','stack','settings'] - - // Record where output is going - myLogger.stdout = stdout - myLogger.errout = errout - - //#region Set the output level for logging - var level = 'log' - if (options.hasOwnProperty('level')) { - if ( myLogger.levels.includes(options.level) ) { - level = options.level - } else { - level = 'log' - } - } else { - level = 'log' - } - myLogger.level = level - // TODO Add support for NODE_DEBUG env var - @see https://www.npmjs.com/package/logdown#wildcards - //#endregion ----- ----- - - /** Function to return a std formatted prefix for error/warn/info/log/verbose & debug outputs - * @param {string} [type="log"] One of: error/warn/info/log/verbose or debug - * @returns {string} Fixed-width string with date/time and log level - */ - myLogger.prefix = function(type='log') { - if ( !types.includes(type) ) type = 'unknown' - return `${fmtDT(new Date())} [${type.padEnd(7,' ')}]` - } - // TODO add module/function prefixes - - //#region Add a prefix to all console logs! - // error - const originalConsoleError = myLogger.error - myLogger.error = function() { - // Only output if logging level is 1 (error) or above - if (myLogger.levels.indexOf(myLogger.level) < 1 ) return - - const args = Array.from(arguments) - args.unshift( myLogger.prefix('error') ) - originalConsoleError.apply( myLogger, args ) - } - - // warn - const originalConsoleWarn = myLogger.warn - myLogger.warn = function() { - // Only output if logging level is 2 (warn) or above - if (myLogger.levels.indexOf(myLogger.level) < 2 ) return - - const args = Array.from(arguments) - args.unshift( myLogger.prefix('warn') ) - originalConsoleWarn.apply( myLogger, args ) - } - - // info - const originalConsoleInfo = myLogger.info - myLogger.info = function() { - // Only output if logging level is 3 (info) or above - if (myLogger.levels.indexOf(myLogger.level) < 3 ) return - - const args = Array.from(arguments) - args.unshift( myLogger.prefix('info') ) - originalConsoleInfo.apply( myLogger, args ) - } - - // log - const originalConsoleLog = myLogger.log - myLogger.log = function() { - // Only output if logging level is 4 (log) or above - if (myLogger.levels.indexOf(myLogger.level) < 4 ) return - - const args = Array.from(arguments) - args.unshift( myLogger.prefix('log') ) - originalConsoleLog.apply( myLogger, args ) - } - - //verbose - new logging type, clone of log - myLogger.verbose = function() { - // Only output if logging level is 5 (verbose) or above - if (myLogger.levels.indexOf(myLogger.level) < 5 ) return - - const args = Array.from(arguments) - args.unshift( myLogger.prefix('verbose') ) - originalConsoleLog.apply( myLogger, args ) - } - - //debug (currently an alias for console.log in node.js) - const originalConsoleDebug = myLogger.debug - /** Clone of 'log' but only outputs if myLogger.debugging = true */ - myLogger.debug = function() { - // Only output if logging level is 4 (log) or above - //if (myLogger.levels.indexOf(myLogger.level) < 4 ) return - // Only output if debugging flag is true - if (myLogger.debugging !== true) return - - const args = Array.from(arguments) - args.unshift( myLogger.prefix('debug') ) - originalConsoleDebug.apply( myLogger, args ) - } - - // Stack - myLogger.stack = function() { - // Only output if logging level is above 0 (none) - if (myLogger.levels.indexOf(myLogger.level) <= 0 ) return - - const args = Array.from(arguments) - args.unshift( fullStack() ) - args.unshift( 'Current stack: ' ) - args.unshift( myLogger.prefix('stack') ) - originalConsoleLog.apply( myLogger, args ) - } - - // Settings - output the current logger settings - myLogger.settings = function() { - // Only output if logging level is above 0 (none) - if (myLogger.levels.indexOf(myLogger.level) <= 0 ) return - - const args = Array.from(arguments) - args.unshift( {'Level': myLogger.level, 'Debugging': myLogger.debugging} ) - args.unshift( myLogger.prefix('settings') ) - originalConsoleLog.apply( myLogger, args ) - } - //#endregion ----- ----- - - return myLogger - } -} \ No newline at end of file +"use strict";const g=require("fs");function m(n){typeof n=="string"&&(n=new Date(n));var t=String(n.getMonth()+1).padStart(2,"0"),o=String(n.getDate()).padStart(2,"0"),s=String(n.getHours()).padStart(2,"0"),i=String(n.getMinutes()).padStart(2,"0"),e=String(n.getSeconds()).padStart(2,"0"),l=String(n.getMilliseconds()).padStart(4,"0");return`${t}-${o} ${s}:${i}:${e}.${l}`}function d(n,t){const o=[];var s=0;for(let i=0;i + ${window.hljs.highlightAuto(n).value}`}finally{}return`
${i.utils.escapeHtml(n)}
`}},i=window.markdownit(r);e.slotMarkdown=i.render(e.slotMarkdown),window.DOMPurify&&(e.slotMarkdown=window.DOMPurify.sanitize(e.slotMarkdown)),t.innerHTML=e.slotMarkdown}loadScriptSrc(t){const e=document.createElement("script");e.src=t,e.async=!1,document.head.appendChild(e)}loadStyleSrc(t){const e=document.createElement("link");e.href=t,e.rel="stylesheet",e.type="text/css",document.head.appendChild(e)}loadScriptTxt(t){const e=document.createElement("script");e.async=!1,e.textContent=t,document.head.appendChild(e)}loadStyleTxt(t){const e=document.createElement("style");e.textContent=t,document.head.appendChild(e)}showDialog(t,e,r){let i="";if(r.payload&&typeof r.payload=="string"&&(i+=r.payload),e.content&&(i+=e.content),i===""){s(1,"Ui:showDialog","Toast content is blank. Not shown.")();return}!e.title&&r.topic&&(e.title=r.topic),e.title&&(i=`

${e.title}

${i}

`),e.noAutohide&&(e.noAutoHide=e.noAutohide),e.noAutoHide&&(e.autohide=!e.noAutoHide),e.autoHideDelay?(e.autohide||(e.autohide=!0),e.delay=e.autoHideDelay):e.autoHideDelay=1e4,Object.prototype.hasOwnProperty.call(e,"autohide")||(e.autohide=!0),t==="alert"&&(e.modal=!0,e.autohide=!1,i=` ${i}`);let n=document.getElementById("toaster");n===null&&(n=document.createElement("div"),n.id="toaster",n.title="Click to clear all notifcations",n.setAttribute("class","toaster"),n.setAttribute("role","dialog"),n.setAttribute("arial-label","Toast message"),n.onclick=function(){n.remove()},document.body.insertAdjacentElement("afterbegin",n));const a=document.createElement("div");a.title="Click to clear this notifcation",a.setAttribute("class",`toast ${e.variant?e.variant:""} ${t}`),a.innerHTML=i,a.setAttribute("role","alertdialog"),e.modal&&a.setAttribute("aria-modal",e.modal),a.onclick=function(o){o.stopPropagation(),a.remove(),n.childElementCount<1&&n.remove()},n.insertAdjacentElement(e.appendToast===!0?"beforeend":"afterbegin",a),e.autohide===!0&&setInterval(()=>{a.remove(),n.childElementCount<1&&n.remove()},e.autoHideDelay)}_showNotification(t){t.topic&&!t.title&&(t.title=t.topic),t.title||(t.title="uibuilder notification"),t.payload&&!t.body&&(t.body=t.payload),t.body||(t.body=" No message given.");try{const e=new Notification(t.title,t);return new Promise((r,i)=>{e.addEventListener("close",n=>{n.currentTarget.userAction="close",r(n)}),e.addEventListener("click",n=>{n.currentTarget.userAction="click",r(n)}),e.addEventListener("error",n=>{n.currentTarget.userAction="error",i(n)})})}catch{return Promise.reject(new Error("Browser refused to create a Notification"))}}async notification(t){if(typeof t=="string"&&(t={body:t}),typeof Notification>"u")return Promise.reject(new Error("Notifications not available in this browser"));let e=Notification.permission;return e==="denied"?Promise.reject(new Error("Notifications not permitted by user")):e==="granted"?this._showNotification(t):(e=await Notification.requestPermission(),e==="granted"?this._showNotification(t):Promise.reject(new Error("Notifications not permitted by user")))}loadui(t){if(!fetch){s(0,"Ui:loadui","Current environment does not include `fetch`, skipping.")();return}if(!t){s(0,"Ui:loadui","url parameter must be provided, skipping.")();return}fetch(t).then(e=>{if(e.ok===!1)throw new Error(`Could not load '${t}'. Status ${e.status}, Error: ${e.statusText}`);s("trace","Ui:loadui:then1",`Loaded '${t}'. Status ${e.status}, ${e.statusText}`)();const r=e.headers.get("content-type");if(!r||!r.includes("application/json"))throw new TypeError(`Fetch '${t}' did not return JSON, ignoring`);return e.json()}).then(e=>e!==void 0?(s("trace","Ui:loadui:then2","Parsed JSON successfully obtained")(),this._uiManager({_ui:e}),!0):!1).catch(e=>{s("warn","Ui:loadui:catch","Error. ",e)()})}_uiComposeComponent(t,e){e.attributes&&Object.keys(e.attributes).forEach(r=>{r==="value"&&(t.value=e.attributes[r]),r.startsWith("xlink:")?t.setAttributeNS("http://www.w3.org/1999/xlink",r,e.attributes[r]):t.setAttribute(r,e.attributes[r])}),e.id&&t.setAttribute("id",e.id),e.type==="svg"&&(t.setAttributeNS("http://www.w3.org/2000/xmlns/","xmlns","http://www.w3.org/2000/svg"),t.setAttributeNS("http://www.w3.org/2000/xmlns/","xmlns:xlink","http://www.w3.org/1999/xlink")),e.events&&Object.keys(e.events).forEach(r=>{r.toLowerCase==="onclick"&&(r="click");try{t.addEventListener(r,i=>{new Function("evt",`${e.events[r]}(evt)`)(i)})}catch(i){s("error","Ui:_uiComposeComponent",`Add event '${r}' for element '${e.type}': Cannot add event handler. ${i.message}`)()}}),e.properties&&Object.keys(e.properties).forEach(r=>{t[r]=e.properties[r]}),e.slot&&this.replaceSlot(t,e),e.slotMarkdown&&this.replaceSlotMarkdown(t,e)}uiEnhanceElement(t,e){this._uiComposeComponent(t,e)}_uiExtendEl(t,e,r=""){e.forEach((i,n)=>{s("trace",`Ui:_uiExtendEl:components-forEach:${n}`,i)();let a;i.ns=r,i.ns==="html"?(a=t,t.innerHTML=i.slot):i.ns==="svg"?(a=document.createElementNS("http://www.w3.org/2000/svg",i.type),this._uiComposeComponent(a,i),t.appendChild(a)):(a=document.createElement(i.type==="html"?"div":i.type),this._uiComposeComponent(a,i),t.appendChild(a)),i.components&&this._uiExtendEl(a,i.components,i.ns)})}_uiAdd(t,e){s("trace","Ui:_uiManager:add","Starting _uiAdd")(),t.components.forEach((r,i)=>{s("trace",`Ui:_uiAdd:components-forEach:${i}`,"Component to add: ",r)();let n;switch(r.type){case"html":{r.ns="html",n=document.createElement("div");break}case"svg":{r.ns="svg",n=document.createElementNS("http://www.w3.org/2000/svg","svg");break}default:{r.ns="dom",n=document.createElement(r.type);break}}!r.slot&&t.payload&&(r.slot=t.payload),this._uiComposeComponent(n,r);let a;r.parentEl?a=r.parentEl:t.parentEl?a=t.parentEl:r.parent?a=document.querySelector(r.parent):t.parent&&(a=document.querySelector(t.parent)),a||(s("info","Ui:_uiAdd","No parent found, adding to body")(),a=document.querySelector("body")),r.position&&r.position==="first"?a.insertBefore(n,a.firstChild):r.position&&Number.isInteger(Number(r.position))?a.insertBefore(n,a.children[r.position]):a.appendChild(n),r.components&&this._uiExtendEl(n,r.components,r.ns)})}_uiRemove(t,e=!1){t.components.forEach(r=>{let i;e!==!0?i=[document.querySelector(r)]:i=document.querySelectorAll(r),i.forEach(n=>{try{n.remove()}catch(a){s("trace","Ui:_uiRemove",`Could not remove. ${a.message}`)()}})})}_uiReplace(t){s("trace","Ui:_uiReplace","Starting")(),t.components.forEach((e,r)=>{s("trace",`Ui:_uiReplace:components-forEach:${r}`,"Component to replace: ",e)();let i;if(e.id?i=document.getElementById(e.id):e.selector||e.select?i=document.querySelector(e.selector):e.name?i=document.querySelector(`[name="${e.name}"]`):e.type&&(i=document.querySelector(e.type)),s("trace",`Ui:_uiReplace:components-forEach:${r}`,"Element to replace: ",i)(),i==null){s("trace",`Ui:_uiReplace:components-forEach:${r}:noReplace`,"Cannot find the DOM element. Adding instead.",e)(),this._uiAdd({components:[e]},!1);return}let n;switch(e.type){case"html":{e.ns="html",n=document.createElement("div");break}case"svg":{e.ns="svg",n=document.createElementNS("http://www.w3.org/2000/svg","svg");break}default:{e.ns="dom",n=document.createElement(e.type);break}}this._uiComposeComponent(n,e),i.replaceWith(n),e.components&&this._uiExtendEl(n,e.components,e.ns)})}_uiUpdate(t){s("trace","Ui:_uiManager:update","Starting _uiUpdate")(),t.components||(t.components=[Object.assign({},t)]),t.components.forEach((e,r)=>{s("trace","_uiUpdate:components-forEach",`Component #${r}`,e)();let i;if(e.id?i=document.querySelectorAll(`#${e.id}`):e.selector||e.select?i=document.querySelectorAll(e.selector):e.name?i=document.querySelectorAll(`[name="${e.name}"]`):e.type&&(i=document.querySelectorAll(e.type)),i===void 0||i.length<1){s("warn","Ui:_uiManager:update","Cannot find the DOM element. Ignoring.",e)();return}s("trace","_uiUpdate:components-forEach",`Element(s) to update. Count: ${i.length}`,i)(),!e.slot&&e.payload&&(e.slot=e.payload),i.forEach(n=>{this._uiComposeComponent(n,e)}),e.components&&i.forEach(n=>{s("trace","_uiUpdate:components","el",n)(),this._uiUpdate({method:t.method,parentEl:n,components:e.components})})})}_uiLoad(t){t.components&&(Array.isArray(t.components)||(t.components=[t.components]),t.components.forEach(async e=>{await import(e)})),t.srcScripts&&(Array.isArray(t.srcScripts)||(t.srcScripts=[t.srcScripts]),t.srcScripts.forEach(e=>{this.loadScriptSrc(e)})),t.txtScripts&&(Array.isArray(t.txtScripts)||(t.txtScripts=[t.txtScripts]),this.loadScriptTxt(t.txtScripts.join(` +`))),t.srcStyles&&(Array.isArray(t.srcStyles)||(t.srcStyles=[t.srcStyles]),t.srcStyles.forEach(e=>{this.loadStyleSrc(e)})),t.txtStyles&&(Array.isArray(t.txtStyles)||(t.txtStyles=[t.txtStyles]),this.loadStyleTxt(t.txtStyles.join(` +`)))}_uiReload(){s("trace","Ui:uiManager:reload","reloading")(),location.reload()}_uiManager(t){t._ui&&(Array.isArray(t._ui)||(t._ui=[t._ui]),t._ui.forEach((e,r)=>{if(!e.method){s("error","Ui:_uiManager",`No method defined for msg._ui[${r}]. Ignoring`)();return}switch(e.payload=t.payload,e.topic=t.topic,e.method){case"add":{this._uiAdd(e,!1);break}case"remove":{this._uiRemove(e,!1);break}case"removeAll":{this._uiRemove(e,!0);break}case"replace":{this._uiReplace(e);break}case"update":{this._uiUpdate(e);break}case"load":{this._uiLoad(e);break}case"reload":{this._uiReload();break}case"notify":{this.showDialog("notify",e,t);break}case"alert":{this.showDialog("alert",e,t);break}default:{s("error","Ui:_uiManager",`Invalid msg._ui[${r}].method (${e.method}). Ignoring`)();break}}}))}nodeGet(t,e){const r={id:t.id===""?void 0:t.id,name:t.name,children:t.childNodes.length,type:t.nodeName,attributes:void 0,isUserInput:!!t.validity,userInput:t.validity?{value:t.value,validity:void 0,willValidate:t.willValidate,valueAsDate:t.valueAsDate,valueAsNumber:t.valueAsNumber,type:t.type}:void 0};if(["UL","OL"].includes(t.nodeName)){const i=document.querySelectorAll(`${e} li`);i&&(r.list={entries:i.length})}if(t.nodeName==="DL"){const i=document.querySelectorAll(`${e} dt`);i&&(r.list={entries:i.length})}if(t.nodeName==="TABLE"){const i=document.querySelectorAll(`${e} > tbody > tr`),n=document.querySelectorAll(`${e} > thead > tr`),a=document.querySelectorAll(`${e} > tbody > tr:last-child > *`);(i||n||a)&&(r.table={headRows:n?n.length:0,bodyRows:i?i.length:0,columns:a?a.length:0})}if(t.nodeName!=="#text"&&t.attributes&&t.attributes.length>0){r.attributes={};for(const i of t.attributes)i.name!=="id"&&(r.attributes[i.name]=t.attributes[i.name].value)}t.nodeName==="#text"&&(r.text=t.textContent),t.validity&&(r.userInput.validity={});for(const i in t.validity)r.userInput.validity[i]=t.validity[i];return r}uiGet(t,e=null){const r=document.querySelectorAll(t),i=[];return r.forEach(n=>{if(e!==null&&e!==""){let a=n.getAttribute(e);if(a==null)try{a=n[e]}catch{}if(a==null)e.toLowerCase()==="value"?i.push(n.innerText):i.push(`Property '${e}' not found`);else if(a.constructor.name==="NamedNodeMap"){const o={};for(const l of a)o[l.name]=a[l.name].value;i.push(o)}else if(!a.constructor.name.toLowerCase().includes("map"))i.push({[e]:a});else{const o={};for(const l in a)o[l]=a[l];i.push(o)}}else i.push(this.nodeGet(n,t))}),i}async include(t,e){if(!fetch)return s(0,"Ui:include","Current environment does not include `fetch`, skipping.")(),"Current environment does not include `fetch`, skipping.";if(!t)return s(0,"Ui:include","url parameter must be provided, skipping.")(),"url parameter must be provided, skipping.";if(!e||!e.id)return s(0,"Ui:include","uiOptions parameter MUST be provided and must contain at least an `id` property, skipping.")(),"uiOptions parameter MUST be provided and must contain at least an `id` property, skipping.";let r;try{r=await fetch(t)}catch(c){return s(0,"Ui:include",`Fetch of file '${t}' failed. `,c.message)(),c.message}if(!r.ok)return s(0,"Ui:include",`Fetch of file '${t}' failed. Status='${r.statusText}'`)(),r.statusText;const i=await r.headers.get("content-type");let n=null;i&&(i.includes("text/html")?n="html":i.includes("application/json")?n="json":i.includes("multipart/form-data")?n="form":i.includes("image/")?n="image":i.includes("video/")?n="video":i.includes("application/pdf")?n="pdf":i.includes("text/plain")&&(n="text"));let a="",o="Include successful",l;switch(n){case"html":{l=await r.text(),a=l;break}case"json":{l=await r.json(),a='
',a+=this.syntaxHighlight(l),a+="
";break}case"form":{l=await r.formData(),a='
',a+=this.syntaxHighlight(l),a+="
";break}case"image":{l=await r.blob(),a=``,window&&window.DOMPurify&&(o="Include successful. BUT DOMPurify loaded which may block its use.",s("warn","Ui:include:image",o)());break}case"video":{l=await r.blob(),a=``,window&&window.DOMPurify&&(o="Include successful. BUT DOMPurify loaded which may block its use.",s("warn","Ui:include:video",o)());break}case"pdf":case"text":default:{l=await r.blob(),a=` + ## Developers/Contributors - [Julian Knight](https://github.com/TotallyInformation) - the designer and main author. From 8d6724e206f2bc62df856b8d5c9c231ddf96ce16 Mon Sep 17 00:00:00 2001 From: Julian Knight <1591850+TotallyInformation@users.noreply.github.com> Date: Sun, 1 Oct 2023 18:18:08 +0100 Subject: [PATCH 65/85] GitHub improvements --- .github/FUNDING.yml | 2 + .github/ISSUE_TEMPLATE/bug_report.yml | 65 ++++++++++++++++++++++ .github/ISSUE_TEMPLATE/config.yml | 11 ++++ .github/ISSUE_TEMPLATE/feature_request.yml | 19 +++++++ .github/issue_template.md | 35 ------------ .github/workflows/chk-links-v66.yml | 19 +++++++ .github/workflows/codeql-analysis.yml | 2 +- CITATION.cff | 49 ++++++++++++++++ README.md | 2 - package.json | 5 +- 10 files changed, 169 insertions(+), 40 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.yml create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/ISSUE_TEMPLATE/feature_request.yml delete mode 100644 .github/issue_template.md create mode 100644 .github/workflows/chk-links-v66.yml create mode 100644 CITATION.cff diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 2e1ad665..5115e214 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -3,4 +3,6 @@ # Developing open source projects takes a lot of time and effort. Any level of one-off or recuring sponsorship # helps focus my energies on improving the projects. If you would like to contribute, please use one of these links. +github: TotallyInformation +ko_fi: totallyinformation custom: ['https://paypal.me/TotallyInformation'] diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 00000000..e42e572a --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,65 @@ +name: 🪲 Report a bug or issue +description: File an issue for UIBUILDER +labels: [needs-triage] +body: +- type: markdown + attributes: + value: | + If your issue is a general 'how-to' type question, or something that needs discussion, please + firstly ask on [the Node-RED forum using the node-red-contrib-uibuilder tag](https://totallyinformation.github.io/node-red-contrib-uibuilder). + + If you have a feature request or suggestion for a change, please use the Feature Request template. + + Before logging an issue, please make sure that you are either on the latest npm version or the latest GitHub version of UIBUILDER. + + Please ensure that as much of the following information is included as possible as it makes analysing and fixing it much easier. + + Also, please try to include a simplified Node-RED flow that illustrates the issue. +- type: textarea + attributes: + label: Current Behavior + description: A clear & concise description of what you're experiencing. + validations: + required: false +- type: textarea + attributes: + label: Expected Behavior + description: A clear & concise description of what you expected to happen. + validations: + required: false +- type: textarea + attributes: + label: Steps To Reproduce + description: Steps to reproduce the behavior. + validations: + required: false +- type: textarea + attributes: + label: Example Node-RED flow + description: If you have a minimal example flow that demonstrates the issue, share it here. + value: | + ``` + paste your flow here + ``` + validations: + required: false +- type: textarea + attributes: + label: Environment + description: Please tell us about your environment. Include any relevant information on how you are running Node-RED. + value: | + - UIBUILDER version: + - Node-RED version: + - Node.js version: + - npm version: + - Platform/OS: + - Browser: + + How is Node-RED installed? (globally/locally? admin/user?): + + How/where is UIBUILDER installed (palette-manager/globally/locally? Which folder?) + + Have you changed any of the `uibuilder` settings in your `settings.js` file? + + validations: + required: false diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 00000000..1e0832c0 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,11 @@ +blank_issues_enabled: true +contact_links: + - name: ❓ Ideas, questions & general help + url: https://discourse.nodered.org/tag/node-red-contrib-uibuilder + about: Ask your question on the Node-RED forum using the node-red-contrib-uibuilder tag + - name: 🗂 Documentation + url: https://totallyinformation.github.io/node-red-contrib-uibuilder + about: Go to the latest documentation + - name: 🧑‍💻 Flows + url: https://flows.nodered.org/search?term=uibuilder + about: Example flows, nodes and collections related to UIBUILDER diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 00000000..3d8a0c2a --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,19 @@ +name: 🆕 Request a feature +description: File a bug/issue on the core of Node-RED +labels: [needs-triage] +body: +- type: markdown + attributes: + value: | + Before requesting a new feature or improvement, please check the docs/roadmap.md file in the latest branch to see + whether it is already on the roadmap. + + New features can be discussed in the Node-RED forum under the `node-red-contrib-uibuilder` tag and you may wish + to discuss there before raising a request. +- type: textarea + attributes: + label: Request + description: A clear & concise description of what you would like. + validations: + required: true + diff --git a/.github/issue_template.md b/.github/issue_template.md deleted file mode 100644 index 5973965c..00000000 --- a/.github/issue_template.md +++ /dev/null @@ -1,35 +0,0 @@ -Before logging an issue, please make sure that you are either on the latest npm version or the latest GitHub version. - -Please ensure that as much of the following information is included as possible. - -### Software and Package Versions - -> Node.JS, Node-RED and UIBUILDER back-end versions are listed in the Node-RED log on startup. - `npm --version` shows the installed version of npm. - From your browser's developer console (F12), the UIBUILDER front-end - version can be seen by issuing the command `uibuilder.get('version'). - Please include all 5. Node.JS needs to be at least 4 but realistically, anything less than 6 is unlikely to work. - -> Please also include your Operating System name and version and your browser's name and version. Any browser version less than n-2 or IE < v11 very unlikely to work without help from [polyfill.io](https://polyfill.io). - -> I know it seems like a lot but it saves time in the long run - -Software | Version --------------- | ------- -Node.JS | -npm | -Node-RED | -uibuilder node | -uibuilderFE | -OS | -Browser | - - -### How is Node-RED installed? Where is UIBUILDER installed? - -> Often, issues with nodes occur because of non-standard installations. - This may still indicate a bug so it is fine to report an issue. Just be sure you understand the consequences of how you have installed things. - -> A very common set of issues come from installing nodes as root instead of the user that runs Node-RED (e.g. using sudo on Mac/Linux). You will most likely be asked to undo that before we can analyse the issue. - -> Also quite common is to install the `uibuilder` node in the wrong folder. It is best to install using the Node-RED admin interface "Manage Palette". If installing manually, make sure you are in your `userDir` folder before installing (typically `~/.node-red`). diff --git a/.github/workflows/chk-links-v66.yml b/.github/workflows/chk-links-v66.yml new file mode 100644 index 00000000..0d2958a2 --- /dev/null +++ b/.github/workflows/chk-links-v66.yml @@ -0,0 +1,19 @@ +name: Check .md links on v6.6.0 branch + +on: + push: [v6.6.0] + +jobs: + markdown-link-check: + runs-on: ubuntu-latest + # check out the the code + steps: + - uses: actions/checkout@master + with: + ref: v6.6.0 + + # Checks the status of hyperlinks in .md files in verbose mode + - name: Check links + uses: gaurav-nelson/github-action-markdown-link-check@v1 + with: + use-verbose-mode: 'yes' diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index a3b69441..50518161 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -39,7 +39,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v2 + uses: actions/checkout@master # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL diff --git a/CITATION.cff b/CITATION.cff new file mode 100644 index 00000000..c06e5a6f --- /dev/null +++ b/CITATION.cff @@ -0,0 +1,49 @@ +# This CITATION.cff file was generated with cffinit. +# Visit https://bit.ly/cffinit to generate yours today! + +cff-version: 1.2.0 +title: node-red-contrib-uibuilder +message: >- + If you use this software, please cite it using the + metadata from this file. +type: software +authors: + - given-names: Julian + family-names: Knight + affiliation: Totally Information + - name: Totally Information + city: Sheffield + country: GB + region: South Yorkshire + alias: TotallyInformation +identifiers: + - type: url + value: >- + https://github.com/TotallyInformation/node-red-contrib-uibuilder/releases/latest + description: Latest release of UIBUILDER for Node-RED +repository-code: >- + https://github.com/TotallyInformation/node-red-contrib-uibuilder/ +url: >- + https://totallyinformation.github.io/node-red-contrib-uibuilder +abstract: >- + UIBUILDER for Node-RED allows the easy creation of + data-driven front-end web applications. + + + It includes many helper features that can reduce or + eliminate the need to write code for building data-driven + web applications and user interfaces integrated with + Node-RED. +keywords: + - node-red + - ui + - gui + - dashboard + - SPA + - web + - website + - data-driven + - webpage + - web-app + - ui-builder +license: Apache-2.0 diff --git a/README.md b/README.md index 687498d9..92d22103 100644 --- a/README.md +++ b/README.md @@ -206,8 +206,6 @@ You can also support the development of UIBUILDER by sponsoring the development. [GitHub Sponsorship](https://github.com/sponsors/TotallyInformation), [PayPal Sponsorship](https://paypal.me/TotallyInformation) - - ## Developers/Contributors - [Julian Knight](https://github.com/TotallyInformation) - the designer and main author. diff --git a/package.json b/package.json index 16a57d3a..9e4a8fc2 100644 --- a/package.json +++ b/package.json @@ -98,11 +98,12 @@ "ui", "gui", "dashboard", - "spa", + "SPA", "web", "website", "data-driven", - "webpage" + "webpage", + "web-app" ], "homepage": "https://totallyinformation.github.io/node-red-contrib-uibuilder/#/", "bugs": "https://github.com/TotallyInformation/node-red-contrib-uibuilder/issues", From 93d74eba41d87ad1adfcedda2a0fec18c9dd80bf Mon Sep 17 00:00:00 2001 From: Julian Knight <1591850+TotallyInformation@users.noreply.github.com> Date: Thu, 5 Oct 2023 22:02:40 +0100 Subject: [PATCH 66/85] Add more error handling to the syntaxHighlight function. Allow for undefined input. Output error msg on other errors. --- front-end/uibuilder.esm.js | 40 +++++++++++++--------- front-end/uibuilder.esm.min.js | 2 +- front-end/uibuilder.esm.min.js.map | 4 +-- front-end/uibuilder.iife.js | 40 +++++++++++++--------- front-end/uibuilder.iife.min.js | 2 +- front-end/uibuilder.iife.min.js.map | 4 +-- src/front-end-module/uibuilder.module.js | 42 +++++++++++++++--------- 7 files changed, 80 insertions(+), 54 deletions(-) diff --git a/front-end/uibuilder.esm.js b/front-end/uibuilder.esm.js index fa0c56e3..e1b2edbe 100644 --- a/front-end/uibuilder.esm.js +++ b/front-end/uibuilder.esm.js @@ -4461,23 +4461,30 @@ function urlJoin() { return url2.replace("//", "/"); } function syntaxHighlight(json) { - json = JSON.stringify(json, void 0, 4); - json = json.replace(/&/g, "&").replace(//g, ">"); - json = json.replace(/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+-]?\d+)?)/g, function(match) { - let cls = "number"; - if (/^"/.test(match)) { - if (/:$/.test(match)) { - cls = "key"; - } else { - cls = "string"; - } - } else if (/true|false/.test(match)) { - cls = "boolean"; - } else if (/null/.test(match)) { - cls = "null"; + if (json === void 0) { + json = 'undefined'; + } else { + try { + json = JSON.stringify(json, void 0, 4); + json = json.replace(/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+-]?\d+)?)/g, function(match) { + let cls = "number"; + if (/^"/.test(match)) { + if (/:$/.test(match)) { + cls = "key"; + } else { + cls = "string"; + } + } else if (/true|false/.test(match)) { + cls = "boolean"; + } else if (/null/.test(match)) { + cls = "null"; + } + return `${match}`; + }); + } catch (e) { + json = `Syntax Highlight ERROR: ${e.message}`; } - return '' + match + ""; - }); + } return json; } var _ui = new import_ui.default(window, log, syntaxHighlight); @@ -5756,6 +5763,7 @@ ${document.documentElement.outerHTML}`; case "elementExists": { response = this.elementExists(prop, false); info = `Element "${prop}" ${response ? "exists" : "does not exist"}`; + break; } default: { log("warning", "Uib:_uibCommand", `Command '${cmd}' not yet implemented`)(); diff --git a/front-end/uibuilder.esm.min.js b/front-end/uibuilder.esm.min.js index 5e7af368..1e8091e6 100644 --- a/front-end/uibuilder.esm.min.js +++ b/front-end/uibuilder.esm.min.js @@ -1,2 +1,2 @@ -var wt=Object.create;var oe=Object.defineProperty;var vt=Object.getOwnPropertyDescriptor;var kt=Object.getOwnPropertyNames;var _t=Object.getPrototypeOf,Et=Object.prototype.hasOwnProperty;var Ct=(s,e,t)=>e in s?oe(s,e,{enumerable:!0,configurable:!0,writable:!0,value:t}):s[e]=t;var Ue=(s=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(s,{get:(e,t)=>(typeof require<"u"?require:e)[t]}):s)(function(s){if(typeof require<"u")return require.apply(this,arguments);throw Error('Dynamic require of "'+s+'" is not supported')});var St=(s,e)=>()=>(e||s((e={exports:{}}).exports,e),e.exports),Nt=(s,e)=>{for(var t in e)oe(s,t,{get:e[t],enumerable:!0})},xt=(s,e,t,i)=>{if(e&&typeof e=="object"||typeof e=="function")for(let n of kt(e))!Et.call(s,n)&&n!==t&&oe(s,n,{get:()=>e[n],enumerable:!(i=vt(e,n))||i.enumerable});return s};var we=(s,e,t)=>(t=s!=null?wt(_t(s)):{},xt(e||!s||!s.__esModule?oe(t,"default",{value:s,enumerable:!0}):t,s));var h=(s,e,t)=>(Ct(s,typeof e!="symbol"?e+"":e,t),t),Le=(s,e,t)=>{if(!e.has(s))throw TypeError("Cannot "+t)};var u=(s,e,t)=>(Le(s,e,"read from private field"),t?t.call(s):e.get(s)),S=(s,e,t)=>{if(e.has(s))throw TypeError("Cannot add the same private member more than once");e instanceof WeakSet?e.add(s):e.set(s,t)},R=(s,e,t,i)=>(Le(s,e,"write to private field"),i?i.call(s,t):e.set(s,t),t);var Pe=St((Zt,Be)=>{var d,Ot=(d=class{constructor(e,t,i){h(this,"version","6.6.0-src");h(this,"window");if(e)this.window=e;else throw new Error("Ui:constructor. Current environment does not include `window`, UI functions cannot be used.");this.document=this.window.document,t?d.log=t:d.log=function(){return function(){}},i?this.syntaxHighlight=i:this.syntaxHighlight=function(){}}ui(e){let t={};e._ui?t=e:t._ui=e,this._uiManager(t)}replaceSlot(e,t){t.slot&&e&&(this.window.DOMPurify&&(t.slot=this.window.DOMPurify.sanitize(t.slot)),e.innerHTML=t.slot)}replaceSlotMarkdown(e,t){if(!e||!this.window.markdownit||!t.slotMarkdown)return;let i={html:!0,linkify:!0,_highlight:!0,langPrefix:"language-",highlight(r,o){if(o&&this.window.hljs&&this.window.hljs.getLanguage(o))try{return'
\n                                ').concat(this.window.hljs.highlightAuto(r).value,"
")}finally{}return'
'.concat(n.utils.escapeHtml(r),"
")}},n=this.window.markdownit(i);t.slotMarkdown=n.render(t.slotMarkdown),this.window.DOMPurify&&(t.slotMarkdown=this.window.DOMPurify.sanitize(t.slotMarkdown)),e.innerHTML=t.slotMarkdown}loadScriptSrc(e){let t=this.document.createElement("script");t.src=e,t.async=!1,this.document.head.appendChild(t)}loadStyleSrc(e){let t=this.document.createElement("link");t.href=e,t.rel="stylesheet",t.type="text/css",this.document.head.appendChild(t)}loadScriptTxt(e){let t=this.document.createElement("script");t.async=!1,t.textContent=e,this.document.head.appendChild(t)}loadStyleTxt(e){let t=this.document.createElement("style");t.textContent=e,this.document.head.appendChild(t)}showDialog(e,t,i){let n="";if(i.payload&&typeof i.payload=="string"&&(n+=i.payload),t.content&&(n+=t.content),n===""){d.log(1,"Ui:showDialog","Toast content is blank. Not shown.")();return}!t.title&&i.topic&&(t.title=i.topic),t.title&&(n='

'.concat(t.title,"

").concat(n,"

")),t.noAutohide&&(t.noAutoHide=t.noAutohide),t.noAutoHide&&(t.autohide=!t.noAutoHide),t.autoHideDelay?(t.autohide||(t.autohide=!0),t.delay=t.autoHideDelay):t.autoHideDelay=1e4,Object.prototype.hasOwnProperty.call(t,"autohide")||(t.autohide=!0),e==="alert"&&(t.modal=!0,t.autohide=!1,n=' '.concat(n));let r=this.document.getElementById("toaster");r===null&&(r=this.document.createElement("div"),r.id="toaster",r.title="Click to clear all notifcations",r.setAttribute("class","toaster"),r.setAttribute("role","dialog"),r.setAttribute("arial-label","Toast message"),r.onclick=function(){r.remove()},this.document.body.insertAdjacentElement("afterbegin",r));let o=this.document.createElement("div");o.title="Click to clear this notifcation",o.setAttribute("class","toast ".concat(t.variant?t.variant:""," ").concat(e)),o.innerHTML=n,o.setAttribute("role","alertdialog"),t.modal&&o.setAttribute("aria-modal",t.modal),o.onclick=function(c){c.stopPropagation(),o.remove(),r.childElementCount<1&&r.remove()},r.insertAdjacentElement(t.appendToast===!0?"beforeend":"afterbegin",o),t.autohide===!0&&setInterval(()=>{o.remove(),r.childElementCount<1&&r.remove()},t.autoHideDelay)}_showNotification(e){e.topic&&!e.title&&(e.title=e.topic),e.title||(e.title="uibuilder notification"),e.payload&&!e.body&&(e.body=e.payload),e.body||(e.body=" No message given.");try{let t=new Notification(e.title,e);return new Promise((i,n)=>{t.addEventListener("close",r=>{r.currentTarget.userAction="close",i(r)}),t.addEventListener("click",r=>{r.currentTarget.userAction="click",i(r)}),t.addEventListener("error",r=>{r.currentTarget.userAction="error",n(r)})})}catch{return Promise.reject(new Error("Browser refused to create a Notification"))}}async notification(e){if(typeof e=="string"&&(e={body:e}),typeof Notification>"u")return Promise.reject(new Error("Notifications not available in this browser"));let t=Notification.permission;return t==="denied"?Promise.reject(new Error("Notifications not permitted by user")):t==="granted"?this._showNotification(e):(t=await Notification.requestPermission(),t==="granted"?this._showNotification(e):Promise.reject(new Error("Notifications not permitted by user")))}loadui(e){if(!fetch){d.log(0,"Ui:loadui","Current environment does not include `fetch`, skipping.")();return}if(!e){d.log(0,"Ui:loadui","url parameter must be provided, skipping.")();return}fetch(e).then(t=>{if(t.ok===!1)throw new Error("Could not load '".concat(e,"'. Status ").concat(t.status,", Error: ").concat(t.statusText));d.log("trace","Ui:loadui:then1","Loaded '".concat(e,"'. Status ").concat(t.status,", ").concat(t.statusText))();let i=t.headers.get("content-type");if(!i||!i.includes("application/json"))throw new TypeError("Fetch '".concat(e,"' did not return JSON, ignoring"));return t.json()}).then(t=>t!==void 0?(d.log("trace","Ui:loadui:then2","Parsed JSON successfully obtained")(),this._uiManager({_ui:t}),!0):!1).catch(t=>{d.log("warn","Ui:loadui:catch","Error. ",t)()})}_uiComposeComponent(e,t){t.attributes&&Object.keys(t.attributes).forEach(i=>{i==="value"&&(e.value=t.attributes[i]),i.startsWith("xlink:")?e.setAttributeNS("http://www.w3.org/1999/xlink",i,t.attributes[i]):e.setAttribute(i,t.attributes[i])}),t.id&&e.setAttribute("id",t.id),t.type==="svg"&&(e.setAttributeNS("http://www.w3.org/2000/xmlns/","xmlns","http://www.w3.org/2000/svg"),e.setAttributeNS("http://www.w3.org/2000/xmlns/","xmlns:xlink","http://www.w3.org/1999/xlink")),t.events&&Object.keys(t.events).forEach(i=>{i.toLowerCase==="onclick"&&(i="click");try{e.addEventListener(i,n=>{new Function("evt","".concat(t.events[i],"(evt)"))(n)})}catch(n){d.log("error","Ui:_uiComposeComponent","Add event '".concat(i,"' for element '").concat(t.type,"': Cannot add event handler. ").concat(n.message))()}}),t.properties&&Object.keys(t.properties).forEach(i=>{e[i]=t.properties[i]}),t.slot&&this.replaceSlot(e,t),t.slotMarkdown&&this.replaceSlotMarkdown(e,t)}uiEnhanceElement(e,t){this._uiComposeComponent(e,t)}_uiExtendEl(e,t,i=""){t.forEach((n,r)=>{d.log("trace","Ui:_uiExtendEl:components-forEach:".concat(r),n)();let o;n.ns=i,n.ns==="html"?(o=e,e.innerHTML=n.slot):n.ns==="svg"?(o=this.document.createElementNS("http://www.w3.org/2000/svg",n.type),this._uiComposeComponent(o,n),e.appendChild(o)):(o=this.document.createElement(n.type==="html"?"div":n.type),this._uiComposeComponent(o,n),e.appendChild(o)),n.components&&this._uiExtendEl(o,n.components,n.ns)})}_uiAdd(e,t){d.log("trace","Ui:_uiManager:add","Starting _uiAdd")(),e.components.forEach((i,n)=>{d.log("trace","Ui:_uiAdd:components-forEach:".concat(n),"Component to add: ",i)();let r;switch(i.type){case"html":{i.ns="html",r=this.document.createElement("div");break}case"svg":{i.ns="svg",r=this.document.createElementNS("http://www.w3.org/2000/svg","svg");break}default:{i.ns="dom",r=this.document.createElement(i.type);break}}!i.slot&&e.payload&&(i.slot=e.payload),this._uiComposeComponent(r,i);let o;i.parentEl?o=i.parentEl:e.parentEl?o=e.parentEl:i.parent?o=this.document.querySelector(i.parent):e.parent&&(o=this.document.querySelector(e.parent)),o||(d.log("info","Ui:_uiAdd","No parent found, adding to body")(),o=this.document.querySelector("body")),i.position&&i.position==="first"?o.insertBefore(r,o.firstChild):i.position&&Number.isInteger(Number(i.position))?o.insertBefore(r,o.children[i.position]):o.appendChild(r),i.components&&this._uiExtendEl(r,i.components,i.ns)})}_uiRemove(e,t=!1){e.components.forEach(i=>{let n;t!==!0?n=[this.document.querySelector(i)]:n=this.document.querySelectorAll(i),n.forEach(r=>{try{r.remove()}catch(o){d.log("trace","Ui:_uiRemove","Could not remove. ".concat(o.message))()}})})}_uiReplace(e){d.log("trace","Ui:_uiReplace","Starting")(),e.components.forEach((t,i)=>{d.log("trace","Ui:_uiReplace:components-forEach:".concat(i),"Component to replace: ",t)();let n;if(t.id?n=this.document.getElementById(t.id):t.selector||t.select?n=this.document.querySelector(t.selector):t.name?n=this.document.querySelector('[name="'.concat(t.name,'"]')):t.type&&(n=this.document.querySelector(t.type)),d.log("trace","Ui:_uiReplace:components-forEach:".concat(i),"Element to replace: ",n)(),n==null){d.log("trace","Ui:_uiReplace:components-forEach:".concat(i,":noReplace"),"Cannot find the DOM element. Adding instead.",t)(),this._uiAdd({components:[t]},!1);return}let r;switch(t.type){case"html":{t.ns="html",r=this.document.createElement("div");break}case"svg":{t.ns="svg",r=this.document.createElementNS("http://www.w3.org/2000/svg","svg");break}default:{t.ns="dom",r=this.document.createElement(t.type);break}}this._uiComposeComponent(r,t),n.replaceWith(r),t.components&&this._uiExtendEl(r,t.components,t.ns)})}_uiUpdate(e){d.log("trace","Ui:_uiManager:update","Starting _uiUpdate")(),e.components||(e.components=[Object.assign({},e)]),e.components.forEach((t,i)=>{d.log("trace","_uiUpdate:components-forEach","Component #".concat(i),t)();let n;if(t.id?n=this.document.querySelectorAll("#".concat(t.id)):t.selector||t.select?n=this.document.querySelectorAll(t.selector):t.name?n=this.document.querySelectorAll('[name="'.concat(t.name,'"]')):t.type&&(n=this.document.querySelectorAll(t.type)),n===void 0||n.length<1){d.log("warn","Ui:_uiManager:update","Cannot find the DOM element. Ignoring.",t)();return}d.log("trace","_uiUpdate:components-forEach","Element(s) to update. Count: ".concat(n.length),n)(),!t.slot&&t.payload&&(t.slot=t.payload),n.forEach(r=>{this._uiComposeComponent(r,t)}),t.components&&n.forEach(r=>{d.log("trace","_uiUpdate:components","el",r)(),this._uiUpdate({method:e.method,parentEl:r,components:t.components})})})}_uiLoad(e){e.components&&(Array.isArray(e.components)||(e.components=[e.components]),e.components.forEach(async t=>{await Promise.resolve().then(()=>we(Ue(t)))})),e.srcScripts&&(Array.isArray(e.srcScripts)||(e.srcScripts=[e.srcScripts]),e.srcScripts.forEach(t=>{this.loadScriptSrc(t)})),e.txtScripts&&(Array.isArray(e.txtScripts)||(e.txtScripts=[e.txtScripts]),this.loadScriptTxt(e.txtScripts.join("\n"))),e.srcStyles&&(Array.isArray(e.srcStyles)||(e.srcStyles=[e.srcStyles]),e.srcStyles.forEach(t=>{this.loadStyleSrc(t)})),e.txtStyles&&(Array.isArray(e.txtStyles)||(e.txtStyles=[e.txtStyles]),this.loadStyleTxt(e.txtStyles.join("\n")))}_uiReload(){d.log("trace","Ui:uiManager:reload","reloading")(),location.reload()}_uiManager(e){e._ui&&(Array.isArray(e._ui)||(e._ui=[e._ui]),e._ui.forEach((t,i)=>{if(!t.method){d.log("error","Ui:_uiManager","No method defined for msg._ui[".concat(i,"]. Ignoring"))();return}switch(t.payload=e.payload,t.topic=e.topic,t.method){case"add":{this._uiAdd(t,!1);break}case"remove":{this._uiRemove(t,!1);break}case"removeAll":{this._uiRemove(t,!0);break}case"replace":{this._uiReplace(t);break}case"update":{this._uiUpdate(t);break}case"load":{this._uiLoad(t);break}case"reload":{this._uiReload();break}case"notify":{this.showDialog("notify",t,e);break}case"alert":{this.showDialog("alert",t,e);break}default:{d.log("error","Ui:_uiManager","Invalid msg._ui[".concat(i,"].method (").concat(t.method,"). Ignoring"))();break}}}))}nodeGet(e,t){let i={id:e.id===""?void 0:e.id,name:e.name,children:e.childNodes.length,type:e.nodeName,attributes:void 0,isUserInput:!!e.validity,userInput:e.validity?{value:e.value,validity:void 0,willValidate:e.willValidate,valueAsDate:e.valueAsDate,valueAsNumber:e.valueAsNumber,type:e.type}:void 0};if(["UL","OL"].includes(e.nodeName)){let n=this.document.querySelectorAll("".concat(t," li"));n&&(i.list={entries:n.length})}if(e.nodeName==="DL"){let n=this.document.querySelectorAll("".concat(t," dt"));n&&(i.list={entries:n.length})}if(e.nodeName==="TABLE"){let n=this.document.querySelectorAll("".concat(t," > tbody > tr")),r=this.document.querySelectorAll("".concat(t," > thead > tr")),o=this.document.querySelectorAll("".concat(t," > tbody > tr:last-child > *"));(n||r||o)&&(i.table={headRows:r?r.length:0,bodyRows:n?n.length:0,columns:o?o.length:0})}if(e.nodeName!=="#text"&&e.attributes&&e.attributes.length>0){i.attributes={};for(let n of e.attributes)n.name!=="id"&&(i.attributes[n.name]=e.attributes[n.name].value)}e.nodeName==="#text"&&(i.text=e.textContent),e.validity&&(i.userInput.validity={});for(let n in e.validity)i.userInput.validity[n]=e.validity[n];return i}uiGet(e,t=null){let i=this.document.querySelectorAll(e),n=[];return i.forEach(r=>{if(t!==null&&t!==""){let o=r.getAttribute(t);if(o==null)try{o=r[t]}catch{}if(o==null)t.toLowerCase()==="value"?n.push(r.innerText):n.push("Property '".concat(t,"' not found"));else if(o.constructor.name==="NamedNodeMap"){let c={};for(let l of o)c[l.name]=o[l.name].value;n.push(c)}else if(!o.constructor.name.toLowerCase().includes("map"))n.push({[t]:o});else{let c={};for(let l in o)c[l]=o[l];n.push(c)}}else n.push(this.nodeGet(r,e))}),n}async include(e,t){if(!fetch)return d.log(0,"Ui:include","Current environment does not include `fetch`, skipping.")(),"Current environment does not include `fetch`, skipping.";if(!e)return d.log(0,"Ui:include","url parameter must be provided, skipping.")(),"url parameter must be provided, skipping.";if(!t||!t.id)return d.log(0,"Ui:include","uiOptions parameter MUST be provided and must contain at least an `id` property, skipping.")(),"uiOptions parameter MUST be provided and must contain at least an `id` property, skipping.";let i;try{i=await fetch(e)}catch(m){return d.log(0,"Ui:include","Fetch of file '".concat(e,"' failed. "),m.message)(),m.message}if(!i.ok)return d.log(0,"Ui:include","Fetch of file '".concat(e,"' failed. Status='").concat(i.statusText,"'"))(),i.statusText;let n=await i.headers.get("content-type"),r=null;n&&(n.includes("text/html")?r="html":n.includes("application/json")?r="json":n.includes("multipart/form-data")?r="form":n.includes("image/")?r="image":n.includes("video/")?r="video":n.includes("application/pdf")?r="pdf":n.includes("text/plain")&&(r="text"));let o="",c="Include successful",l;switch(r){case"html":{l=await i.text(),o=l;break}case"json":{l=await i.json(),o='
',o+=this.syntaxHighlight(l),o+="
";break}case"form":{l=await i.formData(),o='
',o+=this.syntaxHighlight(l),o+="
";break}case"image":{l=await i.blob(),o=''),this.window.DOMPurify&&(c="Include successful. BUT DOMPurify loaded which may block its use.",d.log("warn","Ui:include:image",c)());break}case"video":{l=await i.blob(),o=''),this.window.DOMPurify&&(c="Include successful. BUT DOMPurify loaded which may block its use.",d.log("warn","Ui:include:video",c)());break}case"pdf":case"text":default:{l=await i.blob(),o='