Repository: node-red/node-red (js)

Issue:
CSV node does not cope with single quote characters in property names
### Current Behavior

If an array of objects is passed to the CSV node and it is configured to generate headers then, if any of the properties includes a single quote character, then it fails with errors like
Error: Invalid property expression: unexpected array expression at position 12

Confirmed by @TotallyInformation in https://discourse.nodered.org/t/csv-node-does-not-cope-with-quotes-in-property-names-bug-or-feature/69027

### Expected Behavior

It should not fail

### Steps To Reproduce

Run the example flow below

### Example flow

```
[{"id":"0a117283c6ec0e9a","type":"inject","z":"c57d82f577bb37d5","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"[{\"a'a\":\"A1\",\"b\":\"B1\"},{\"a'a\":\"A2\",\"b\":\"B2\"}]","payloadType":"json","x":160,"y":460,"wires":[["e9b9c226cb22fc89"]]},{"id":"e9b9c226cb22fc89","type":"csv","z":"c57d82f577bb37d5","name":"","sep":",","hdrin":"","hdrout":"all","multi":"one","ret":"\\n","temp":"","skip":"0","strings":true,"include_empty_strings":"","include_null_values":"","x":330,"y":460,"wires":[["8a79b816b21ebf6f"]]},{"id":"8a79b816b21ebf6f","type":"debug","z":"c57d82f577bb37d5","name":"debug 28","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":500,"y":460,"wires":[]}]```
```

### Environment

- Node-RED version: 3.2.0
- Node.js version: 16.17.1
- npm version: 8.15.0
- Platform/OS: Ubuntu
- Browser: Chrome


Pull request description:
CSV node check header properties for ' and "
and add test
to close #3919

<!--
## Before you hit that Submit button....

Please read our [contribution guidelines](https://github.com/node-red/node-red/blob/master/CONTRIBUTING.md)
before submitting a pull-request.

## Types of changes

What types of changes does your code introduce?
Put an `x` in the boxes that apply
-->

- [X] Bugfix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)

<!--
If you want to raise a pull-request with a new feature, or a refactoring
of existing code, it **may well get rejected** if it hasn't been discussed on
the [forum](https://discourse.nodered.org) or
[slack team](https://nodered.org/slack) first.

-->

## Proposed changes

<!-- Describe the nature of this change. What problem does it address? -->
Fixes Issue #3919 - if property names contain quotes (unusual but no impossible) then we should output then as the header line of the CSV correctly rather than breaking

## Checklist
<!-- Put an `x` in the boxes that apply -->

- [x] I have read the [contribution guidelines](https://github.com/node-red/node-red/blob/master/CONTRIBUTING.md)
- [ ] For non-bugfix PRs, I have discussed this change on the forum/slack team.
- [x] I have run `grunt` to verify the unit tests pass
- [x] I have added suitable unit tests to cover the new/changed functionality


Patch:
diff --git a/packages/node_modules/@node-red/nodes/core/parsers/70-CSV.js b/packages/node_modules/@node-red/nodes/core/parsers/70-CSV.js
index 39d03f05c..ca193c2b9 100644
--- a/packages/node_modules/@node-red/nodes/core/parsers/70-CSV.js
+++ b/packages/node_modules/@node-red/nodes/core/parsers/70-CSV.js
@@ -135,7 +135,10 @@ module.exports = function(RED) {
                                             ou += node.sep;
                                         }
                                         else {
-                                            var p = RED.util.getMessageProperty(msg,"payload["+s+"]['"+template[t]+"']");
+                                            var tt = template[t];
+                                            if (template[t].indexOf('"') >=0 ) { tt = "'"+tt+"'"; }
+                                            else { tt = '"'+tt+'"'; }
+                                            var p = RED.util.getMessageProperty(msg,'payload["'+s+'"]['+tt+']');
                                             /* istanbul ignore else */
                                             if (p === undefined) { p = ""; }
                                             // fix to honour include null values flag


Test patch:
diff --git a/test/nodes/core/network/21-httprequest_spec.js b/test/nodes/core/network/21-httprequest_spec.js
index 0348dcea1..c6c6ac4ac 100644
--- a/test/nodes/core/network/21-httprequest_spec.js
+++ b/test/nodes/core/network/21-httprequest_spec.js
@@ -1529,7 +1529,7 @@ describe('HTTP Request Node', function() {
                         msg.payload.headers.should.have.property('Content-Type').which.startWith('application/json');
                         //msg.dynamicHeaderName should be present in headers with the value of msg.dynamicHeaderValue
                         msg.payload.headers.should.have.property('dyn-header-name').which.startWith('dyn-header-value');
-                        //static (custom) header set in Flow UI should be present 
+                        //static (custom) header set in Flow UI should be present
                         msg.payload.headers.should.have.property('static-header-name').which.startWith('static-header-value');
                         //msg.headers['location'] should be deleted because Flow UI "Location" header has a blank value
                         //ensures headers with matching characters but different case are eliminated
@@ -2281,7 +2281,7 @@ describe('HTTP Request Node', function() {
             let port = testPort++
 
             let server;
-        
+
             before(function() {
                 server = net.createServer(function (socket) {
                     socket.write("HTTP/1.0 200\nContent-Type: text/plain\n\nHelloWorld")
@@ -2291,7 +2291,7 @@ describe('HTTP Request Node', function() {
                 server.listen(port,'127.0.0.1', function(err) {
                 })
             });
-            
+
             after(function() {
                 server.close()
             });
@@ -2322,14 +2322,14 @@ describe('HTTP Request Node', function() {
                     var n2 = helper.getNode("n2");
                     n2.on('input', function(msg) {
                         try{
-                            msg.payload.should.equal(`RequestError: Parse Error: Missing expected CR after header value : http://localhost:${port}/`)
+                            msg.payload.should.match(/RequestError: Parse Error/)
                             done()
                         } catch (err) {
                             done(err)
                         }
                     })
                     n1.receive({payload: 'foo'})
-                    
+
                 });
             });
         }
diff --git a/test/nodes/core/parsers/70-CSV_spec.js b/test/nodes/core/parsers/70-CSV_spec.js
index d56f05db3..681711b3b 100644
--- a/test/nodes/core/parsers/70-CSV_spec.js
+++ b/test/nodes/core/parsers/70-CSV_spec.js
@@ -766,6 +766,33 @@ describe('CSV node', function() {
             });
         });
 
+        it('should handle a template with quotes in the property names', function(done) {
+            var flow = [ { id:"n1", type:"csv", temp:"", hdrout:"all", wires:[["n2"]] },
+                {id:"n2", type:"helper"} ];
+            helper.load(csvNode, flow, function() {
+                var n1 = helper.getNode("n1");
+                var n2 = helper.getNode("n2");
+                n2.on("input", function(msg) {
+                    try {
+                        msg.should.have.property('payload', 'a"a,b\'b\nA1,B1\nA2,B2\n');
+                        done();
+                    }
+                    catch(e) { done(e); }
+                });
+                var testJson = [
+                    {
+                        "a\"a": "A1",
+                        "b'b": "B1"
+                    },
+                    {
+                        "a\"a": "A2",
+                        "b'b": "B2"
+                    }
+                ]
+                n1.emit("input", {payload:testJson});
+            });
+        });
+
         it('should convert an array of objects to a multi-line csv', function(done) {
             var flow = [ { id:"n1", type:"csv", temp:"a,d,c,b", wires:[["n2"]] },
                 {id:"n2", type:"helper"} ];