From 6c4854846fde2da7ddeee1485058838f5720ff12 Mon Sep 17 00:00:00 2001 From: PaulDalek Date: Thu, 11 Jul 2024 17:43:17 +0200 Subject: [PATCH] Utils module updated with handleResources function moved out, touch #456. --- lib/chart.js | 66 +++++++++++++++- lib/index.js | 5 +- lib/utils.js | 207 ++++++++++++++++++++------------------------------- 3 files changed, 151 insertions(+), 127 deletions(-) diff --git a/lib/chart.js b/lib/chart.js index 47b28f8d..5b1fa059 100644 --- a/lib/chart.js +++ b/lib/chart.js @@ -19,7 +19,6 @@ import { log, logWithStack } from './logger.js'; import { killPool, postWork, stats } from './pool.js'; import { fixType, - handleResources, isCorrectJSON, optionsStringify, roundNumber, @@ -283,6 +282,66 @@ export const findChartSize = (options) => { return size; }; +/** + * Handles and validates resources for export. + * + * @param {Object|string} resources - The resources to be handled. Can be either + * a JSON object, stringified JSON or a path to a JSON file. + * @param {boolean} allowFileResources - Whether to allow loading resources from + * files. + * + * @returns {Object|undefined} - The handled resources or undefined if no valid + * resources are found. + */ +const handleResources = (resources = false, allowFileResources) => { + const allowedProps = ['js', 'css', 'files']; + + let handledResources = resources; + let correctResources = false; + + // Try to load resources from a file + if (allowFileResources && resources.endsWith('.json')) { + try { + handledResources = isCorrectJSON(readFileSync(resources, 'utf8')); + } catch { + return false; + } + } else { + // Try to get JSON + handledResources = isCorrectJSON(resources); + + // Get rid of the files section + if (handledResources && !allowFileResources) { + delete handledResources.files; + } + } + + // Filter from unnecessary properties + for (const propName in handledResources) { + if (!allowedProps.includes(propName)) { + delete handledResources[propName]; + } else if (!correctResources) { + correctResources = true; + } + } + + // Check if at least one of allowed properties is present + if (!correctResources) { + return false; + } + + // Handle files section + if (handledResources.files) { + handledResources.files = handledResources.files.map((item) => item.trim()); + if (!handledResources.files || handledResources.files.length <= 0) { + delete handledResources.files; + } + } + + // Return resources + return handledResources; +}; + /** * Function for finalizing options before export. * @@ -328,6 +387,11 @@ const doExport = async (options, chartJson, endCallback, svg) => { ); } } + + // Check if there are any resources + if (options.customLogic.resources === false) { + log(3, `[cli] No resources found.`); + } } // If the allowCodeExecution flag isn't set, we should refuse the usage diff --git a/lib/index.js b/lib/index.js index 844a9ba5..06c68b19 100644 --- a/lib/index.js +++ b/lib/index.js @@ -33,6 +33,7 @@ import { initPool, killPool } from './pool.js'; import { shutdownCleanUp } from './resource_release.js'; import server, { startServer } from './server/server.js'; import { printLogo, printVersion, printUsage } from './utils.js'; +import { defaultConfig } from '../lib/schemas/config.js'; /** * Attaches exit listeners to the process, ensuring proper cleanup of resources @@ -140,6 +141,8 @@ export default { mapToNewConfig, manualConfig, printLogo, - printUsage, + printUsage: (noLogo) => { + printUsage(defaultConfig, noLogo); + }, printVersion }; diff --git a/lib/utils.js b/lib/utils.js index 64f5d46e..f2c8512a 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -16,9 +16,6 @@ import { readFileSync } from 'fs'; import { join } from 'path'; import { fileURLToPath } from 'url'; -import { defaultConfig } from '../lib/schemas/config.js'; -import { log, logWithStack } from './logger.js'; - const MAX_BACKOFF_ATTEMPTS = 6; const readme = 'https://github.com/highcharts/node-export-server#readme'; @@ -37,8 +34,32 @@ export const __dirname = fileURLToPath(new URL('../.', import.meta.url)); * * @returns {string} - The cleared and standardized text. */ -export const clearText = (text, rule = /\s\s+/g, replacer = ' ') => - text.replaceAll(rule, replacer).trim(); +export function clearText(text, rule = /\s\s+/g, replacer = ' ') { + return text.replaceAll(rule, replacer).trim(); +} + +/** + * Creates a deep copy of the given object or array. + * + * @param {Object|Array} obj - The object or array to be deeply copied. + * + * @returns {Object|Array} - The deep copy of the provided object or array. + */ +export function deepCopy(obj) { + if (obj === null || typeof obj !== 'object') { + return obj; + } + + const copy = Array.isArray(obj) ? [] : {}; + + for (const key in obj) { + if (Object.prototype.hasOwnProperty.call(obj, key)) { + copy[key] = deepCopy(obj[key]); + } + } + + return copy; +} /** * Implements an exponential backoff strategy for retrying a function until @@ -54,7 +75,7 @@ export const clearText = (text, rule = /\s\s+/g, replacer = ' ') => * @throws {Error} - Throws an error if the maximum number of attempts * is reached. */ -export const expBackoff = async (fn, attempt = 0, ...args) => { +export async function expBackoff(fn, attempt = 0, ...args) { try { // Try to call the function return await fn(...args); @@ -69,15 +90,11 @@ export const expBackoff = async (fn, attempt = 0, ...args) => { // Wait given amount of time await new Promise((response) => setTimeout(response, delayInMs)); - log( - 3, - `[pool] Waited ${delayInMs}ms until next call for the resource id: ${args[0]}.` - ); // Try again return expBackoff(fn, attempt, ...args); } -}; +} /** * Fixes the export type based on MIME types and file extensions. @@ -87,7 +104,7 @@ export const expBackoff = async (fn, attempt = 0, ...args) => { * * @returns {string} - The corrected export type. */ -export const fixType = (type, outfile) => { +export function fixType(type, outfile) { // MIME types const mimeTypes = { 'image/png': 'png', @@ -112,67 +129,22 @@ export const fixType = (type, outfile) => { // Return a correct type return mimeTypes[type] || formats.find((t) => t === type) || 'png'; -}; +} /** - * Handles and validates resources for export. - * - * @param {Object|string} resources - The resources to be handled. Can be either - * a JSON object, stringified JSON or a path to a JSON file. - * @param {boolean} allowFileResources - Whether to allow loading resources from - * files. - * - * @returns {Object|undefined} - The handled resources or undefined if no valid - * resources are found. + * Returns stringified date without the GMT text information. */ -export const handleResources = (resources = false, allowFileResources) => { - const allowedProps = ['js', 'css', 'files']; - - let handledResources = resources; - let correctResources = false; - - // Try to load resources from a file - if (allowFileResources && resources.endsWith('.json')) { - try { - handledResources = isCorrectJSON(readFileSync(resources, 'utf8')); - } catch (error) { - return logWithStack(2, error, `[cli] No resources found.`); - } - } else { - // Try to get JSON - handledResources = isCorrectJSON(resources); - - // Get rid of the files section - if (handledResources && !allowFileResources) { - delete handledResources.files; - } - } - - // Filter from unnecessary properties - for (const propName in handledResources) { - if (!allowedProps.includes(propName)) { - delete handledResources[propName]; - } else if (!correctResources) { - correctResources = true; - } - } - - // Check if at least one of allowed properties is present - if (!correctResources) { - return log(3, `[cli] No resources found.`); - } - - // Handle files section - if (handledResources.files) { - handledResources.files = handledResources.files.map((item) => item.trim()); - if (!handledResources.files || handledResources.files.length <= 0) { - delete handledResources.files; - } - } +export function getNewDate() { + // Get rid of the GMT text information + return new Date().toString().split('(')[0].trim(); +} - // Return resources - return handledResources; -}; +/** + * Returns the stored time value in milliseconds. + */ +export function getNewDateTime() { + return new Date().getTime(); +} /** * Validates and parses JSON data. Checks if provided data is or can @@ -211,8 +183,9 @@ export function isCorrectJSON(data, toString) { * * @returns {boolean} - True if the item is an object, false otherwise. */ -export const isObject = (item) => - typeof item === 'object' && !Array.isArray(item) && item !== null; +export function isObject(item) { + return typeof item === 'object' && !Array.isArray(item) && item !== null; +} /** * Checks if the given object is empty. @@ -221,11 +194,14 @@ export const isObject = (item) => * * @returns {boolean} - True if the object is empty, false otherwise. */ -export const isObjectEmpty = (item) => - typeof item === 'object' && - !Array.isArray(item) && - item !== null && - Object.keys(item).length === 0; +export function isObjectEmpty(item) { + return ( + typeof item === 'object' && + !Array.isArray(item) && + item !== null && + Object.keys(item).length === 0 + ); +} /** * Checks if a private IP range URL is found in the given string. @@ -235,7 +211,7 @@ export const isObjectEmpty = (item) => * @returns {boolean} - True if a private IP range URL is found, false * otherwise. */ -export const isPrivateRangeUrlFound = (item) => { +export function isPrivateRangeUrlFound(item) { const regexPatterns = [ /xlink:href="(?:http:\/\/|https:\/\/)?localhost\b/, /xlink:href="(?:http:\/\/|https:\/\/)?10\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/, @@ -245,30 +221,18 @@ export const isPrivateRangeUrlFound = (item) => { ]; return regexPatterns.some((pattern) => pattern.test(item)); -}; +} /** - * Creates a deep copy of the given object or array. - * - * @param {Object|Array} obj - The object or array to be deeply copied. + * Utility to measure elapsed time using the Node.js process.hrtime() method. * - * @returns {Object|Array} - The deep copy of the provided object or array. + * @returns {function(): number} - A function to calculate the elapsed time + * in milliseconds. */ -export const deepCopy = (obj) => { - if (obj === null || typeof obj !== 'object') { - return obj; - } - - const copy = Array.isArray(obj) ? [] : {}; - - for (const key in obj) { - if (Object.prototype.hasOwnProperty.call(obj, key)) { - copy[key] = deepCopy(obj[key]); - } - } - - return copy; -}; +export function measureTime() { + const start = process.hrtime.bigint(); + return () => Number(process.hrtime.bigint() - start) / 1000000; +} /** * Converts the provided options object to a JSON-formatted string with the @@ -280,7 +244,7 @@ export const deepCopy = (obj) => { * * @returns {string} - The JSON-formatted string representing the options. */ -export const optionsStringify = (options, allowFunctions) => { +export function optionsStringify(options, allowFunctions) { const replacerCallback = (name, value) => { if (typeof value === 'string') { value = value.trim(); @@ -306,7 +270,7 @@ export const optionsStringify = (options, allowFunctions) => { /"EXP_FUN|EXP_FUN"/g, '' ); -}; +} /** * Prints the Highcharts Export Server logo and version information. @@ -314,7 +278,7 @@ export const optionsStringify = (options, allowFunctions) => { * @param {boolean} noLogo - If true, only prints version information without * the logo. */ -export const printLogo = (noLogo) => { +export function printLogo(noLogo) { // Get package version either from env or from package.json const packageVersion = JSON.parse( readFileSync(join(__dirname, 'package.json')) @@ -331,16 +295,17 @@ export const printLogo = (noLogo) => { readFileSync(__dirname + '/msg/startup.msg').toString().bold.yellow, `v${packageVersion}\n`.bold ); -}; +} /** * Prints the usage information for CLI arguments. If required, it can list * properties recursively. * + * @param {Object} defaultConfig - Default configuration object for reference. * @param {boolean} noLogo - If true, only prints version information without * the logo. */ -export function printUsage(noLogo) { +export function printUsage(defaultConfig, noLogo) { const pad = 48; // Print the logo and version information @@ -392,7 +357,7 @@ export function printUsage(noLogo) { /** * Prints the Highcharts Export Server logo, version and license information. */ -export const printVersion = () => { +export function printVersion() { // Print the logo and version information printLogo(); @@ -407,7 +372,7 @@ export const printVersion = () => { '\nTo customize your installation, please refer to the README file at:', `\n${readme}\n`.green ); -}; +} /** * Rounds a number to the specified precision. @@ -417,10 +382,10 @@ export const printVersion = () => { * * @returns {number} - The rounded number. */ -export const roundNumber = (value, precision = 1) => { +export function roundNumber(value, precision = 1) { const multiplier = Math.pow(10, precision || 0); return Math.round(+value * multiplier) / multiplier; -}; +} /** * Converts a value to a boolean. @@ -429,10 +394,11 @@ export const roundNumber = (value, precision = 1) => { * * @returns {boolean} - The boolean representation of the input value. */ -export const toBoolean = (item) => - ['false', 'undefined', 'null', 'NaN', '0', ''].includes(item) +export function toBoolean(item) { + return ['false', 'undefined', 'null', 'NaN', '0', ''].includes(item) ? false : !!item; +} /** * Wraps custom code to execute it safely. @@ -443,7 +409,7 @@ export const toBoolean = (item) => * @returns {string|boolean} - The wrapped custom code or false if wrapping * fails. */ -export const wrapAround = (customCode, allowFileResources) => { +export function wrapAround(customCode, allowFileResources) { if (customCode && typeof customCode === 'string') { customCode = customCode.trim(); @@ -461,35 +427,26 @@ export const wrapAround = (customCode, allowFileResources) => { } return customCode.replace(/;$/, ''); } -}; - -/** - * Utility to measure elapsed time using the Node.js process.hrtime() method. - * - * @returns {function(): number} - A function to calculate the elapsed time - * in milliseconds. - */ -export const measureTime = () => { - const start = process.hrtime.bigint(); - return () => Number(process.hrtime.bigint() - start) / 1000000; -}; +} export default { __dirname, clearText, + deepCopy, expBackoff, fixType, - handleResources, + getNewDate, + getNewDateTime, isCorrectJSON, isObject, isObjectEmpty, isPrivateRangeUrlFound, + measureTime, optionsStringify, printLogo, - printVersion, printUsage, + printVersion, roundNumber, toBoolean, - wrapAround, - measureTime + wrapAround };