boomack 0.13.1 → 0.13.3

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.
@@ -199,11 +199,13 @@ header.home-header {
199
199
  .slot, .slot.ui.segment {
200
200
  margin: 0;
201
201
  padding: 0;
202
- display: flex;
203
- flex-direction: column;
204
202
  overflow: hidden;
205
203
  position: relative;
206
204
  }
205
+ .layout-grid .slot {
206
+ display: flex;
207
+ flex-direction: column;
208
+ }
207
209
  .slot.ui.segment.without-border {
208
210
  border: none;
209
211
  background: transparent;
@@ -49,11 +49,11 @@ function replaceThemePropsInStr(s) {
49
49
  %><%= slot.zoom !== 1.0 && !contentScale ? ' zoom' : '' %><%
50
50
  %>"
51
51
  style="<%
52
- if (!singled && panel.layout.grid) {
52
+ if (!singled && panel.layout.type === 'grid') {
53
53
  %>grid-column: <%= slot.column + 1 %> / span <%= slot.columnSpan %>;<%
54
54
  %>grid-row: <%= slot.row + 1 %> / span <%= slot.rowSpan %><%
55
55
  }
56
- if (panel.layout.document && commandSrc) {
56
+ if (panel.layout.type === 'document' && commandSrc) {
57
57
  %>height: calc(100vh - 2em);<%
58
58
  }
59
59
  %>"
@@ -185,6 +185,9 @@ function replaceThemePropsInStr(s) {
185
185
  if (slot.minHeight) {
186
186
  %>min-height: <%= slot.minHeight %>;<%
187
187
  }
188
+ if (slot.height) {
189
+ %>height: <%= slot.height %>;<%
190
+ }
188
191
  %>">
189
192
  <% if (slot.showId) { %><div class="slot-id"><%= slot.id %></div><% } %>
190
193
  </div>
@@ -192,6 +195,9 @@ function replaceThemePropsInStr(s) {
192
195
  if (slot.minHeight) {
193
196
  %>min-height: <%= slot.minHeight %>;<%
194
197
  }
198
+ if (slot.height) {
199
+ %>height: <%= slot.height %>;<%
200
+ }
195
201
  if (commandBackground) {
196
202
  %>background: <%= replaceThemePropsInStr(commandBackground) %>;<%
197
203
  }
@@ -200,6 +206,12 @@ function replaceThemePropsInStr(s) {
200
206
  if (!commandContent) {
201
207
  %>display:none;<%
202
208
  }
209
+ if (slot.height) {
210
+ %>height: <%= slot.height %>;<%
211
+ }
212
+ if (slot.maxHeight) {
213
+ %>max-height:<%= slot.maxHeight %>;overflow-y:scroll;<%
214
+ }
203
215
  if (slot.colorFilter) {
204
216
  if (_.isString(slot.colorFilter) && slot.colorFilter !== 'theme') {
205
217
  %>filter: <%= slot.colorFilter %>;<%
@@ -177,6 +177,11 @@ api:
177
177
  yaml: true
178
178
  # The size limit for API requests in JSON or YAML
179
179
  maxBodySize: 16 MB
180
+ # A list with allowed root paths for file URLs in display requests.
181
+ # Allows displaying files directly from the filesystem of the server.
182
+ # Empty by default for security reasons.
183
+ # Examples: /home/me/media, or C:\\Users\\Me\\Pictures
184
+ fileSrcRoots: []
180
185
 
181
186
  cache:
182
187
  # Control how cached resources are handled
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "boomack",
3
- "version": "0.13.1",
3
+ "version": "0.13.3",
4
4
  "description": "web app for displaying hyper-media items in concert with e.g. an IDE",
5
5
  "author": "Tobias Kiertscher <dev@mastersign.de>",
6
6
  "license": "MIT",
@@ -140,6 +140,11 @@ function showConfiguration(cb) {
140
140
  _.forEach(cfg.get('init.actions'), (a, aId) => {
141
141
  txt += `\n ${aId} (${a.type})`;
142
142
  });
143
+ const fileSrcRoots = cfg.get('api.request.fileSrcRoots');
144
+ txt += `\nFile Source Roots: ${_.size(fileSrcRoots)}`;
145
+ _.forEach(fileSrcRoots, r => {
146
+ txt += `\n ${utils.normalizePath(r)}`;
147
+ });
143
148
  log.verbose('Configuration: \n' + txt);
144
149
  cb(null);
145
150
  }
@@ -189,6 +194,7 @@ function loadPlugIns(cb) {
189
194
  const explicitPkgNames = new Set(cfg.get('plugins.packages', []));
190
195
  let success = true;
191
196
  for (const name of explicitPkgNames) {
197
+ log.verbose(`Loading explicit plugin '${name}'...`);
192
198
  success && (success = loadPlugIn(pluginRoot, name));
193
199
  }
194
200
  if (discover) {
@@ -201,6 +207,7 @@ function loadPlugIns(cb) {
201
207
  log.error(`Discovering plugins failed with: ${err}`);
202
208
  }
203
209
  for (const name of discoveredPkgNames) {
210
+ log.verbose(`Loading discovered plugin '${name}'...`);
204
211
  success && (success = loadPlugIn(pluginRoot, name));
205
212
  }
206
213
  }
@@ -2,9 +2,12 @@
2
2
  /**
3
3
  * @module
4
4
  */
5
+ const { fileURLToPath } = require('url');
6
+ const path = require('path');
5
7
  const _ = require('lodash');
6
8
  const mimeType = require('../service/mime-type');
7
9
  const utils = require('../utils');
10
+ const cfg = require('../service/config');
8
11
  const presets = require('../service/presets');
9
12
  const typeMappings = require('../service/media-types');
10
13
  const displayRequestDefaults = {
@@ -53,13 +56,26 @@ function sanitizeOptions(options) {
53
56
  options.requires = utils.enforceArrayOf(options.requires, utils.enforceString);
54
57
  return options;
55
58
  }
59
+ function isValidFileSource(filename) {
60
+ if (!path.isAbsolute(filename))
61
+ return false;
62
+ filename = utils.normalizePath(filename);
63
+ return _.some(cfg.get('api.request.fileSrcRoots'), root => filename.startsWith(utils.normalizePath(root, true)));
64
+ }
56
65
  function isValidSourceUrl(src) {
57
66
  if (!_.isString(src))
58
67
  return false;
59
- if (!src.match(/^(https?|file):\/\//))
68
+ const isHttpUrl = src.startsWith('http://') || src.startsWith('https://');
69
+ const isFileUrl = src.startsWith('file://');
70
+ if (!isHttpUrl && !isFileUrl)
71
+ return false;
72
+ if (isFileUrl && _.isEmpty(cfg.get('api.request.fileSrcRoots')))
60
73
  return false;
61
74
  try {
62
- new URL(src);
75
+ const uri = new URL(src);
76
+ if (isFileUrl && !isValidFileSource(fileURLToPath(uri))) {
77
+ return false;
78
+ }
63
79
  }
64
80
  catch (err) {
65
81
  return false;
@@ -4,6 +4,10 @@
4
4
  const expect = require('chai').expect;
5
5
  const mimeType = require('../service/mime-type');
6
6
  const displayRequest = require('./display-request');
7
+ const cfg = require('../service/config');
8
+ beforeEach(function () {
9
+ cfg.reset();
10
+ });
7
11
  describe('model/display-request', function () {
8
12
  describe('sanitize', function () {
9
13
  it('should return an error, if no content is given', function (done) {
@@ -65,9 +69,41 @@ describe('model/display-request', function () {
65
69
  done();
66
70
  });
67
71
  });
68
- it('should return a sanitized request, if only src is given', function (done) {
72
+ it('should return a sanitized request, if only http src is given', function (done) {
73
+ displayRequest.sanitize({
74
+ src: 'http://localhost/demo.html',
75
+ }, (err, x) => {
76
+ expect(err).to.be.null;
77
+ expect(x).to.be.an('object');
78
+ expect(x).to.deep.equal({
79
+ panel: 'default',
80
+ slot: null,
81
+ title: null,
82
+ type: null,
83
+ text: null,
84
+ data: null,
85
+ stream: null,
86
+ src: 'http://localhost/demo.html',
87
+ options: {
88
+ debug: false,
89
+ transformation: null,
90
+ syntax: null,
91
+ renderer: null,
92
+ iframe: null,
93
+ cache: 'auto',
94
+ extend: 'no',
95
+ scale: 'auto',
96
+ requires: [],
97
+ },
98
+ });
99
+ done();
100
+ });
101
+ });
102
+ it('should return a sanitized request, if only file src is given', function (done) {
103
+ const prefix = process.platform === 'win32' ? 'C:\\Users\\Me' : '/home/me';
104
+ cfg.set('api.request.fileSrcRoots', [prefix]);
69
105
  displayRequest.sanitize({
70
- src: 'file:///user/me/demo.html',
106
+ src: `file://${prefix}/demo.html`,
71
107
  }, (err, x) => {
72
108
  expect(err).to.be.null;
73
109
  expect(x).to.be.an('object');
@@ -79,7 +115,7 @@ describe('model/display-request', function () {
79
115
  text: null,
80
116
  data: null,
81
117
  stream: null,
82
- src: 'file:///user/me/demo.html',
118
+ src: `file://${prefix}/demo.html`,
83
119
  options: {
84
120
  debug: false,
85
121
  transformation: null,
@@ -95,6 +131,28 @@ describe('model/display-request', function () {
95
131
  done();
96
132
  });
97
133
  });
134
+ it('should return an error, if disallowed file src is given', function (done) {
135
+ const prefix1 = process.platform === 'win32' ? 'C:\\Users\\Me' : '/home/me';
136
+ const prefix2 = process.platform === 'win32' ? 'C:\\Windows' : '/etc';
137
+ cfg.set('api.request.fileSrcRoots', [prefix1]);
138
+ displayRequest.sanitize({
139
+ src: `file://${prefix2}/secret.txt`,
140
+ }, (err, x) => {
141
+ expect(err).to.exist;
142
+ expect(x).to.be.undefined;
143
+ done();
144
+ });
145
+ });
146
+ it('should return an error, if relative file src is given', function (done) {
147
+ cfg.set('api.request.fileSrcRoots', ['/relative']);
148
+ displayRequest.sanitize({
149
+ src: 'file://relative/demo.html',
150
+ }, (err, x) => {
151
+ expect(err).to.exist;
152
+ expect(x).to.be.undefined;
153
+ done();
154
+ });
155
+ });
98
156
  it('should sanitize request', function (done) {
99
157
  displayRequest.sanitize({
100
158
  unknown: 'unknown',
@@ -29,6 +29,8 @@ const gridSlotDefaults = _.defaults({
29
29
  }, slotDefaults);
30
30
  const documentSlotDefaults = _.defaults({
31
31
  minHeight: '0',
32
+ height: null,
33
+ maxHeight: null,
32
34
  }, slotDefaults);
33
35
  const slotTemplateDefaults = {
34
36
  history: 0,
@@ -43,6 +45,8 @@ const slotTemplateDefaults = {
43
45
  border: true,
44
46
  zoom: 1.0,
45
47
  minHeight: '0',
48
+ height: null,
49
+ maxHeight: null,
46
50
  };
47
51
  const gridLayoutDefaults = {
48
52
  columns: 1,
@@ -105,6 +105,8 @@ describe('model/layout', function () {
105
105
  border: true,
106
106
  zoom: 1.0,
107
107
  minHeight: '0',
108
+ height: null,
109
+ maxHeight: null,
108
110
  },
109
111
  autoSlots: 0,
110
112
  nextIndex: 1,
@@ -228,6 +230,8 @@ describe('model/layout', function () {
228
230
  noMaximize: true,
229
231
  zoom: 1.25,
230
232
  minHeight: '20px',
233
+ height: '200px',
234
+ maxHeight: '50vh',
231
235
  },
232
236
  autoSlots: 0,
233
237
  nextIndex: 1,
@@ -250,6 +254,8 @@ describe('model/layout', function () {
250
254
  zoom: 1.5,
251
255
  colorFilter: 'saturate(130%)',
252
256
  minHeight: '25vh',
257
+ height: '20rem',
258
+ maxHeight: '100vh',
253
259
  },
254
260
  stash: {
255
261
  id: 'stash',
@@ -270,7 +276,9 @@ describe('model/layout', function () {
270
276
  0.0, -1.0, 0.0, 1.0,
271
277
  0.0, 0.0, 1.0, 0.0,
272
278
  ],
273
- minHeight: '10em',
279
+ minHeight: '2rem',
280
+ height: '5em',
281
+ maxHeight: '100px',
274
282
  },
275
283
  },
276
284
  title: 'Panel Title',
@@ -66,7 +66,7 @@ function applyDisplayRequestDefaults(displayRequest, cb) {
66
66
  if (!r.slot) {
67
67
  r.slot = defaultSlotForPanel(r.panel);
68
68
  }
69
- if (!r.slot) {
69
+ if (panel.layout.type !== 'document' && !r.slot) {
70
70
  cb(error("Slot not found"));
71
71
  return;
72
72
  }
@@ -170,19 +170,17 @@ exports.setup = function (app, io) {
170
170
  return null;
171
171
  }
172
172
  }
173
- let data;
174
173
  try {
175
174
  if (cfg.getBoolean('api.request.yaml')) {
176
- data = YAML.parse(s, { schema: 'yaml-1.1' });
175
+ return YAML.parse(s, { schema: 'yaml-1.1' });
177
176
  }
178
177
  else {
179
- data = JSON.parse(s);
178
+ return JSON.parse(s);
180
179
  }
181
180
  }
182
181
  catch (e) {
183
182
  return null;
184
183
  }
185
- return _.isArray(data) ? data : [data];
186
184
  }
187
185
  function titleFromHttpHeader(req) {
188
186
  const titleHeader = req.get('X-Boomack-Title');
@@ -191,16 +189,12 @@ exports.setup = function (app, io) {
191
189
  null;
192
190
  }
193
191
  function optionsFromHttpHeader(req) {
194
- let result = [];
195
- const presetHeader = req.get('X-Boomack-Presets');
196
- const presets = decodePresets(presetHeader);
197
- if (presets)
198
- result = presets;
199
192
  const optionsHeader = req.get('X-Boomack-Options');
200
- const options = decodeOptions(optionsHeader);
201
- if (options)
202
- result = result.concat(options);
203
- return result;
193
+ return decodeOptions(optionsHeader);
194
+ }
195
+ function presetsFromHttpHeader(req) {
196
+ const presetHeader = req.get('X-Boomack-Presets');
197
+ return decodePresets(presetHeader);
204
198
  }
205
199
  function handleStreamingDisplayRequest(req, res, panelId, slotId) {
206
200
  const type = req.get('Content-Type');
@@ -213,6 +207,7 @@ exports.setup = function (app, io) {
213
207
  return;
214
208
  }
215
209
  const title = titleFromHttpHeader(req);
210
+ const presets = presetsFromHttpHeader(req);
216
211
  const options = optionsFromHttpHeader(req);
217
212
  const displayRequest = {
218
213
  panel: panelId,
@@ -222,6 +217,8 @@ exports.setup = function (app, io) {
222
217
  };
223
218
  if (title)
224
219
  displayRequest.title = title;
220
+ if (presets)
221
+ displayRequest.presets = presets;
225
222
  if (options)
226
223
  displayRequest.options = options;
227
224
  handleDisplayRequest([displayRequest], req.authorization, res);
@@ -236,8 +233,7 @@ exports.setup = function (app, io) {
236
233
  message: `The target panel "${panelId}" does not exist.`,
237
234
  });
238
235
  }
239
- const slotId = panel.layout.defaultSlot;
240
- handleStreamingDisplayRequest(req, res, panelId, slotId);
236
+ handleStreamingDisplayRequest(req, res, panelId, null);
241
237
  });
242
238
  app.post('/v1/panels/:panelId/slots/:slotId/display', apiAuth('content.display'), (req, res) => {
243
239
  const panelId = req.params.panelId;
@@ -252,7 +248,7 @@ exports.setup = function (app, io) {
252
248
  }
253
249
  const slotId = req.params.slotId;
254
250
  const slot = panel.layout.slots[slotId];
255
- if (!slot) {
251
+ if (panel.layout.type != 'document' && !slot) {
256
252
  error(res, {
257
253
  status: 404,
258
254
  title: 'Target Not Found',
@@ -203,7 +203,7 @@ exports.pushDisplayCommand = function (displayCommand, cb) {
203
203
  return;
204
204
  }
205
205
  const slotId = displayCommand.slot;
206
- if (!panel.content.has(slotId)) {
206
+ if (panel.layout.type !== 'document' && !panel.content.has(slotId)) {
207
207
  cb(new BadRequestError(`Panel '${panelId}' has no slot '${slotId}'`));
208
208
  return;
209
209
  }
@@ -98,6 +98,8 @@
98
98
  *
99
99
  * @typedef {Slot} DocumentSlot
100
100
  * @property {string} minHeight - The CSS length for the minimal height of the slot.
101
+ * @property {string} height - The CSS length for the height of the slot.
102
+ * @property {string} maxHeight - The CSS length for the maximal height of the slot.
101
103
  */
102
104
  /**
103
105
  * The structure to describe a general panel layout.
@@ -2,6 +2,7 @@
2
2
  /**
3
3
  * @module
4
4
  */
5
+ const path = require('path');
5
6
  const zlib = require('zlib');
6
7
  const _ = require('lodash');
7
8
  exports.escapeRegEx = s => {
@@ -245,6 +246,23 @@ exports.isHttpUrl = function (urlStr) {
245
246
  }
246
247
  return srcUrl.protocol == 'http:' || srcUrl.protocol == 'https:';
247
248
  };
249
+ /**
250
+ * Normalize a filesystem path.
251
+ *
252
+ * @param {string} filename An absolute path of a file or directory
253
+ * @param {boolean|null} forceTrailingSlash Assures a trailing slash on the end of the path
254
+ * @returns {string} The normalized path
255
+ */
256
+ exports.normalizePath = function (filename, forceTrailingSlash) {
257
+ filename = path.normalize(filename);
258
+ if (process.platform === 'win32') {
259
+ filename = filename.toLowerCase();
260
+ }
261
+ if (forceTrailingSlash && !filename.endsWith(path.sep)) {
262
+ filename = filename + path.sep;
263
+ }
264
+ return filename;
265
+ };
248
266
  const dataAmountPattern = /^([+-]?(?:0|[1-9][\d,]*))\s*(?:([KMG])[B]?)?$/i;
249
267
  /**
250
268
  * Parse data amount values from string to number.