fhirsmith 0.4.2 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +1 -1
  3. package/library/cron-utilities.js +136 -0
  4. package/library/html-server.js +13 -29
  5. package/library/html.js +3 -8
  6. package/library/languages.js +160 -37
  7. package/library/package-manager.js +48 -1
  8. package/library/utilities.js +100 -19
  9. package/package.json +2 -2
  10. package/packages/package-crawler.js +6 -1
  11. package/packages/packages.js +38 -54
  12. package/publisher/publisher.js +19 -27
  13. package/registry/api.js +11 -10
  14. package/registry/crawler.js +31 -29
  15. package/registry/model.js +5 -26
  16. package/registry/registry.js +32 -41
  17. package/server.js +53 -5
  18. package/shl/shl.js +0 -18
  19. package/static/assets/js/statuspage.js +1 -9
  20. package/stats.js +39 -1
  21. package/token/token.js +14 -9
  22. package/translations/Messages.properties +2 -1
  23. package/tx/README.md +17 -6
  24. package/tx/cs/cs-api.js +19 -1
  25. package/tx/cs/cs-base.js +77 -0
  26. package/tx/cs/cs-country.js +46 -0
  27. package/tx/cs/cs-cpt.js +9 -5
  28. package/tx/cs/cs-cs.js +27 -13
  29. package/tx/cs/cs-lang.js +60 -22
  30. package/tx/cs/cs-loinc.js +69 -98
  31. package/tx/cs/cs-mimetypes.js +4 -0
  32. package/tx/cs/cs-ndc.js +6 -0
  33. package/tx/cs/cs-omop.js +16 -15
  34. package/tx/cs/cs-rxnorm.js +23 -1
  35. package/tx/cs/cs-snomed.js +283 -40
  36. package/tx/cs/cs-ucum.js +90 -70
  37. package/tx/importers/import-sct.module.js +371 -35
  38. package/tx/importers/readme.md +117 -7
  39. package/tx/library/bundle.js +5 -0
  40. package/tx/library/capabilitystatement.js +3 -142
  41. package/tx/library/codesystem.js +19 -173
  42. package/tx/library/conceptmap.js +4 -218
  43. package/tx/library/designations.js +14 -1
  44. package/tx/library/extensions.js +7 -0
  45. package/tx/library/namingsystem.js +3 -89
  46. package/tx/library/operation-outcome.js +8 -3
  47. package/tx/library/parameters.js +3 -2
  48. package/tx/library/renderer.js +10 -6
  49. package/tx/library/terminologycapabilities.js +3 -243
  50. package/tx/library/valueset.js +3 -235
  51. package/tx/library.js +100 -13
  52. package/tx/operation-context.js +23 -4
  53. package/tx/params.js +35 -38
  54. package/tx/provider.js +6 -5
  55. package/tx/sct/expressions.js +12 -3
  56. package/tx/tx-html.js +80 -89
  57. package/tx/tx.fhir.org.yml +6 -5
  58. package/tx/tx.js +163 -13
  59. package/tx/vs/vs-database.js +56 -39
  60. package/tx/vs/vs-package.js +21 -2
  61. package/tx/vs/vs-vsac.js +175 -39
  62. package/tx/workers/batch-validate.js +2 -0
  63. package/tx/workers/batch.js +2 -0
  64. package/tx/workers/expand.js +132 -112
  65. package/tx/workers/lookup.js +33 -14
  66. package/tx/workers/metadata.js +2 -2
  67. package/tx/workers/read.js +3 -2
  68. package/tx/workers/related.js +574 -0
  69. package/tx/workers/search.js +46 -9
  70. package/tx/workers/subsumes.js +13 -3
  71. package/tx/workers/translate.js +7 -3
  72. package/tx/workers/validate.js +258 -285
  73. package/tx/workers/worker.js +43 -39
  74. package/tx/xml/bundle-xml.js +237 -0
  75. package/tx/xml/xml-base.js +215 -64
  76. package/tx/xversion/xv-bundle.js +71 -0
  77. package/tx/xversion/xv-capabiliityStatement.js +137 -0
  78. package/tx/xversion/xv-codesystem.js +169 -0
  79. package/tx/xversion/xv-conceptmap.js +224 -0
  80. package/tx/xversion/xv-namingsystem.js +88 -0
  81. package/tx/xversion/xv-operationoutcome.js +27 -0
  82. package/tx/xversion/xv-parameters.js +87 -0
  83. package/tx/xversion/xv-resource.js +45 -0
  84. package/tx/xversion/xv-terminologyCapabilities.js +214 -0
  85. package/tx/xversion/xv-valueset.js +234 -0
  86. package/utilities/dev-proxy-server.js +126 -0
  87. package/utilities/explode-results.js +58 -0
  88. package/utilities/split-by-system.js +198 -0
  89. package/utilities/vsac-cs-fetcher.js +0 -0
  90. package/{windows-install.js → utilities/windows-install.js} +2 -0
  91. package/vcl/vcl.js +0 -18
  92. package/xig/xig.js +108 -99
@@ -25,11 +25,42 @@ const Utilities = {
25
25
  return isNaN(num) ? defaultValue : num;
26
26
  },
