Merge branch 'master' into master-rebase

pull/5445/head
Nick O'Leary 2026-01-20 17:12:55 +00:00
commit 736bc30277
No known key found for this signature in database
GPG Key ID: 4F2157149161A6C9
27 changed files with 12392 additions and 120 deletions

View File

@ -24,7 +24,7 @@ jobs:
with:
node-version: ${{ matrix.node-version }}
- name: Install Dependencies
run: npm install
run: npm ci
- name: Run tests
run: |
npm run test

1
.gitignore vendored
View File

@ -27,7 +27,6 @@ docs
.vscode
.nyc_output
sync.ffs_db
package-lock.json
.editorconfig
test/resources/50-file-test-file.txt
test/unit/@node-red/runtime/lib/nodes/.testUserHome

View File

@ -22,8 +22,41 @@ Nodes
- Add ability to use pfx or p12 file for TLS connection settings option (#4907) @dceejay
#### 4.1.2: Maintenance Release
#### 4.1.3: Maintenance Release
Editor
- 5343/Editor/Bug: Node help tab resets focus when arrow keys are used to switch between nodes (#5406) @piotrbogun
- Ensure quick-add filter is applied properly when retriggering add (#5427) @knolleary
- TreeList: Fix widget treeList keyboard navigation scroll behavior (#5421) @piotrbogun
- Editor: Flow & subflow names are changed to all lowercase in search dialog #5348 (#5401) @n-lark
- Allow actions show-next-tab and previous to loop (#5355) @GogoVega
- 5404/Editor/Bug: Junction error in Quick Add dialog (#5407) @piotrbogun
- Add tooltip to delete button in node property UI (#5410) @kazuhitoyokoi
- Fix invalid node size in quick add dialog (#5403) @kazuhitoyokoi
- Expand folder to avoid error in library (#5399) @kazuhitoyokoi
- Stricter validator for flow file name in project feature (#5398) @kazuhitoyokoi
- Fix size and scrolling in Git config UI (#5396) @kazuhitoyokoi
- Reveal node in search results via mouseover (#5368) @gorenje
Runtime
- Add package-lock.json for reproducible dependency chains (#5426) @dimitrieh
- Readme markdown refactor for legibility in IDE's (#5423) @dimitrieh
- Update body-parser (#5418) @knolleary
Nodes
- fix(http-request): prevent uncaught exceptions in async hooks (#5392) @Dennis-SEG
- Fix flushing when in variable delay mode (#5382) @dceejay
- File node TypedInput width fix (#5425) @knolleary
- Use TextDecoder() to decode UTF-8 characters (#5416) @kazuhitoyokoi
- Support source information in complete node (#5414) @kazuhitoyokoi
- Fix status node to retrieve status from all nodes (#5412) @kazuhitoyokoi
- Decrement count of http requests after error (#5409) @kazuhitoyokoi
- Fix debug tab to copy displayed value (#5400) @kazuhitoyokoi
#### 4.1.2: Maintenance Release
Editor

View File

@ -1,44 +1,18 @@
<p align="center">
<a href="https://opensource.org/license/apache-2-0"><img src="https://img.shields.io/badge/License-Apache_2.0-blue.svg?color=3F51B5&style=for-the-badge&label=License&logoColor=000000&labelColor=ececec" alt="License: Apache 2.0"></a>
<a href="https://github.com/node-red/node-red/actions?query=branch%3Amaster">
<img src="https://img.shields.io/github/actions/workflow/status/node-red/node-red/tests.yml?branch=master&label=Build%20Status&style=for-the-badge" alt="Build Status"/>
</a>
<br/>
<br/>
<a href="https://discourse.nodered.org">
<img src="https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fdiscourse.nodered.org%2Fabout.json&query=%24.about.stats.users_count&suffix=%20members&label=Forum&logo=discourse&style=for-the-badge&color=0e76b2&logoColor=0e76b2&labelColor=ececec" alt="Node-RED Forum on Discourse"/>
</a>
<a href="https://github.com/node-red">
<img src="https://img.shields.io/badge/dynamic/json?url=https://api.github.com/orgs/node-red&query=$.followers&suffix=%20followers&label=GitHub%20org&style=for-the-badge&logo=github&logoColor=24292F&labelColor=ececec&color=24292F" alt="Node-RED GitHub Organisation Followers"/>
</a>
<a href="https://nodered.org/slack">
<img src="https://img.shields.io/badge/Slack-Join%20Us-4A154B.svg?logo=slack&style=for-the-badge&logoColor=4A154B&labelColor=ececec" alt="Slack"/>
</a>
<br/>
<br/>
</p>
<div align="center">
<p align="center">
<img src="https://nodered.org/about/resources/media/node-red-icon-2.svg" width="200" title="Node-RED Logo">
</p>
<h3 align="center">Low-code programming for event-driven applications</h3>
<br/>
<p align="center">
<a href="https://flows.nodered.org/search?type=node">
<img src="https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fflows.nodered.org%2Fthings%3Fformat%3Djson%26per_page%3D1%26type%3Dnode&query=$.meta.results.count&label=Nodes&style=for-the-badge&labelColor=ececec&color=8f0000" alt="Node-RED Library Nodes"/>
</a>
<a href="https://flows.nodered.org/search?type=flow">
<img src="https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fflows.nodered.org%2Fthings%3Fformat%3Djson%26per_page%3D1%26type%3Dflow&query=$.meta.results.count&label=Flows&style=for-the-badge&labelColor=ececec&color=8f0000" alt="Node-RED Library Flows"/>
</a>
<a href="https://flows.nodered.org/search?type=collection">
<img src="https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fflows.nodered.org%2Fthings%3Fformat%3Djson%26per_page%3D1%26type%3Dcollection&query=$.meta.results.count&label=Collections&style=for-the-badge&labelColor=ececec&color=8f0000" alt="Node-RED Library Collections"/>
</a>
</p>
<br/>
<a href="https://nodered.org/">
<img src="https://nodered.org/images/node-red-screenshot.png" title="Node-RED: Low-code programming for event-driven applications">
</a>
<br/>
[![License][badge-license]][link-license] [![Build Status][badge-build]][link-build]
<br/><img src="https://nodered.org/about/resources/media/node-red-icon-2.svg" width="150" title="Node-RED Logo"><br/>
[![Nodes][badge-nodes]][link-nodes] [![Flows][badge-flows]][link-flows] [![Collections][badge-collections]][link-collections]
### Low-code programming for event-driven applications
<br/><img src="https://nodered.org/images/node-red-screenshot.png" width="600" alt="Node-RED Screenshot"><br/>
[![Forum][badge-forum]][link-forum] [![GitHub][badge-github]][link-github] [![Slack][badge-slack]][link-slack]
</div>
## Quick Start
@ -78,7 +52,7 @@ If you want to run the latest code from git, here's how to get started:
2. Install the node-red dependencies
npm install
npm ci
3. Build the code
@ -124,3 +98,24 @@ Node-RED is a project of the [OpenJS Foundation](http://openjsf.org). Copyright
<a href="http://openjsf.org">
<img src="https://raw.githubusercontent.com/node-red/community-survey/refs/heads/main/public/openjs-foundation-logo.svg" width="240" title="OpenJS Foundation Logo">
</a>
<!-- Badge images -->
[badge-license]: https://img.shields.io/badge/License-Apache_2.0-blue.svg?color=3F51B5&style=for-the-badge&label=License&logoColor=000000&labelColor=ececec
[badge-build]: https://img.shields.io/github/actions/workflow/status/node-red/node-red/tests.yml?branch=master&label=Build%20Status&style=for-the-badge
[badge-forum]: https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fdiscourse.nodered.org%2Fabout.json&query=%24.about.stats.users_count&suffix=%20members&label=Forum&logo=discourse&style=for-the-badge&color=0e76b2&logoColor=0e76b2&labelColor=ececec
[badge-github]: https://img.shields.io/badge/dynamic/json?url=https://api.github.com/orgs/node-red&query=$.followers&suffix=%20followers&label=GitHub%20org&style=for-the-badge&logo=github&logoColor=24292F&labelColor=ececec&color=24292F
[badge-slack]: https://img.shields.io/badge/Slack-Join%20Us-4A154B.svg?logo=slack&style=for-the-badge&logoColor=4A154B&labelColor=ececec
[badge-nodes]: https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fflows.nodered.org%2Fthings%3Fformat%3Djson%26per_page%3D1%26type%3Dnode&query=$.meta.results.count&label=Nodes&style=for-the-badge&labelColor=ececec&color=8f0000
[badge-flows]: https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fflows.nodered.org%2Fthings%3Fformat%3Djson%26per_page%3D1%26type%3Dflow&query=$.meta.results.count&label=Flows&style=for-the-badge&labelColor=ececec&color=8f0000
[badge-collections]: https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fflows.nodered.org%2Fthings%3Fformat%3Djson%26per_page%3D1%26type%3Dcollection&query=$.meta.results.count&label=Collections&style=for-the-badge&labelColor=ececec&color=8f0000
<!-- Badge links -->
[link-license]: https://opensource.org/license/apache-2-0
[link-build]: https://github.com/node-red/node-red/actions?query=branch%3Amaster
[link-forum]: https://discourse.nodered.org
[link-github]: https://github.com/node-red
[link-slack]: https://nodered.org/slack
[link-nodes]: https://flows.nodered.org/search?type=node
[link-flows]: https://flows.nodered.org/search?type=flow
[link-collections]: https://flows.nodered.org/search?type=collection
[link-nodered]: https://nodered.org/

11985
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -32,7 +32,7 @@
"async-mutex": "0.5.0",
"basic-auth": "2.0.1",
"bcryptjs": "3.0.2",
"body-parser": "1.20.3",
"body-parser": "1.20.4",
"chalk": "^4.1.2",
"cheerio": "1.0.0-rc.10",
"clone": "2.1.2",

View File

@ -19,7 +19,7 @@
"@node-red/util": "5.0.0-beta.1",
"@node-red/editor-client": "5.0.0-beta.1",
"bcryptjs": "3.0.2",
"body-parser": "1.20.3",
"body-parser": "1.20.4",
"clone": "2.1.2",
"cors": "2.8.5",
"express-session": "1.18.2",

View File

@ -340,8 +340,10 @@
var deleteButton = $('<a/>',{href:"#",class:"red-ui-editableList-item-remove red-ui-button red-ui-button-small"}).appendTo(li);
$('<i/>',{class:"fa fa-remove"}).appendTo(deleteButton);
li.addClass("red-ui-editableList-item-removable");
var removeTip = RED.popover.tooltip(deleteButton, RED._("common.label.delete"));
deleteButton.on("click", function(evt) {
evt.preventDefault();
removeTip.close();
var data = row.data('data');
li.addClass("red-ui-editableList-item-deleting")
li.fadeOut(300, function() {

View File

@ -443,12 +443,18 @@ RED.tabs = (function() {
}
function activatePreviousTab() {
var previous = findPreviousVisibleTab();
if (previous.length === 0) {
previous = ul.find("li.red-ui-tab:not(.hide-tab)").last();
}
if (previous.length > 0) {
activateTab(previous.find("a"));
}
}
function activateNextTab() {
var next = findNextVisibleTab();
if (next.length === 0) {
next = ul.find("li.red-ui-tab:not(.hide-tab)").first();
}
if (next.length > 0) {
activateTab(next.find("a"));
}

View File

@ -188,7 +188,10 @@
} else {
that._topList.find(".focus").removeClass("focus")
}
target.treeList.label.addClass('focus')
if (target.treeList.label) {
target.treeList.label.addClass('focus')
}
that.reveal(target);
}
});
this._data = [];
@ -879,6 +882,10 @@
}
that._topList.find(".focus").removeClass("focus");
if (item.treeList.label) {
item.treeList.label.addClass("focus");
}
if (triggerEvent !== false) {
this._trigger("select",null,item)

View File

@ -327,7 +327,7 @@ RED.library = (function() {
icon: 'fa fa-cube',
label: options.type,
path: "",
expanded: false,
expanded: true,
children: function(done, item) {
loadLibraryFolder(lib.id, options.url, "", function(children) {
item.children = children;

View File

@ -720,7 +720,7 @@ RED.projects = (function() {
var validateForm = function() {
var valid = true;
var flowFile = projectFlowFileInput.val();
if (flowFile === "" || !/\.json$/.test(flowFile)) {
if (flowFile === "" || !/^[a-zA-Z0-9\-_]+\.json$/.test(flowFile)) {
valid = false;
if (!projectFlowFileInput.hasClass("input-error")) {
projectFlowFileInput.addClass("input-error");
@ -1142,7 +1142,7 @@ RED.projects = (function() {
} else if (projectType === 'empty') {
var flowFile = projectFlowFileInput.val();
if (flowFile === "" || !/\.json$/.test(flowFile)) {
if (flowFile === "" || !/^[a-zA-Z0-9\-_]+\.json$/.test(flowFile)) {
valid = false;
if (!projectFlowFileInput.hasClass("input-error")) {
projectFlowFileInput.addClass("input-error");

View File

@ -49,9 +49,13 @@ RED.search = (function() {
function indexNode(n) {
var l = RED.utils.getNodeLabel(n);
if (l) {
l = (""+l).toLowerCase();
index[l] = index[l] || {};
index[l][n.id] = {node:n,label:l}
const originalLabel = "" + l;
const indexLabel = originalLabel.toLowerCase();
index[indexLabel] = index[indexLabel] || {};
index[indexLabel][n.id] = {
node: n,
label: originalLabel
};
}
l = l||n.label||n.name||n.id||"";
@ -681,7 +685,17 @@ RED.search = (function() {
show: show,
hide: hide,
search: search,
getSearchOptions: getSearchOptions
getSearchOptions: getSearchOptions,
// Expose internals for testing
_indexNode: indexNode,
get _index() { return index; },
set _index(val) { index = val; }
};
})();
// Allow CommonJS import for testing
if (typeof module !== "undefined" && module.exports) {
module.exports = RED.search;
}

View File

@ -265,7 +265,13 @@ RED.utils = (function() {
var copyPayload = $('<button class="red-ui-button red-ui-button-small"><i class="fa fa-clipboard"></i></button>').appendTo(copyTools).on("click", function(e) {
e.preventDefault();
e.stopPropagation();
RED.clipboard.copyText(msg,copyPayload,"clipboard.copyMessageValue");
var payloadToCopy;
if (typeof msg === "number") {
payloadToCopy = obj.find(".red-ui-debug-msg-type-number").first().text();
} else {
payloadToCopy = msg;
}
RED.clipboard.copyText(payloadToCopy, copyPayload, "clipboard.copyMessageValue");
})
RED.popover.tooltip(copyPayload,RED._("node-red:debug.sidebar.copyPayload"));
if (enablePinning && strippedKey !== undefined && strippedKey !== '') {
@ -593,7 +599,7 @@ RED.utils = (function() {
var sr = $('<div class="red-ui-debug-msg-object-entry collapsed"></div>').appendTo(stringRow);
var stringEncoding = "";
try {
stringEncoding = String.fromCharCode.apply(null, new Uint16Array(data))
stringEncoding = new TextDecoder().decode(new Uint8Array(data));
} catch(err) {
console.log(err);
}

View File

@ -1746,6 +1746,13 @@ RED.view = (function() {
quickAddLink.virtualLink = true;
}
hideDragLines();
} else if (quickAddLink) {
// continuing an existing quick add - set the filter accordingly
if (quickAddLink.portType === PORT_TYPE_OUTPUT) {
filter = {input:true}
} else {
filter = {output:true}
}
}
if (linkToSplice || spliceMultipleLinks) {
filter = {
@ -7372,6 +7379,9 @@ RED.view = (function() {
suggestedNodes = [suggestedNodes]
}
suggestedNodes = suggestedNodes.filter(n => {
if (n.type === 'junction') {
return true
}
const def = RED.nodes.getType(n.type)
if (def?.set && def.set.enabled === false) {
// Exclude disabled node set

View File

@ -755,6 +755,7 @@ div.red-ui-projects-dialog-ssh-public-key {
}
.red-ui-projects-dialog-list {
display: block;
position: relative;
.red-ui-editableList-container {
padding: 1px;
@ -830,7 +831,13 @@ div.red-ui-projects-dialog-ssh-public-key {
}
#red-ui-settings-tab-gitconfig {
position: absolute;
top: 0;
right: 0;
left: 0;
bottom: 0;
padding: 8px 20px 20px;
overflow-y: auto;
}
.red-ui-settings-section-description {
color: var(--red-ui-secondary-text-color);

View File

@ -83,6 +83,7 @@
width: 18px;
height: 15px;
margin-top: 1px;
flex-shrink: 0;
}
.red-ui-search-result-node-port {
position: absolute;

View File

@ -21,6 +21,7 @@
padding: 2px 16px 2px 4px;
font-size: 0.9em;
}
overflow-x: hidden;
}
.red-ui-editableList-container {
padding: 5px;

View File

@ -20,13 +20,15 @@ module.exports = function(RED) {
function StatusNode(n) {
RED.nodes.createNode(this,n);
var node = this;
this.scope = n.scope || [];
this.scope = n.scope;
// auto-filter out any directly connected nodes to avoid simple loopback
const w = this.wires.flat();
for (let i=0; i < this.scope.length; i++) {
if (w.includes(this.scope[i])) {
this.scope.splice(i, 1);
if (Array.isArray(this.scope)) {
const w = this.wires.flat();
for (let i = 0; i < this.scope.length; i++) {
if (w.includes(this.scope[i])) {
this.scope.splice(i, 1);
}
}
}

View File

@ -192,7 +192,8 @@ module.exports = function(RED) {
}
done();
}, delayvar, () => done());
node.idList.push(id);
if (Object.keys(msg).length === 2 && msg.hasOwnProperty("flush")) { id.clear(); }
else { node.idList.push(id); }
if (msg.hasOwnProperty("reset")) { clearDelayList(true); }
if (msg.hasOwnProperty("flush")) { flushDelayList(msg.flush); done(); }
if (delayvar >= 0) {

View File

@ -264,15 +264,19 @@
$("#tls-config-button-cert-clear").on("click", function() {
clearNameData("cert");
});
RED.popover.tooltip($("#tls-config-button-cert-clear"), RED._("common.label.delete"));
$("#tls-config-button-key-clear").on("click", function() {
clearNameData("key");
});
RED.popover.tooltip($("#tls-config-button-key-clear"), RED._("common.label.delete"));
$("#tls-config-button-ca-clear").on("click", function() {
clearNameData("ca");
});
RED.popover.tooltip($("#tls-config-button-ca-clear"), RED._("common.label.delete"));
$("#tls-config-button-p12-clear").on("click", function() {
clearNameData("p12");
});
RED.popover.tooltip($("#tls-config-button-p12-clear"), RED._("common.label.delete"));
if (RED.settings.tlsConfigDisableLocalFiles) {
$("#node-config-row-uselocalfiles").hide();

View File

@ -143,15 +143,6 @@ in your Node-RED user directory (${RED.settings.userDir}).
});
}
}
/**
* @param {Object} headersObject
* @param {string} name
* @return {any} value
*/
const getHeaderValue = (headersObject, name) => {
const asLowercase = name.toLowercase();
return headersObject[Object.keys(headersObject).find(k => k.toLowerCase() === asLowercase)];
}
this.count = 0;
this.on("input",function(msg,nodeSend,nodeDone) {
node.count++;
@ -256,34 +247,42 @@ in your Node-RED user directory (${RED.settings.userDir}).
opts.hooks = {
beforeRequest: [
options => {
// Whilst HTTP headers are meant to be case-insensitive,
// in the real world, there are servers that aren't so compliant.
// GOT will lower case all headers given a chance, so we need
// to restore the case of any headers the user has set.
Object.keys(options.headers).forEach(h => {
if (originalHeaderMap[h] && originalHeaderMap[h] !== h) {
options.headers[originalHeaderMap[h]] = options.headers[h];
delete options.headers[h];
try {
// Whilst HTTP headers are meant to be case-insensitive,
// in the real world, there are servers that aren't so compliant.
// GOT will lower case all headers given a chance, so we need
// to restore the case of any headers the user has set.
Object.keys(options.headers).forEach(h => {
if (originalHeaderMap[h] && originalHeaderMap[h] !== h) {
options.headers[originalHeaderMap[h]] = options.headers[h];
delete options.headers[h];
}
})
if (node.insecureHTTPParser) {
// Setting the property under _unixOptions as pretty
// much the only hack available to get got to apply
// a core http option it doesn't think we should be
// allowed to set
options._unixOptions = { ...options.unixOptions, insecureHTTPParser: true }
}
})
if (node.insecureHTTPParser) {
// Setting the property under _unixOptions as pretty
// much the only hack available to get got to apply
// a core http option it doesn't think we should be
// allowed to set
options._unixOptions = { ...options.unixOptions, insecureHTTPParser: true }
} catch (err) {
node.warn("Error in beforeRequest hook: " + err.message);
}
}
],
beforeRedirect: [
(options, response) => {
let redirectInfo = {
location: response.headers.location
try {
let redirectInfo = {
location: response.headers.location
}
if (response.headers.hasOwnProperty('set-cookie')) {
redirectInfo.cookies = extractCookies(response.headers['set-cookie']);
}
redirectList.push(redirectInfo)
} catch (err) {
node.warn("Error processing redirect: " + err.message);
}
if (response.headers.hasOwnProperty('set-cookie')) {
redirectInfo.cookies = extractCookies(response.headers['set-cookie']);
}
redirectList.push(redirectInfo)
}
]
}
@ -422,25 +421,30 @@ in your Node-RED user directory (${RED.settings.userDir}).
let digestCreds = this.credentials;
let sentCreds = false;
opts.hooks.afterResponse = [(response, retry) => {
if (response.statusCode === 401) {
if (sentCreds) {
return response
try {
if (response.statusCode === 401) {
if (sentCreds) {
return response
}
const requestUrl = new URL(response.request.requestUrl);
const options = { headers: {} }
const normalisedHeaders = {};
Object.keys(response.headers).forEach(k => {
normalisedHeaders[k.toLowerCase()] = response.headers[k]
})
if (normalisedHeaders['www-authenticate']) {
let authHeader = buildDigestHeader(digestCreds.user,digestCreds.password, response.request.options.method, requestUrl.pathname + requestUrl.search, normalisedHeaders['www-authenticate'])
options.headers.Authorization = authHeader;
}
// response.request.options.merge(options)
sentCreds = true;
return retry(options);
}
const requestUrl = new URL(response.request.requestUrl);
const options = { headers: {} }
const normalisedHeaders = {};
Object.keys(response.headers).forEach(k => {
normalisedHeaders[k.toLowerCase()] = response.headers[k]
})
if (normalisedHeaders['www-authenticate']) {
let authHeader = buildDigestHeader(digestCreds.user,digestCreds.password, response.request.options.method, requestUrl.pathname + requestUrl.search, normalisedHeaders['www-authenticate'])
options.headers.Authorization = authHeader;
}
// response.request.options.merge(options)
sentCreds = true;
return retry(options);
return response
} catch (err) {
node.warn("Digest authentication failed: " + err.message);
return response;
}
return response
}];
} else if (this.authType === "bearer") {
opts.headers.Authorization = `Bearer ${this.credentials.password||""}`
@ -688,6 +692,7 @@ in your Node-RED user directory (${RED.settings.userDir}).
if (!sendErrorsToCatch) {
nodeSend(msg);
}
node.count--;
nodeDone();
});
});
@ -720,13 +725,29 @@ in your Node-RED user directory (${RED.settings.userDir}).
function extractCookies(setCookie) {
var cookies = {};
if (!Array.isArray(setCookie)) {
return cookies;
}
setCookie.forEach(function(c) {
var parsedCookie = cookie.parse(c);
var eq_idx = c.indexOf('=');
var key = c.substr(0, eq_idx).trim()
parsedCookie.value = parsedCookie[key];
delete parsedCookie[key];
cookies[key] = parsedCookie;
try {
if (typeof c !== 'string') {
return;
}
var parsedCookie = cookie.parse(c);
var eq_idx = c.indexOf('=');
if (eq_idx === -1) {
return;
}
var key = c.substr(0, eq_idx).trim()
if (!key) {
return;
}
parsedCookie.value = parsedCookie[key];
delete parsedCookie[key];
cookies[key] = parsedCookie;
} catch (err) {
// Skip malformed cookies
}
});
return cookies;
}
@ -778,6 +799,9 @@ in your Node-RED user directory (${RED.settings.userDir}).
}
function buildDigestHeader(user, pass, method, path, authHeader) {
if (!authHeader || typeof authHeader !== 'string') {
throw new Error("Invalid or missing WWW-Authenticate header");
}
var challenge = {}
var re = /([a-z0-9_-]+)=(?:"([^"]+)"|([a-z0-9_-]+))/gi
for (;;) {
@ -787,6 +811,12 @@ in your Node-RED user directory (${RED.settings.userDir}).
}
challenge[match[1]] = match[2] || match[3]
}
if (!challenge.nonce) {
throw new Error("Invalid digest challenge: missing nonce");
}
if (!challenge.realm) {
throw new Error("Invalid digest challenge: missing realm");
}
var qop = /(^|,)\s*auth\s*($|,)/.test(challenge.qop) && 'auth'
var nc = qop && '00000001'
var cnonce = qop && uuid().replace(/-/g, '')

View File

@ -2,7 +2,7 @@
<script type="text/html" data-template-name="file">
<div class="form-row node-input-filename">
<label for="node-input-filename"><i class="fa fa-file"></i> <span data-i18n="file.label.filename"></span></label>
<input id="node-input-filename" type="text">
<input id="node-input-filename" type="text" style="width: 100% !important;">
<input type="hidden" id="node-input-filenameType">
</div>
<div class="form-row">
@ -38,7 +38,7 @@
<script type="text/html" data-template-name="file in">
<div class="form-row">
<label for="node-input-filename"><i class="fa fa-file"></i> <span data-i18n="file.label.filename"></span></label>
<input id="node-input-filename" type="text">
<input id="node-input-filename" type="text" style="width: 100% !important;">
<input type="hidden" id="node-input-filenameType">
</div>
<div class="form-row">

View File

@ -18,7 +18,7 @@
"acorn": "8.15.0",
"acorn-walk": "8.3.4",
"ajv": "8.17.1",
"body-parser": "1.20.3",
"body-parser": "1.20.4",
"cheerio": "1.0.0-rc.10",
"content-type": "1.0.5",
"cookie-parser": "1.4.7",

View File

@ -699,6 +699,13 @@ class Flow {
let toSend = msg;
this.completeNodeMap[node.id].forEach((completeNode,index) => {
toSend = redUtil.cloneMessage(msg);
toSend.complete = {
source: {
id: node.id,
type: node.type,
name: node.name
}
};
completeNode.receive(toSend);
})
}

View File

@ -15,7 +15,7 @@
**/
var should = require("should");
var catchNode = require("nr-test-utils").require("@node-red/nodes/core/common/25-status.js");
var statusNode = require("nr-test-utils").require("@node-red/nodes/core/common/25-status.js");
var helper = require("node-red-node-test-helper");
describe('status Node', function() {
@ -27,7 +27,7 @@ describe('status Node', function() {
it('should output a message when called', function(done) {
var flow = [ { id:"n1", type:"status", name:"status", wires:[["n2"]], scope:[] },
{id:"n2", type:"helper"} ];
helper.load(catchNode, flow, function() {
helper.load(statusNode, flow, function() {
var n1 = helper.getNode("n1");
var n2 = helper.getNode("n2");
n1.should.have.property('name', 'status');

View File

@ -0,0 +1,162 @@
const should = require("should");
const sinon = require("sinon");
const NR_TEST_UTILS = require("nr-test-utils");
// Path to the search.js module
const searchModulePath = NR_TEST_UTILS.resolve("@node-red/editor-client/src/js/ui/search.js");
describe("editor-client/ui/search", function() {
let search;
let mockRED;
beforeEach(function() {
// Set up minimal RED global mock - only what's needed for tests
mockRED = {
utils: {
getNodeLabel: sinon.stub()
}
};
global.RED = mockRED;
// Clear require cache to get fresh module instance
delete require.cache[searchModulePath];
search = require(searchModulePath);
// Reset the index for each test
search._index = {};
});
afterEach(function() {
sinon.restore();
delete global.RED;
delete require.cache[searchModulePath];
});
describe("indexNode", function() {
it("preserves original label casing in search results", function() {
const node = {
id: "node1",
type: "tab",
_def: { category: "config" }
};
mockRED.utils.getNodeLabel.returns("MyFlow Name");
search._indexNode(node);
// Verify the index key is lowercase (for case-insensitive searching)
should.exist(search._index["myflow name"]);
// Verify the stored label preserves original casing
search._index["myflow name"]["node1"].label.should.equal("MyFlow Name");
});
it("indexes node with mixed case label correctly", function() {
const node = {
id: "node2",
type: "subflow",
_def: { category: "subflows" }
};
mockRED.utils.getNodeLabel.returns("MySubFlow_Test");
search._indexNode(node);
// Index key should be lowercase
should.exist(search._index["mysubflow_test"]);
// Label should preserve original casing
search._index["mysubflow_test"]["node2"].label.should.equal("MySubFlow_Test");
});
it("handles uppercase labels", function() {
const node = {
id: "node3",
type: "tab",
_def: { category: "config" }
};
mockRED.utils.getNodeLabel.returns("UPPERCASE FLOW");
search._indexNode(node);
should.exist(search._index["uppercase flow"]);
search._index["uppercase flow"]["node3"].label.should.equal("UPPERCASE FLOW");
});
it("handles lowercase labels", function() {
const node = {
id: "node4",
type: "tab",
_def: { category: "config" }
};
mockRED.utils.getNodeLabel.returns("lowercase flow");
search._indexNode(node);
should.exist(search._index["lowercase flow"]);
search._index["lowercase flow"]["node4"].label.should.equal("lowercase flow");
});
it("stores node reference correctly", function() {
const node = {
id: "node5",
type: "tab",
_def: { category: "config" }
};
mockRED.utils.getNodeLabel.returns("Test Flow");
search._indexNode(node);
search._index["test flow"]["node5"].node.should.equal(node);
});
it("handles nodes without labels by falling back to id", function() {
const node = {
id: "node6",
type: "tab",
_def: { category: "config" }
};
mockRED.utils.getNodeLabel.returns(null);
search._indexNode(node);
// When there's no label from getNodeLabel,
// the node is still indexed by its id
should.exist(search._index["node6"]);
search._index["node6"]["node6"].label.should.equal("node6");
});
});
describe("search", function() {
it("finds nodes with case-insensitive search", function() {
// Manually set up index with mixed case labels
search._index = {
"myflow": {
"node1": {
node: { id: "node1", type: "tab", _def: { category: "config" } },
label: "MyFlow"
}
}
};
// Search with lowercase should find the node
const results = search.search("myflow");
results.length.should.equal(1);
results[0].label.should.equal("MyFlow");
});
it("returns preserved casing in search results", function() {
search._index = {
"test subflow": {
"node1": {
node: { id: "node1", type: "subflow", _def: { category: "subflows" } },
label: "Test SubFlow"
}
}
};
const results = search.search("test");
results.length.should.equal(1);
results[0].label.should.equal("Test SubFlow");
});
});
});