├── .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 | ![001](/screenshots/001.png) 33 | ![002](/screenshots/002.png) 34 | ![003](/screenshots/003.png) 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 | 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 | 375 | 376 | 377 | 378 | 379 | 380 |
381 |

XML to CSV/Google Sheet

382 |
383 | 384 | 385 | 386 | 390 | 391 | 392 | 393 | 396 | 397 | 398 | 399 | 402 | 403 | 404 | 405 | 409 | 410 | 411 | 412 | 416 | 417 | 418 | 419 | 422 | 423 | 424 | 425 | 428 | 429 | 430 | 431 | 432 | 433 |
from 387 |
388 |
389 |
select XML file 394 | 395 |
enter XML 400 | 401 |
destination type 406 |
407 |
408 |
Google Sheet type 413 |
414 |
415 |
existing Google Sheet sheet URL 420 | 421 |
submit 426 | 427 |
log
434 |
435 |
436 |
437 |

CSV/Google Sheet to XML

438 |

use with caution

439 | 443 |
444 | 445 | 446 | 447 | 452 | 453 | 454 | 455 | 469 | 470 | 471 | 472 | 475 | 476 | 477 | 478 | 481 | 482 | 483 | 484 | 487 | 488 | 489 | 490 | 491 | 492 |
from 448 |
449 |
450 |
451 |
Google Sheet 456 | 457 | 458 | 459 | 462 | 463 | 464 | 465 | 466 | 467 |
URL 460 | 461 |
sheet
468 |
select CSV file 473 | 474 |
enter CSV 479 | 480 |
submit 485 | 486 |
log
493 |
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 | } --------------------------------------------------------------------------------