27
27
  parseFloatOrDefault(value, defaultValue) {
28
- const num = parseFloat(value);
29
- return isNaN(num) ? defaultValue : num;
28
+ const num = parseFloat(value);
29
+ return isNaN(num) ? defaultValue : num;
30
30
 
31
31
 
32
- }
32
+ },
33
+
34
+ /**
35
+ * Format the difference between two Date.now() timestamps for human reading
36
+ * @param {number} start - earlier timestamp (from Date.now())
37
+ * @param {number} end - later timestamp (from Date.now())
38
+ * @returns {string} formatted duration
39
+ */
40
+ formatDuration(start, end) {
41
+ let ms = Math.abs(end - start);
42
+
43
+ if (ms < 1000) return `${ms}ms`;
44
+
45
+ const days = Math.floor(ms / 86400000);
46
+ ms %= 86400000;
47
+ const hours = Math.floor(ms / 3600000);
48
+ ms %= 3600000;
49
+ const minutes = Math.floor(ms / 60000);
50
+ ms %= 60000;
51
+ const seconds = Math.floor(ms / 1000);
52
+ ms %= 1000;
53
+
54
+ const parts = [];
55
+ if (days) parts.push(`${days}d`);
56
+ if (hours) parts.push(`${hours}h`);
57
+ if (minutes) parts.push(`${minutes}m`);
58
+ if (seconds || ms) {
59
+ parts.push(ms ? `${seconds}.${String(ms).padStart(3, '0')}s` : `${seconds}s`);
60
+ }
61
+
62
+ return parts.join(' ');
63
+ }
33
64
 
34
65
  };
35
66
 
@@ -172,25 +203,75 @@ function isAbsoluteUrl(s) {
172
203
  }
173
204
 
174
205
  /**
175
- * Escape HTML special characters
206
+ * This class takes two lists, and matches between the lists, producing three new lists:
207
+ * * items that are in both
208
+ * * items that only in left
209
+ * * items that are only in right
210
+ *
211
+ * You have to give it a match function that is called asynchronously
212
+ *
213
+ * examples of use:
214
+ *
215
+ * const matcher = new ArrayMatcher((l, r) =>
216
+ * this.filtersMatch(localstatus, cs, l, r)
217
+ * );
218
+ * await matcher.match(leftArray, rightArray);
219
+ *
220
+ * // Use the results
221
+ * for (const { left, right } of matcher.matched) { ... }
222
+ * for (const item of matcher.unmatchedLeft) { ... }
223
+ * for (const item of matcher.unmatchedRight) { ... }
224
+ *
225
+ * // or
226
+ * const matcher2 = new ArrayMatcher((l, r) =>
227
+ * this.compareProperties(system, version, l, r)
228
+ * );
229
+ * await matcher2.match(propsA, propsB);
230
+ *
176
231
  */
