├── .clasp.json
├── .gitattributes
├── .vscode
└── tasks.json
├── LICENSE
├── README.md
├── appsscript.json
├── index.html
├── screenshots
├── 001.png
├── 002.png
├── 003.png
└── gas-published-settings.png
├── web app.js
└── xml rule schemas.js
/.clasp.json:
--------------------------------------------------------------------------------
1 | {"scriptId":"1uCXe4NZbHYxVict4z3yYm3FWufKmeOphHCejQTs7p_WbyaW0G0_8ClIB"}
2 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Auto detect text files and perform LF normalization
2 | * text=auto
3 |
--------------------------------------------------------------------------------
/.vscode/tasks.json:
--------------------------------------------------------------------------------
1 | {
2 | // See https://go.microsoft.com/fwlink/?LinkId=733558
3 | // for the documentation about the tasks.json format
4 | "version": "2.0.0",
5 | "tasks": [
6 | {
7 | "label": "push to GAS",
8 | "type": "shell",
9 | "command": "clasp push",
10 | "group": {
11 | "kind": "build",
12 | "isDefault": true
13 | }
14 | },
15 | {
16 | "label": "deploy",
17 | "type": "shell",
18 | "command": "clasp deploy -i AKfycby0nVfwX-AKKwhydWfojp0AxlL5zUHdKbRs0iCEZAvziDao8Iou",
19 | "problemMatcher": []
20 | }
21 | ]
22 | }
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 IMTheNachoMan
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # pfSense Firewall Rules Manager
2 |
3 | A GAS web-app to manage pfSense FW rules from a Google Sheets spreadsheet
4 |
5 | - [overview](#overview)
6 | - [screenshots](#screenshots)
7 | - [to use](#to-use)
8 | - [how it works](#how-it-works)
9 | - [to do](#to-do)
10 | - [about Google Apps Script, permissions, and security](#about-google-apps-script-permissions-and-security)
11 | - [self-publish](#self-publish)
12 | - [disclaimer and warnings](#disclaimer-and-warnings)
13 | - [contact, support, help](#contact-support-help)
14 |
15 | ## overview
16 |
17 | I am not a fan of the pfSense rule editor. I like having all of my rules in one table.
18 |
19 | So I wrote a [Google Apps Script](https://developers.google.com/apps-script/) web-app to make it easier for me to **view the rules** I have and to **create new rules**. I wrote this for myself and thought others might be interested so I am sharing with others.
20 |
21 | The app lets you:
22 |
23 | - convert the firewall rules from pfSense's **Backup & Restore** XML file into a CSV or Google Sheet; you can:
24 | - upload the XML file or manually enter it
25 | - create a new Google Sheet, use an existing one, or get the CSV string
26 | - convert a CSV or Google Sheet table back to an XML file that you can import into pfSense; you can:
27 | - select a Google Sheet, upload a CSV file, or manually enter the CSV
28 |
29 | ## screenshots
30 |
31 |
32 | 
33 | 
34 | 
35 |
36 | ## to use
37 |
38 | You have two options:
39 |
40 | - you can use the one I published at https://script.google.com/macros/s/AKfycby0nVfwX-AKKwhydWfojp0AxlL5zUHdKbRs0iCEZAvziDao8Iou/exec (see [about Google Apps Script, permissions, and security](#about-google-apps-script-permissions-and-security))
41 | - [self-publish your own copy](#self-publish)
42 |
43 | ## how it works
44 |
45 | - the app parses pfSense's **Backup & Restore** XML file into a table
46 | - I recommend exporting your configuration from pfSense and then importing them with this app to see how the data is stored
47 | - then you can copy data from other rows to create new firewall rules
48 |
49 | ## to do
50 |
51 | - download CSV/XML file
52 |
53 | ## about Google Apps Script, permissions, and security
54 |
55 | For those who are not familiar with [Google Apps Script](https://developers.google.com/apps-script/):
56 |
57 | - Google Apps Script web-apps can be published in many ways
58 | - you can read more about this at https://developers.google.com/apps-script/guides/web#permissions
59 | - mine is published with ([screenshot](/screenshots/gas-published-settings.png)):
60 | - **Execute the app as**: `User accessing the web app`
61 | - **Who has access to the app**: `Anyone`
62 | - this means that when you go to the app, it will run under **your Google account**
63 | - this is so the app can read/write Google Sheet files from your account
64 | - I personally cannot access your Google account, **the app can**
65 | - you can see the code yourself to make sure the app is not doing anything nefarious like reading your Google files/emails and sending them to me
66 | - **the first time you go to the app, it will ask you to authorize the app to run as you**
67 | - **if you do not trust me, you can go the [self-publish](#self-publish) route**
68 |
69 | ## self-publish
70 |
71 | These instructions require a little knowledge/understanding of [Google Apps Script](https://www.google.com/script/start/). There are many articles online about it if you get lost.
72 |
73 | 1. Create a new script by going to [https://script.google.com/home/projects/create](https://script.google.com/home/projects/create)
74 | 2. You will need to create 3 files in your project and copy the contents from the files in this repos:
75 | - `web app.gs` => [https://github.com/imthenachoman/pfSense-Firewall-Rules-Manager/blob/main/web%20app.js](https://github.com/imthenachoman/pfSense-Firewall-Rules-Manager/blob/main/web%20app.js)
76 | - `xml rule schemas.gs` => [https://github.com/imthenachoman/pfSense-Firewall-Rules-Manager/blob/main/xml%20rule%20schemas.js](https://github.com/imthenachoman/pfSense-Firewall-Rules-Manager/blob/main/xml%20rule%20schemas.js)
77 | - `index.html` => [https://github.com/imthenachoman/pfSense-Firewall-Rules-Manager/blob/main/index.html](https://github.com/imthenachoman/pfSense-Firewall-Rules-Manager/blob/main/index.html)
78 | 3. Save the project and deploy as a `web app` with these settings:
79 | - `Execute as`: `User accessing the web app`
80 | - `Who has access`: either option
81 | 4. Then go to the URL of your deployed web-app.
82 |
83 | ## disclaimer and warnings
84 |
85 | - Please use at your own discretion. This app comes with no warranty. I am not responsible for anything resulting from this app.
86 | - it is rather dumb in that it is not aware of your pfSense configuration, like what VLANs you have, what your IPs are, etc...; it only knows what it sees in the XML file
87 | - not all pfSense firewall rule settings are included (...yet)
88 |
89 | **Remember to make a backup of your pfSense configuration.**
90 |
91 | ## contact, support, help
92 |
93 | Submit a new issue at https://github.com/imthenachoman/pfSense-Firewall-Rules-Manager/issues/new.
94 |
--------------------------------------------------------------------------------
/appsscript.json:
--------------------------------------------------------------------------------
1 | {
2 | "timeZone": "America/New_York",
3 | "dependencies": {
4 | },
5 | "webapp": {
6 | "access": "ANYONE",
7 | "executeAs": "USER_ACCESSING"
8 | },
9 | "exceptionLogging": "STACKDRIVER",
10 | "runtimeVersion": "V8"
11 | }
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | pfSense FW Rules Manager - CSV/Google Sheet
7 |
156 |
358 |
359 |
360 |
361 | pfSense FW Rules Manager - CSV/Google Sheet
362 | This web-app will let you:
363 |
364 | - convert an XML export of pfSense's firewall rules into a CSV or Google Sheet spreadhseet
365 | - convert a CSV or Google Sheet spreadsheet of firewall rules into an XML file that pfSense can import
366 |
367 |
368 | Please read through https://github.com/imthenachoman/pfSense-Firewall-Rules-Manager for more details, including disclaimer/warnings.
369 |
370 |
371 | This site looks ugly. Front-end is not my thing. If you want to make it prettier, please submit a pull request or contact me.
372 |
373 | Main Event
374 |
380 |
381 |
XML to CSV/Google Sheet
382 |
435 |
436 |
437 |
CSV/Google Sheet to XML
438 |
use with caution
439 |
443 |
494 |
495 |
496 |
497 |
--------------------------------------------------------------------------------
/screenshots/001.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/imthenachoman/pfSense-Firewall-Rules-Manager/7004832824e0ed12819d533be2a120b34b4c7a52/screenshots/001.png
--------------------------------------------------------------------------------
/screenshots/002.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/imthenachoman/pfSense-Firewall-Rules-Manager/7004832824e0ed12819d533be2a120b34b4c7a52/screenshots/002.png
--------------------------------------------------------------------------------
/screenshots/003.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/imthenachoman/pfSense-Firewall-Rules-Manager/7004832824e0ed12819d533be2a120b34b4c7a52/screenshots/003.png
--------------------------------------------------------------------------------
/screenshots/gas-published-settings.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/imthenachoman/pfSense-Firewall-Rules-Manager/7004832824e0ed12819d533be2a120b34b4c7a52/screenshots/gas-published-settings.png
--------------------------------------------------------------------------------
/web app.js:
--------------------------------------------------------------------------------
1 | function doGet(e)
2 | {
3 | // log some data
4 | // Logger.log("e: " + JSON.stringify(e));
5 | // Logger.log("Session.getActiveUser().getEmail(): " + Session.getActiveUser().getEmail());
6 | // Logger.log("Session.getEffectiveUser().getEmail(): " + Session.getEffectiveUser().getEmail());
7 |
8 | // load the HTML file
9 | return HtmlService.createHtmlOutputFromFile("index");
10 | }
11 |
12 | function importXML(formObject)
13 | {
14 | var xml, xmlText, spreadsheet, rootFilterElement, fwRules;
15 | var dataRows = [_allFields_], numFields = _allFields_.length;
16 |
17 | // get the raw XML text
18 | if(formObject.fromSource == "xmlFile")
19 | {
20 | if(!formObject.xmlFile.getContentType().match(/^text\/((plain)|(xml))$/))
21 | {
22 | return {"success": false, "message": "invalid file type: " + formObject.xmlFile.getContentType()};
23 | }
24 | xmlText = formObject.xmlFile.getDataAsString();
25 | }
26 | else
27 | {
28 | xmlText = formObject.xmlText;
29 | }
30 |
31 | // check if the existing sheet exists
32 | if(formObject.destinationType == "googleSheet" && formObject.googleSheetType == "existing")
33 | {
34 | var sheetURL = formObject.existingSheetURL;
35 | var fileID = sheetURL.match(/[-\w]{25,}/);
36 | if(fileID)
37 | {
38 | try
39 | {
40 | spreadsheet = SpreadsheetApp.openById(fileID);
41 | }
42 | catch(e)
43 | {
44 | return {"success": false, "message": "unable to open '" + sheetURL + "'"};
45 | }
46 | }
47 | else
48 | {
49 | return {"success": false, "message": "invalid URL '" + sheetURL + "'"};
50 | }
51 | }
52 |
53 | // try reading the XML
54 | try
55 | {
56 | xml = XmlService.parse(xmlText);
57 | }
58 | catch(e)
59 | {
60 | return {"success": false, "message": "invalid XML"};
61 | }
62 |
63 | rootFilterElement = xml.getRootElement();
64 | if(rootFilterElement.getName() != "filter") rootFilterElement = rootFilterElement.getChild("filter");
65 | if(!rootFilterElement)
66 | {
67 | return {"success": false, "message": "no 'filter' element"};
68 | }
69 |
70 | fwRules = rootFilterElement.getChildren("rule");
71 | if(!fwRules.length)
72 | {
73 | return {"success": false, "message": "no rules"};
74 | }
75 |
76 | for(var i = 0, numRules = fwRules.length; i < numRules; ++i)
77 | {
78 | var fwRule = fwRules[i];
79 | var dataRow = [i + 1];
80 | dataRows.push(dataRow);
81 |
82 | for(var j = 0; j < numFields; ++j)
83 | {
84 | var fieldName = _allFields_[j];
85 | var fieldToPropertyXMLSchemaMapping = _rulePropertyXMLSchema_[fieldName];
86 | dataRow[j] = _xmlElementGet_(fieldToPropertyXMLSchemaMapping, fwRule, i);
87 |
88 | if(formObject.destinationType == "csv" && dataRow[j])
89 | {
90 | dataRow[j] = dataRow[j].toString().replace(/"/g, '""');
91 | }
92 | }
93 | }
94 |
95 | if(formObject.destinationType == "googleSheet")
96 | {
97 | if(formObject.googleSheetType == "new")
98 | {
99 | spreadsheet = SpreadsheetApp.create("pfSense FW RM - " + Utilities.formatDate(new Date(), Session.getScriptTimeZone(), "yyyy-MM-dd_HH:mm:ss"));
100 | }
101 |
102 | var sheet = spreadsheet.insertSheet(Utilities.formatDate(new Date(), Session.getScriptTimeZone(), "yyyy-MM-dd_HH:mm:ss"));
103 |
104 | sheet.deleteColumns(2, sheet.getMaxColumns() - 2);
105 |
106 | sheet.getRange(1, 1, dataRows.length, dataRows[0].length).setValues(dataRows);
107 | sheet.getRange(1, 1, dataRows.length, dataRows[0].length).applyRowBanding(SpreadsheetApp.BandingTheme.CYAN);
108 | sheet.getRange(1, 1, dataRows.length, dataRows[0].length).createFilter();
109 |
110 | sheet.setFrozenRows(1);
111 |
112 | for(var j = 0; j < numFields; ++j)
113 | {
114 | var fieldName = _allFields_[j];
115 | var fieldToPropertyXMLSchemaMapping = _rulePropertyXMLSchema_[fieldName];
116 |
117 | if(fieldToPropertyXMLSchemaMapping.options)
118 | {
119 | sheet.getRange(2, j + 1, sheet.getMaxRows() - 1).setDataValidation(SpreadsheetApp.newDataValidation()
120 | .setAllowInvalid(false)
121 | .setHelpText("Please select a valid option from the drop-down.")
122 | .requireValueInList(fieldToPropertyXMLSchemaMapping.options, true)
123 | .build()
124 | );
125 | }
126 | else if(fieldToPropertyXMLSchemaMapping.type == RULE_PROPERTY_XML_TYPE.BOOL)
127 | {
128 | sheet.getRange(2, j + 1, sheet.getMaxRows() - 1).setDataValidation(SpreadsheetApp.newDataValidation()
129 | .setAllowInvalid(false)
130 | .setHelpText("Please enter TRUE or FALSE.")
131 | .requireCheckbox()
132 | .build()
133 | );
134 | }
135 | }
136 |
137 | SpreadsheetApp.flush();
138 |
139 | return {"success": true, "data": spreadsheet.getUrl() + "#gid=" + sheet.getSheetId()};
140 | }
141 | else
142 | {
143 | for(var i = 0, numRows = dataRows.length; i < numRows; ++i)
144 | {
145 | dataRows[i] = '"' + dataRows[i].join('","') + '"'
146 | }
147 |
148 | return {"success": true, "data": dataRows.join("\n")};
149 | }
150 | }
151 |
152 | function getSheetNames(sheetURL)
153 | {
154 | var spreadsheet;
155 | var fileID = sheetURL.match(/[-\w]{25,}/);
156 | if(fileID)
157 | {
158 | try
159 | {
160 | spreadsheet = SpreadsheetApp.openById(fileID);
161 | }
162 | catch(e)
163 | {
164 | return {"success": false, "message": "unable to open '" + sheetURL + "'"};
165 | }
166 | }
167 | else
168 | {
169 | return {"success": false, "message": "invalid URL '" + sheetURL + "'"};
170 | }
171 |
172 | var sheetNames = [];
173 | var allSheets = spreadsheet.getSheets();
174 | for(var i = 0, numSheets = allSheets.length; i < numSheets; ++i)
175 | {
176 | var sheet = allSheets[i];
177 | if(sheet.getRange(1, 1, 1, _allFields_.length).getDisplayValues()[0].join("|") == _allFields_.join("|"))
178 | {
179 | sheetNames.push(sheet.getName());
180 | }
181 | }
182 |
183 | if(sheetNames.length)
184 | {
185 | return {"success": true, "names": sheetNames};
186 | }
187 | else
188 | {
189 | return {"success": false, "message": "no valid sheets found"};
190 | }
191 | }
192 |
193 | function exportXML(formObject)
194 | {
195 | var dataRows;
196 |
197 | switch(formObject.fromSource)
198 | {
199 | case "googleSheet":
200 | var fileID = formObject.sheetURL.match(/[-\w]{25,}/);
201 | dataRows = SpreadsheetApp.openById(fileID).getSheetByName(formObject.sheetName).getDataRange().getValues();
202 | break;
203 | case "csvFile":
204 | dataRows = Utilities.parseCsv(formObject.csvFile.getDataAsString());
205 | break;
206 | case "csvInput":
207 | dataRows = Utilities.parseCsv(formObject.csvText);
208 | break;
209 | }
210 |
211 | if(dataRows[0].join("|") != _allFields_.join("|"))
212 | {
213 | return {"success": false, "message": "malformed/invalid data"};
214 | }
215 |
216 | var headerRow = dataRows.shift();
217 | var numFields = headerRow.length;
218 | var orderColumn = headerRow.indexOf("Order");
219 |
220 | dataRows.sort((a, b) =>
221 | {
222 | if(a[orderColumn] == b[orderColumn]) return 0;
223 | else return a[orderColumn] < b[orderColumn] ? -1 : 1;
224 | });
225 |
226 | var xmlRoot = XmlService.createElement("filter");
227 |
228 | for(var i = 0, numRows = dataRows.length; i < numRows; ++i)
229 | {
230 | var dataRow = dataRows[i];
231 |
232 | // skip rows where interface is not set
233 | if(!dataRow[orderColumn]) continue;
234 |
235 | var xmlRule = XmlService.createElement("rule");
236 | xmlRoot.addContent(xmlRule);
237 |
238 | for(var j = 0; j < numFields; ++j)
239 | {
240 | var fieldName = headerRow[j];
241 | var fieldToPropertyXMLSchemaMapping = _rulePropertyXMLSchema_[fieldName];
242 |
243 | _xmlElementSet_(fieldToPropertyXMLSchemaMapping, xmlRule, dataRow[j], lookupQuery =>
244 | {
245 | return dataRow[headerRow.indexOf(lookupQuery)];
246 | });
247 | }
248 | }
249 |
250 | return {"success": true, "xml": XmlService.getPrettyFormat().format(XmlService.createDocument(xmlRoot))};
251 | }
--------------------------------------------------------------------------------
/xml rule schemas.js:
--------------------------------------------------------------------------------
1 | const RULE_PROPERTY_XML_TYPE = {
2 | BOOL: "bool",
3 | VALUE: "value",
4 | CHILD: "child",
5 | CDATA: "cdata",
6 | ORDER: "order",
7 | CUSTOM: "custom"
8 | }
9 |
10 | const _rulePropertyXMLSchema_ = {
11 | "Order": {
12 | "path": "",
13 | "type": RULE_PROPERTY_XML_TYPE.ORDER
14 | },
15 | "Action": {
16 | "path": "type",
17 | "options": ["block", "pass", "reject"],
18 | "type": RULE_PROPERTY_XML_TYPE.VALUE,
19 | },
20 | "Disabled": {
21 | "path": "disabled",
22 | "type": RULE_PROPERTY_XML_TYPE.BOOL,
23 | },
24 | "Interface": "interface",
25 | "Address Family": {
26 | "path": "ipprotocol",
27 | "options": ["inet", "inet6", "inet46"],
28 | "type": RULE_PROPERTY_XML_TYPE.VALUE,
29 | },
30 | "Protocol": {
31 | "path": "protocol",
32 | "options": ["any", "tcp", "udp", "tcp/udp", "icmp", "esp", "ah", "gre", "ipv6", "igmp", "pim", "ospf", "sctp", "carp", "pfsync"],
33 | "type": RULE_PROPERTY_XML_TYPE.VALUE,
34 | "default": "any",
35 | },
36 | "ICMP Type": "icmptype",
37 | "Source Type": {
38 | "path": "source",
39 | "options": ["any", "address", "network"],
40 | "type": RULE_PROPERTY_XML_TYPE.CHILD,
41 | },
42 | "Not From Source": {
43 | "path": "source/not",
44 | "type": RULE_PROPERTY_XML_TYPE.BOOL,
45 | },
46 | "Source": {
47 | "path": "source/{Source Type}",
48 | "type": RULE_PROPERTY_XML_TYPE.VALUE,
49 | },
50 | "Source Port": "source/port",
51 | "Destination Type": {
52 | "path": "destination",
53 | "options": ["any", "address", "network"],
54 | "type": RULE_PROPERTY_XML_TYPE.CHILD,
55 | },
56 | "Not To Destination": {
57 | "path": "destination/not",
58 | "type": RULE_PROPERTY_XML_TYPE.BOOL,
59 | },
60 | "Destination": {
61 | "path": "destination/{Destination Type}",
62 | "type": RULE_PROPERTY_XML_TYPE.VALUE,
63 | },
64 | "Destination Port": "destination/port",
65 | "Log": {
66 | "path": "log",
67 | "type": RULE_PROPERTY_XML_TYPE.BOOL,
68 | },
69 | "Description": {
70 | "path": "descr",
71 | "type": RULE_PROPERTY_XML_TYPE.CDATA,
72 | },
73 | "Tracking ID": "tracker",
74 | "Associated Rule ID": "associated-rule-id",
75 | "ID": "id",
76 | "Tag": "tag",
77 | "Tagged": "tagged",
78 | "Max": "max",
79 | "Max Source Nodes": "max-src-nodes",
80 | "Max Source Connections": "max-src-conn",
81 | "Max Source States": "max-src-states",
82 | "State Timeout": "statetimeout",
83 | "State Type": {
84 | "path": "statetype",
85 | "type": RULE_PROPERTY_XML_TYPE.CDATA,
86 | },
87 | "OS": "os",
88 | }
89 |
90 | const _allFields_ = Object.keys(_rulePropertyXMLSchema_);
91 |
92 | function _xmlElementGet_(ruleSchema, parentElement, index)
93 | {
94 | if(typeof ruleSchema == "string")
95 | {
96 | ruleSchema = {
97 | "path": ruleSchema,
98 | "type": RULE_PROPERTY_XML_TYPE.VALUE,
99 | }
100 | }
101 |
102 | var value = ruleSchema.default;
103 | var element = parentElement;
104 | var elementPath = ruleSchema.path.split("/");
105 |
106 | for(var i = 0, numInElementPath = elementPath.length; i < numInElementPath; ++i)
107 | {
108 | if(lookupMatch = elementPath[i].match(/^{(.+?)}$/))
109 | {
110 | elementPath[i] = _xmlElementGet_(_rulePropertyXMLSchema_[lookupMatch[1]], parentElement);
111 | }
112 |
113 | element = element.getChild(elementPath[i]);
114 | }
115 |
116 | switch(ruleSchema.type)
117 | {
118 | case "bool":
119 | value = element ? true : false;
120 | break;
121 | case "order":
122 | value = index + 1;
123 | break;
124 | case "child":
125 | var childOptions = ruleSchema.options;
126 | for(var i = 0, numChildOptions = childOptions.length; i < numChildOptions; ++i)
127 | {
128 | if(element.getChild(childOptions[i]))
129 | {
130 | value = childOptions[i];
131 | break;
132 | }
133 | }
134 | break;
135 | default:
136 | if(element)
137 | {
138 | value = element.getValue();
139 | }
140 | }
141 |
142 | return value;
143 | }
144 |
145 | function _xmlElementSet_(ruleSchema, parentElement, value, valueLookup)
146 | {
147 | // if(!value) return;
148 |
149 | if(typeof ruleSchema == "string")
150 | {
151 | ruleSchema = {
152 | "path": ruleSchema,
153 | "type": RULE_PROPERTY_XML_TYPE.VALUE,
154 | }
155 | }
156 |
157 | // for boolean types, stop if value is 0 or false
158 | if(ruleSchema.type == RULE_PROPERTY_XML_TYPE.BOOL && (!value || (value.match && value.match(/0|false/i)))) return;
159 |
160 | var element = parentElement;
161 | var elementPath = ruleSchema.path.split("/");
162 |
163 | for(var i = 0, numInElementPath = elementPath.length; i < numInElementPath; ++i)
164 | {
165 | if(!elementPath[i]) continue;
166 |
167 | if(lookupMatch = elementPath[i].match(/^{(.+?)}$/))
168 | {
169 | elementPath[i] = valueLookup(lookupMatch[1]);
170 | }
171 |
172 | if(element.getChild(elementPath[i]))
173 | {
174 | element = element.getChild(elementPath[i]);
175 | }
176 | else
177 | {
178 | var child = XmlService.createElement(elementPath[i]);
179 | element.addContent(child);
180 | element = child;
181 | }
182 | }
183 |
184 | switch(ruleSchema.type)
185 | {
186 | case "bool":
187 | // don't need to do anything for bool
188 | // if value is false then this function would have stopped earlier and the element would never have been created
189 | break;
190 | case "order":
191 | // no need to do anything for order
192 | break;
193 | case "child":
194 | if(!element.getChild(value))
195 | {
196 | var child = XmlService.createElement(value);
197 | element.addContent(child);
198 | }
199 | break;
200 | case "cdata":
201 | var child = XmlService.createCdata(value);
202 | element.addContent(child);
203 | break;
204 | default:
205 | element.setText(value);
206 |
207 | }
208 | }
--------------------------------------------------------------------------------