177
- function escapeHtml(text) {
178
- if (text === null || text === undefined) {
179
- return '';
180
- }
181
- if (typeof text !== 'string') {
182
- return String(text);
232
+ class ArrayMatcher {
233
+ constructor(matchFn) {
234
+ this.matchFn = matchFn;
235
+ this.matched = [];
236
+ this.unmatchedLeft = [];
237
+ this.unmatchedRight = [];
183
238
  }
184
239
 
185
- const map = {
186
- '&': '&amp;',
187
- '<': '&lt;',
188
- '>': '&gt;',
189
- '"': '&quot;',
190
- "'": '&#x27;'
191
- };
240
+ /**
241
+ *
242
+ * @param left an array of items (or null/undefined)
243
+ * @param right an array of items (or null/undefined)
244
+ * @returns {Promise<ArrayMatcher>}
245
+ */
246
+ async match(left, right) {
247
+ if (!left) {
248
+ left = [];
249
+ }
250
+ if (!right) {
251
+ right = [];
252
+ }
253
+
254
+ this.matched = [];
255
+ this.unmatchedRight = [...right];
256
+
257
+ for (const l of left) {
258
+ let idx = -1;
259
+ for (let i = 0; i < this.unmatchedRight.length; i++) {
260
+ if (await this.matchFn(l, this.unmatchedRight[i])) {
261
+ idx = i;
262
+ break;
263
+ }
264
+ }
265
+ if (idx !== -1) {
266
+ this.matched.push({ left: l, right: this.unmatchedRight[idx] });
267
+ this.unmatchedRight.splice(idx, 1);
268
+ } else {
269
+ this.unmatchedLeft.push(l);
270
+ }
271
+ }
192
272
 
193
- return text.replace(/[&<>"']/g, m => map[m]);
273
+ return this;
274
+ }
194
275
  }
195
276
 
196
- module.exports = { Utilities, validateParameter, validateOptionalParameter, validateArrayParameter, validateResource, strToBool, getValuePrimitive, getValueDT, getValueName, isAbsoluteUrl, escapeHtml };
277
+ module.exports = { Utilities, ArrayMatcher, validateParameter, validateOptionalParameter, validateArrayParameter, validateResource, strToBool, getValuePrimitive, getValueDT, getValueName, isAbsoluteUrl };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fhirsmith",
3
- "version": "0.4.2",
3
+ "version": "0.5.0",
4
4
  "description": "A Node.js server that provides a collection of tools to serve the FHIR ecosystem",
5
5
  "main": "server.js",
6
6
  "engines": {
@@ -44,7 +44,7 @@
44
44
  "express-rate-limit": "^7.4.1",
45
45
  "express-session": "^1.19.0",
46
46
  "fast-xml-parser": "^5.3.4",
47
- "fhir-validator-wrapper": "1.2.1",
47
+ "fhir-validator-wrapper": "1.2.2",
48
48
  "fhirpath": "^4.8.3",
49
49
  "fs-extra": "^11.3.3",
50
50
  "inquirer": "^8.2.5",
@@ -13,9 +13,10 @@ const path = require('path');
13
13
  class PackageCrawler {
14
14
  log;
15
15
 
16
- constructor(config, db) {
16
+ constructor(config, db, stats) {
17
17
  this.config = config;
18
18
  this.db = db;
19
+ this.stats = stats;
19
20
  this.totalBytes = 0;
20
21
  this.crawlerLog = {};
21
22
  this.errors = '';
@@ -36,6 +37,7 @@ class PackageCrawler {
36
37
  };
37
38
 
38
39
  this.log.info('Running web crawler for packages using master URL: '+ this.config.masterUrl);
40
+ this.stats.task('Package Crawler', 'Running');
39
41
 
40
42
  try {
41
43
  // Fetch the master JSON file
@@ -54,6 +56,7 @@ class PackageCrawler {
54
56
  this.log.info('Skipping feed with no URL: '+ feedConfig);
55
57
  continue;
56
58
  }
59
+ this.stats.task('Package Crawler', 'Running for '+feedConfig.url);
57
60
 
58
61
  try {
59
62
  await this.updateTheFeed(
@@ -76,6 +79,7 @@ class PackageCrawler {
76
79
  this.log.info(`Web crawler completed successfully in ${runTime}ms`);
77
80
  this.log.info(`Total bytes processed: ${this.totalBytes}`);
78
81
 
82
+ this.stats.task('Package Crawler', 'Complete');
79
83
  return this.crawlerLog;
80
84
 
81
85
  } catch (error) {
@@ -83,6 +87,7 @@ class PackageCrawler {
83
87
  this.crawlerLog.runTime = `${runTime}ms`;
84
88
  this.crawlerLog.fatalException = error.message;
85
89
  this.crawlerLog.endTime = new Date().toISOString();
90
+ this.stats.task('Package Crawler', 'Error: '+error.message);
86
91
 
87
92
  this.log.error('Web crawler failed: '+ error);
88
93
  throw error;
@@ -12,9 +12,10 @@ const fs = require('fs');
12
12
  const PackageCrawler = require('./package-crawler.js');
13
13
  const htmlServer = require('../library/html-server');
14
14
  const folders = require('../library/folder-setup');
15
-
15
+ const escape = require('escape-html');
16
16
  const Logger = require('../library/logger');
17
17
  const {validateParameter} = require("../library/utilities");
18
+ const {describeCron} = require("../library/cron-utilities");
18
19
  const pckLog = Logger.getInstance().child({ module: 'packages' });
19
20
 
20
21
  class PackagesModule {
@@ -125,24 +126,6 @@ class PackagesModule {
125
126
  };
126
127
  }
127
128
 
128
- // Enhanced HTML escaping
129
- escapeHtml(str) {
130
- if (!str || typeof str !== 'string') return '';
131
-
132
- const escapeMap = {
133
- '&': '&amp;',
134
- '<': '&lt;',
135
- '>': '&gt;',
136
- '"': '&quot;',
137
- "'": '&#x27;',
138
- '/': '&#x2F;',
139
- '`': '&#x60;',
140
- '=': '&#x3D;'
141
- };
142
-
143
- return str.replace(/[&<>"'`=/]/g, (match) => escapeMap[match]);
144
- }
145
-
146
129
  buildSecureQuery(baseQuery, conditions = []) {
147
130
  let query = baseQuery;
148
131
  const params = [];
@@ -572,7 +555,7 @@ class PackagesModule {
572
555
  await this.ensureMirrorDirectory();
573
556
 
574
557
  // Initialize the crawler
575
- this.crawler = new PackageCrawler(this.config, this.db);
558
+ this.crawler = new PackageCrawler(this.config, this.db, this.stats);
576
559
 
577
560
  // Start the hourly web crawler if enabled
578
561
  if (config.crawler.enabled) {
@@ -804,6 +787,7 @@ class PackagesModule {
804
787
 
805
788
  startCrawlerJob() {
806
789
  if (this.config.crawler && this.config.crawler.schedule) {
790
+ this.stats.addTask("Package Crawler", describeCron(this.config.crawler.schedule));
807
791
  this.crawlerJob = cron.schedule(this.config.crawler.schedule, async () => {
808
792
  pckLog.info('Starting scheduled package crawler...');
809
793
  try {
@@ -1445,12 +1429,12 @@ class PackagesModule {
1445
1429
 
1446
1430
  for (const pkg of updates) {
1447
1431
  table += '<tr>';
1448
- table += `<td><a href="${this.escapeHtml(pkg.url)}">${this.escapeHtml(pkg.name)}</a></td>`;
1449
- table += `<td>${this.escapeHtml(pkg.version)}</td>`;
1450
- table += `<td>${this.escapeHtml(pkg.fhirVersion)}</td>`;
1451
- table += `<td>${this.escapeHtml(pkg.kind)}</td>`;
1432
+ table += `<td><a href="${escape(pkg.url)}">${escape(pkg.name)}</a></td>`;
1433
+ table += `<td>${escape(pkg.version)}</td>`;
1434
+ table += `<td>${escape(pkg.fhirVersion)}</td>`;
1435
+ table += `<td>${escape(pkg.kind)}</td>`;
1452
1436
  table += `<td>${new Date(pkg.date).toLocaleDateString()} ${new Date(pkg.date).toLocaleTimeString()}</td>`;
1453
- table += `<td>${this.escapeHtml(pkg.canonical || '')}</td>`;
1437
+ table += `<td>${escape(pkg.canonical || '')}</td>`;
1454
1438
  table += '</tr>';
1455
1439
  }
1456
1440
 
@@ -1899,12 +1883,12 @@ class PackagesModule {
1899
1883
 
1900
1884
  for (const pv of sortedVersions) {
1901
1885
  table += '<tr>';
1902
- table += `<td title="${this.escapeHtml(pv.GUID)}"><strong>${this.escapeHtml(pv.Version)}</strong></td>`;
1903
- table += `<td>${this.escapeHtml(this.interpretVersion(pv.FhirVersions))}</td>`;
1904
- table += `<td>${this.escapeHtml(this.codeForKind(pv.Kind))}</td>`;
1886
+ table += `<td title="${escape(pv.GUID)}"><strong>${escape(pv.Version)}</strong></td>`;
1887
+ table += `<td>${escape(this.interpretVersion(pv.FhirVersions))}</td>`;
1888
+ table += `<td>${escape(this.codeForKind(pv.Kind))}</td>`;
1905
1889
  table += `<td>${new Date(pv.PubDate).toLocaleDateString()}</td>`;
1906
1890
  table += `<td>${(pv.DownloadCount || 0).toLocaleString()}</td>`;
1907
- table += `<td><a href="/packages/${this.escapeHtml(id)}/${this.escapeHtml(pv.Version)}" class="btn btn-sm btn-primary">Download</a></td>`;
1891
+ table += `<td><a href="/packages/${escape(id)}/${escape(pv.Version)}" class="btn btn-sm btn-primary">Download</a></td>`;
1908
1892
  table += '</tr>';
1909
1893
  }
1910
1894
 
@@ -1949,7 +1933,7 @@ class PackagesModule {
1949
1933
  let content = '<div class="row mb-4">';
1950
1934
  content += '<div class="col-12">';
1951
1935
  content += '<table class="grid">';
1952
- content += `<tr><td>Package ID:</td><td>${this.escapeHtml(id)}</td></tr>`;
1936
+ content += `<tr><td>Package ID:</td><td>${escape(id)}</td></tr>`;
1953
1937
  content += `<tr><td>Description</td><td>${vars.desc}</td></tr>`;
1954
1938
  content += `<tr><td>Total Versions:</td><td>${vars.count}</td></tr>`;
1955
1939
  content += `<tr><td>Total Downloads:</td><td>${vars.downloads}</td></tr>`;
@@ -1969,7 +1953,7 @@ class PackagesModule {
1969
1953
  formatTextToHTML(text) {
1970
1954
  if (!text) return '';
1971
1955
  // Basic text to HTML formatting - convert newlines to <br>
1972
- return this.escapeHtml(text).replace(/\n/g, '<br>');
1956
+ return escape(text).replace(/\n/g, '<br>');
1973
1957
  }
1974
1958
 
1975
1959
  async serveSearch(req, res) {
@@ -2142,13 +2126,13 @@ class PackagesModule {
2142
2126
 
2143
2127
  for (const pkg of results) {
2144
2128
  table += '<tr>';
2145
- table += `<td><a href="${this.escapeHtml(pkg.url)}">${this.escapeHtml(pkg.name)}</a></td>`;
2146
- table += `<td>${this.escapeHtml(pkg.version)} (<a href="/packages/${this.escapeHtml(pkg.name)}">all</a>)</td>`;
2147
- table += `<td>${this.escapeHtml(pkg.fhirVersion)}</td>`;
2148
- table += `<td>${this.escapeHtml(pkg.kind)}</td>`;
2129
+ table += `<td><a href="${escape(pkg.url)}">${escape(pkg.name)}</a></td>`;
2130
+ table += `<td>${escape(pkg.version)} (<a href="/packages/${escape(pkg.name)}">all</a>)</td>`;
2131
+ table += `<td>${escape(pkg.fhirVersion)}</td>`;
2132
+ table += `<td>${escape(pkg.kind)}</td>`;
2149
2133
  table += `<td>${pkg.date ? new Date(pkg.date).toLocaleDateString() : 'N/A'}</td>`;
2150
2134
  table += `<td>${pkg.count ? pkg.count.toLocaleString() : 'N/A'}</td>`;
2151
- table += `<td>${this.escapeHtml(pkg.canonical || '')}</td>`;
2135
+ table += `<td>${escape(pkg.canonical || '')}</td>`;
2152
2136
  table += '</tr>';
2153
2137
  }
2154
2138
 
@@ -2202,22 +2186,22 @@ class PackagesModule {
2202
2186
 
2203
2187
  content += '<tr>';
2204
2188
  content += '<td>Id</td>';
2205
- content += `<td><input type="text" name="name" value="${this.escapeHtml(vars.name)}"></td>`;
2189
+ content += `<td><input type="text" name="name" value="${escape(vars.name)}"></td>`;
2206
2190
  content += '</tr>';
2207
2191
 
2208
2192
  content += '<tr>';
2209
2193
  content += '<td>Depends On</td>';
2210
- content += `<td><input type="text" name="dependson" value="${this.escapeHtml(vars.dependson)}"> <i>includes both direct and indirect dependencies</i></td>`;
2194
+ content += `<td><input type="text" name="dependson" value="${escape(vars.dependson)}"> <i>includes both direct and indirect dependencies</i></td>`;
2211
2195
  content += '</tr>';
2212
2196
 
2213
2197
  content += '<tr>';
2214
2198
  content += '<td>Canonical (Package)</td>';
2215
- content += `<td><input type="text" name="pkgcanonical" value="${this.escapeHtml(vars.canonicalPkg)}"></td>`;
2199
+ content += `<td><input type="text" name="pkgcanonical" value="${escape(vars.canonicalPkg)}"></td>`;
2216
2200
  content += '</tr>';
2217
2201
 
2218
2202
  content += '<tr>';
2219
2203
  content += '<td>Canonical (Resource)</td>';
2220
- content += `<td><input type="text" name="canonical" value="${this.escapeHtml(vars.canonicalUrl)}"></td>`;
2204
+ content += `<td><input type="text" name="canonical" value="${escape(vars.canonicalUrl)}"></td>`;
2221
2205
  content += '</tr>';
2222
2206
 
2223
2207
  content += '<tr>';
@@ -2368,10 +2352,10 @@ class PackagesModule {
2368
2352
  <h1>FHIR Package Search</h1>
2369
2353
 
2370
2354
  <form class="search-form" method="GET">
2371
- <input type="text" name="name" placeholder="Package name" value="${this.escapeHtml(name)}">
2372
- <input type="text" name="dependson" placeholder="Depends on" value="${this.escapeHtml(dependson)}">
2373
- <input type="text" name="canonicalPkg" placeholder="Canonical package" value="${this.escapeHtml(canonicalPkg)}">
2374
- <input type="text" name="canonicalUrl" placeholder="Canonical URL" value="${this.escapeHtml(canonicalUrl)}">
2355
+ <input type="text" name="name" placeholder="Package name" value="${escape(name)}">
2356
+ <input type="text" name="dependson" placeholder="Depends on" value="${escape(dependson)}">
2357
+ <input type="text" name="canonicalPkg" placeholder="Canonical package" value="${escape(canonicalPkg)}">
2358
+ <input type="text" name="canonicalUrl" placeholder="Canonical URL" value="${escape(canonicalUrl)}">
2375
2359
  <select name="fhirVersion">
2376
2360
  <option value="">Any FHIR version</option>
2377
2361
  <option value="R2" ${fhirVersion === 'R2' ? 'selected' : ''}>R2</option>
@@ -2387,13 +2371,13 @@ class PackagesModule {
2387
2371
  ${results.map(pkg => `
2388
2372
  <div class="package">
2389
2373
  <div class="package-name">
2390
- <a href="${pkg.url}">${this.escapeHtml(pkg.name)}</a> v${this.escapeHtml(pkg.version)}
2374
+ <a href="${pkg.url}">${escape(pkg.name)}</a> v${escape(pkg.version)}
2391
2375
  </div>
2392
2376
  <div class="package-details">
2393
- <strong>FHIR Version:</strong> ${this.escapeHtml(pkg.fhirVersion)}<br>
2394
- <strong>Type:</strong> ${this.escapeHtml(pkg.kind)}<br>
2395
- <strong>Canonical:</strong> ${this.escapeHtml(pkg.canonical)}<br>
2396
- ${pkg.description ? `<strong>Description:</strong> ${this.escapeHtml(pkg.description)}<br>` : ''}
2377
+ <strong>FHIR Version:</strong> ${escape(pkg.fhirVersion)}<br>
2378
+ <strong>Type:</strong> ${escape(pkg.kind)}<br>
2379
+ <strong>Canonical:</strong> ${escape(pkg.canonical)}<br>
2380
+ ${pkg.description ? `<strong>Description:</strong> ${escape(pkg.description)}<br>` : ''}
2397
2381
  ${pkg.date ? `<strong>Published:</strong> ${new Date(pkg.date).toLocaleDateString()}<br>` : ''}
2398
2382
  ${pkg.count ? `<strong>Downloads:</strong> ${pkg.count}<br>` : ''}
2399
2383
  </div>
@@ -2593,14 +2577,14 @@ class PackagesModule {
2593
2577
  for (const source of sourcePackages) {
2594
2578
  const dependencies = brokenDependencies[source];
2595
2579
  table += '<tr>';
2596
- table += `<td>${this.escapeHtml(source)}</td>`;
2580
+ table += `<td>${escape(source)}</td>`;
2597
2581
  table += '<td>';
2598
2582
 
2599
2583
  for (let i = 0; i < dependencies.length; i++) {
2600
2584
  if (i > 0) {
2601
2585
  table += ', ';
2602
2586
  }
2603
- table += this.escapeHtml(dependencies[i]);
2587
+ table += escape(dependencies[i]);
2604
2588
  }
2605
2589
 
2606
2590
  table += '</td>';
@@ -2656,7 +2640,7 @@ class PackagesModule {
2656
2640
  buildLogPageContent(status, logData, summary) {
2657
2641
  let content = '<div class="row mb-4">';
2658
2642
  content += '<div class="col-12">';
2659
- content += `<div class="alert ${this.crawlerRunning ? 'alert-info' : 'alert-secondary'}">${this.escapeHtml(status)}</div>`;
2643
+ content += `<div class="alert ${this.crawlerRunning ? 'alert-info' : 'alert-secondary'}">${escape(status)}</div>`;
2660
2644
  content += '</div>';
2661
2645
  content += '</div>';
2662
2646
 
@@ -2752,7 +2736,7 @@ class PackagesModule {
2752
2736
  output += 'No feeds processed.\n';
2753
2737
  }
2754
2738
 
2755
- return this.escapeHtml(output);
2739
+ return escape(output);
2756
2740
  }
2757
2741
 
2758
2742
  getStatus() {
@@ -2825,7 +2809,7 @@ class PackagesModule {
2825
2809
  if (this.lastRunTime) {
2826
2810
  content += `<tr><td>Last Run</td><td>${new Date(this.lastRunTime).toLocaleString()}</td></tr>`;
2827
2811
  }
2828
- content += `<tr><td>Master URL</td><td><a href="${htmlServer.escapeHtml(this.config.masterUrl)}" target="_blank">${htmlServer.escapeHtml(this.config.masterUrl)}</a></td></tr>`;
2812
+ content += `<tr><td>Master URL</td><td><a href="${escape(this.config.masterUrl)}" target="_blank">${escape(this.config.masterUrl)}</a></td></tr>`;
2829
2813
  content += '</table>';
2830
2814
  content += '</div>';
2831
2815
  content += '</div>';
@@ -5,6 +5,7 @@ const Database = require('sqlite3').Database;
5
5
  const bcrypt = require('bcrypt');
6
6
  const session = require('express-session');
7
7
  const folders = require('../library/folder-setup');
8
+ const escape = require('escape-html');
8
9
 
9
10
 
10
11
  class PublisherModule {
@@ -1096,7 +1097,7 @@ class PublisherModule {
1096
1097
  content += '<a href="/publisher/tasks/' + task.id + '/output" class="btn btn-sm btn-outline-info me-1">View Output</a>';
1097
1098
  }
1098
1099
  if (task.failure_reason) {
1099
- content += '<span class="text-danger small me-1">' + this.escapeHtml(task.failure_reason) + '</span>';
1100
+ content += '<span class="text-danger small me-1">' + escape(task.failure_reason) + '</span>';
1100
1101
  }
1101
1102
  }
1102
1103
 
@@ -1343,7 +1344,7 @@ class PublisherModule {
1343
1344
  // Build log section
1344
1345
  if (buildLog) {
1345
1346
  content += '<h4>Build Log</h4>';
1346
- content += '<div class="output-viewer">' + this.escapeHtml(buildLog) + '</div>';
1347
+ content += '<div class="output-viewer">' + escape(buildLog) + '</div>';
1347
1348
  } else if (task.status === 'building') {
1348
1349
  content += '<h4>Build Log</h4>';
1349
1350
  content += '<p><em>Build in progress... Log will appear when available.</em></p>';
@@ -1424,28 +1425,28 @@ class PublisherModule {
1424
1425
 
1425
1426
  const htmlServer = require('../library/html-server');
1426
1427
 
1427
- let content = '<h3>Task History: #' + task.id + ' — ' + this.escapeHtml(task.npm_package_id) + '#' + this.escapeHtml(task.version) + '</h3>';
1428
+ let content = '<h3>Task History: #' + task.id + ' — ' + escape(task.npm_package_id) + '#' + escape(task.version) + '</h3>';
1428
1429
 
1429
1430
  // Task details summary card
1430
1431
  content += '<div class="card mb-4"><div class="card-body">';
1431
1432
  content += '<div class="row">';
1432
1433
  content += '<div class="col-md-6">';
1433
1434
  content += '<p><strong>Status:</strong> <span class="badge bg-' + this.getStatusColor(task.status) + '">' + task.status + '</span></p>';
1434
- content += '<p><strong>Package:</strong> <code>' + this.escapeHtml(task.npm_package_id) + '</code></p>';
1435
- content += '<p><strong>Version:</strong> ' + this.escapeHtml(task.version) + '</p>';
1436
- content += '<p><strong>Website:</strong> ' + this.escapeHtml(task.website_name) + '</p>';
1435
+ content += '<p><strong>Package:</strong> <code>' + escape(task.npm_package_id) + '</code></p>';
1436
+ content += '<p><strong>Version:</strong> ' + escape(task.version) + '</p>';
1437
+ content += '<p><strong>Website:</strong> ' + escape(task.website_name) + '</p>';
1437
1438
  content += '</div>';
1438
1439
  content += '<div class="col-md-6">';
1439
- content += '<p><strong>GitHub:</strong> ' + this.escapeHtml(task.github_org) + '/' + this.escapeHtml(task.github_repo) + ' (' + this.escapeHtml(task.git_branch) + ')</p>';
1440
- content += '<p><strong>Created by:</strong> ' + this.escapeHtml(task.user_name) + ' (' + this.escapeHtml(task.user_login) + ')</p>';
1440
+ content += '<p><strong>GitHub:</strong> ' + escape(task.github_org) + '/' + escape(task.github_repo) + ' (' + escape(task.git_branch) + ')</p>';
1441
+ content += '<p><strong>Created by:</strong> ' + escape(task.user_name) + ' (' + escape(task.user_login) + ')</p>';
1441
1442
  if (task.approved_by_name) {
1442
- content += '<p><strong>Approved by:</strong> ' + this.escapeHtml(task.approved_by_name) + '</p>';
1443
+ content += '<p><strong>Approved by:</strong> ' + escape(task.approved_by_name) + '</p>';
1443
1444
  }
1444
1445
  if (task.local_folder) {
1445
- content += '<p><strong>Local folder:</strong> <code>' + this.escapeHtml(task.local_folder) + '</code></p>';
1446
+ content += '<p><strong>Local folder:</strong> <code>' + escape(task.local_folder) + '</code></p>';
1446
1447
  }
1447
1448
  if (task.failure_reason) {
1448
- content += '<p><strong>Failure reason:</strong> <span class="text-danger">' + this.escapeHtml(task.failure_reason) + '</span></p>';
1449
+ content += '<p><strong>Failure reason:</strong> <span class="text-danger">' + escape(task.failure_reason) + '</span></p>';
1449
1450
  }
1450
1451
  content += '</div>';
1451
1452
  content += '</div>';
@@ -1455,7 +1456,7 @@ class PublisherModule {
1455
1456
  if (task.announcement) {
1456
1457
  content += '<div class="card mb-4"><div class="card-body">';
1457
1458
  content += '<h5>Announcement</h5>';
1458
- content += '<pre class="mb-0" style="white-space: pre-wrap;">' + this.escapeHtml(task.announcement) + '</pre>';
1459
+ content += '<pre class="mb-0" style="white-space: pre-wrap;">' + escape(task.announcement) + '</pre>';
1459
1460
  content += '</div></div>';
1460
1461
  }
1461
1462
 
@@ -1464,7 +1465,7 @@ class PublisherModule {
1464
1465
 
1465
1466
  // Status transition timestamps from the task record
1466
1467
  if (task.queued_at) {
1467
- events.push({ timestamp: task.queued_at, type: 'status', icon: '📋', label: 'Task queued', detail: 'Created by ' + this.escapeHtml(task.user_name), css: '' });
1468
+ events.push({ timestamp: task.queued_at, type: 'status', icon: '📋', label: 'Task queued', detail: 'Created by ' + escape(task.user_name), css: '' });
1468
1469
  }
1469
1470
  if (task.building_at) {
1470
1471
  events.push({ timestamp: task.building_at, type: 'status', icon: '🔨', label: 'Draft build started', detail: '', css: '' });
@@ -1473,14 +1474,14 @@ class PublisherModule {
1473
1474
  events.push({ timestamp: task.waiting_approval_at, type: 'status', icon: '⏳', label: 'Waiting for approval', detail: 'Draft build completed', css: '' });
1474
1475
  }
1475
1476
  if (task.publishing_at) {
1476
- const approver = task.approved_by_name ? 'Approved by ' + this.escapeHtml(task.approved_by_name) : '';
1477
+ const approver = task.approved_by_name ? 'Approved by ' + escape(task.approved_by_name) : '';
1477
1478
  events.push({ timestamp: task.publishing_at, type: 'status', icon: '🚀', label: 'Publishing started', detail: approver, css: '' });
1478
1479
  }
1479
1480
  if (task.completed_at) {
1480
1481
  events.push({ timestamp: task.completed_at, type: 'status', icon: '✅', label: 'Completed', detail: '', css: 'text-success' });
1481
1482
  }
1482
1483
  if (task.failed_at) {
1483
- events.push({ timestamp: task.failed_at, type: 'status', icon: '❌', label: 'Failed', detail: task.failure_reason ? this.escapeHtml(task.failure_reason) : '', css: 'text-danger' });
1484
+ events.push({ timestamp: task.failed_at, type: 'status', icon: '❌', label: 'Failed', detail: task.failure_reason ? escape(task.failure_reason) : '', css: 'text-danger' });
1484
1485
  }
1485
1486
 
1486
1487
  // Task log entries
@@ -1489,7 +1490,7 @@ class PublisherModule {
1489
1490
  timestamp: log.timestamp,
1490
1491
  type: 'log',
1491
1492
  icon: log.level === 'error' ? '🔴' : log.level === 'warn' ? '🟡' : '🔵',
1492
- label: this.escapeHtml(log.message),
1493
+ label: escape(log.message),
1493
1494
  detail: '',
1494
1495
  css: log.level === 'error' ? 'text-danger' : log.level === 'warn' ? 'text-warning' : 'text-muted'
1495
1496
  });
@@ -1497,8 +1498,8 @@ class PublisherModule {
1497
1498
 
1498
1499
  // User actions
1499
1500
  for (const action of actions) {
1500
- const who = action.user_name ? this.escapeHtml(action.user_name) + ' (' + this.escapeHtml(action.user_login) + ')' : 'Unknown';
1501
- const ip = action.ip_address ? ' from ' + this.escapeHtml(action.ip_address) : '';
1501
+ const who = action.user_name ? escape(action.user_name) + ' (' + escape(action.user_login) + ')' : 'Unknown';
1502
+ const ip = action.ip_address ? ' from ' + escape(action.ip_address) : '';
1502
1503
  let actionLabel = action.action;
1503
1504
  if (action.action === 'create_task') actionLabel = 'Created task';
1504
1505
  else if (action.action === 'approve_task') actionLabel = 'Approved task';
@@ -1580,15 +1581,6 @@ class PublisherModule {
1580
1581
  }
1581
1582
  }
1582
1583
 
1583
- escapeHtml(text) {
1584
- return text
1585
- .replace(/&/g, '&amp;')
1586
- .replace(/</g, '&lt;')
1587
- .replace(/>/g, '&gt;')
1588
- .replace(/"/g, '&quot;')
1589
- .replace(/'/g, '&#039;');
1590
- }
1591
-
1592
1584
  async renderWebsites(req, res) {
1593
1585
  const start = Date.now();
1594
1586
  try {
package/registry/api.js CHANGED
@@ -1,6 +1,7 @@
1
1
  // Enhanced registry-api.js with resolver and HTML rendering functions
2
2
 
3
3
  const { ServerRegistryUtilities } = require('./model');
4
+ const escape = require('escape-html');
4
5
 
5
6
  class RegistryAPI {
6
7
  constructor(crawler) {
@@ -631,19 +632,19 @@ class RegistryAPI {
631
632
  html += '<tr>\n';
632
633
 
633
634
  if (!regCode) {
634
- html += `<td><a href="${path}&registry=${row['registry-code']}">${this._escapeHtml(row['registry-name'])}</a></td>\n`;
635
+ html += `<td><a href="${path}&registry=${row['registry-code']}">${escape(row['registry-name'])}</a></td>\n`;
635
636
  }
636
637
  if (!serverCode) {
637
- html += `<td><a href="${path}&server=${row['server-code']}">${this._escapeHtml(row['server-name'])}</a></td>\n`;
638
+ html += `<td><a href="${path}&server=${row['server-code']}">${escape(row['server-name'])}</a></td>\n`;
638
639
  }
639
640
  if (!versionCode) {
640
641
  html += `<td><a href="${path}&fhirVersion=${row.fhirVersion}">${row.fhirVersion}</a></td>\n`;
641
642
  }
642
643
 
643
- html += `<td><a href="${this._escapeHtml(row.url)}">${this._escapeHtml(row.url)}</a></td>\n`;
644
+ html += `<td><a href="${escape(row.url)}">${escape(row.url)}</a></td>\n`;
644
645
 
645
646
  if (row.error) {
646
- html += `<td><span style="color: maroon">Error: ${this._escapeHtml(row.error)}</span> Last OK ${this._formatDuration(row['last-success'])} ago</td>\n`;
647
+ html += `<td><span style="color: maroon">Error: ${escape(row.error)}</span> Last OK ${this._formatDuration(row['last-success'])} ago</td>\n`;
647
648
  } else {
648
649
  html += `<td>Last OK ${this._formatDuration(row['last-success'])} ago</td>\n`;
649
650
  }
@@ -673,20 +674,20 @@ class RegistryAPI {
673
674
  const data = this.crawler.getData();
674
675
  let html = '<table class="grid">';
675
676
 
676
- html += `<tr><td width="130px"><img src="/assets/images/tx-registry-root.gif">&nbsp;Registries</td><td>${data.address} (${this._escapeHtml(data.outcome)})</td></tr>`;
677
+ html += `<tr><td width="130px"><img src="/assets/images/tx-registry-root.gif">&nbsp;Registries</td><td>${data.address} (${escape(data.outcome)})</td></tr>`;
677
678
 
678
679
  data.registries.forEach(registry => {
679
680
  if (registry.error) {
680
- html += `<tr><td title="${this._escapeHtml(registry.name)}">&nbsp;<img src="/assets/images/tx-registry.png">&nbsp;${registry.code}</td><td><a href="${this._escapeHtml(registry.address)}">${this._escapeHtml(registry.address)}</a>. Error: ${this._escapeHtml(registry.error)}</td></tr>`;
681
+ html += `<tr><td title="${escape(registry.name)}">&nbsp;<img src="/assets/images/tx-registry.png">&nbsp;${registry.code}</td><td><a href="${escape(registry.address)}">${escape(registry.address)}</a>. Error: ${escape(registry.error)}</td></tr>`;
681
682
  } else {
682
- html += `<tr><td title="${this._escapeHtml(registry.name)}">&nbsp;&nbsp;<img src="/assets/images/tx-registry.png">&nbsp;${registry.code}</td><td><a href="${this._escapeHtml(registry.address)}">${this._escapeHtml(registry.address)}</a></td></tr>`;
683
+ html += `<tr><td title="${escape(registry.name)}">&nbsp;&nbsp;<img src="/assets/images/tx-registry.png">&nbsp;${registry.code}</td><td><a href="${escape(registry.address)}">${escape(registry.address)}</a></td></tr>`;
683
684
  }
684
685
 
685
686
  registry.servers.forEach(server => {
686
687
  if (server.authCSList.length > 0 || server.authVSList.length > 0 || server.usageList.length > 0) {
687
- html += `<tr><td title="${this._escapeHtml(server.name)}">&nbsp;&nbsp;&nbsp;&nbsp;<img src="/assets/images/tx-server.png">&nbsp;${server.code}</td><td><a href="${this._escapeHtml(server.address)}">${this._escapeHtml(server.address)}</a>. ${server.description}</td></tr>`;
688
+ html += `<tr><td title="${escape(server.name)}">&nbsp;&nbsp;&nbsp;&nbsp;<img src="/assets/images/tx-server.png">&nbsp;${server.code}</td><td><a href="${escape(server.address)}">${escape(server.address)}</a>. ${server.description}</td></tr>`;
688
689
  } else {
689
- html += `<tr><td title="${this._escapeHtml(server.name)}">&nbsp;&nbsp;&nbsp;&nbsp;<img src="/assets/images/tx-server.png">&nbsp;${server.code}</td><td><a href="${this._escapeHtml(server.address)}">${this._escapeHtml(server.address)}</a></td></tr>`;
690
+ html += `<tr><td title="${escape(server.name)}">&nbsp;&nbsp;&nbsp;&nbsp;<img src="/assets/images/tx-server.png">&nbsp;${server.code}</td><td><a href="${escape(server.address)}">${escape(server.address)}</a></td></tr>`;
690
691
  }
691
692
 
692
693
  server.versions.forEach(version => {
@@ -694,7 +695,7 @@ class RegistryAPI {
694
695
  const versionParts = version.version.split('.');
695
696
  const majorMinor = versionParts.slice(0, 2).join('.');
696
697
 
697
- html += `<tr><td>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<img src="/assets/images/tx-version.png">&nbsp;v${majorMinor}</td><td><a href="${this._escapeHtml(version.address)}">${this._escapeHtml(version.address)}</a>. Status: ${this._escapeHtml(version.details)}. ${version.codeSystems.length} CodeSystems, ${version.valueSets.length} ValueSets</td></tr>`;
698
+ html += `<tr><td>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<img src="/assets/images/tx-version.png">&nbsp;v${majorMinor}</td><td><a href="${escape(version.address)}">${escape(version.address)}</a>. Status: ${escape(version.details)}. ${version.codeSystems.length} CodeSystems, ${version.valueSets.length} ValueSets</td></tr>`;
698
699
  });
699
700
  });
700
701
  });