@truedat/core 4.46.0 → 4.46.2

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # Changelog
2
2
 
3
+ ## [4.46.2] 2022-06-16
4
+
5
+ ### Added
6
+
7
+ - [TD-4739] `SafeLink` component to prevent rendering unsafe hyperlinks
8
+
9
+ ### Changed
10
+
11
+ - [TD-4739] `RichTextEditor` now renders hyperlinks using `SafeLink`
12
+
3
13
  ## [4.44.5] 2022-05-20
4
14
 
5
15
  ### Changed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@truedat/core",
3
- "version": "4.46.0",
3
+ "version": "4.46.2",
4
4
  "description": "Truedat Web Core",
5
5
  "sideEffects": false,
6
6
  "jsnext:main": "src/index.js",
@@ -35,7 +35,7 @@
35
35
  "@testing-library/jest-dom": "^5.16.4",
36
36
  "@testing-library/react": "^12.0.0",
37
37
  "@testing-library/user-event": "^13.2.1",
38
- "@truedat/test": "4.46.0",
38
+ "@truedat/test": "4.46.2",
39
39
  "babel-jest": "^28.1.0",
40
40
  "babel-plugin-dynamic-import-node": "^2.3.3",
41
41
  "babel-plugin-lodash": "^3.3.4",
@@ -112,5 +112,5 @@
112
112
  "react-dom": ">= 16.8.6 < 17",
113
113
  "semantic-ui-react": ">= 0.88.2 < 2.1"
114
114
  },
115
- "gitHead": "ed9e54123f20ee10a528c865653da09022457327"
115
+ "gitHead": "161f3fae4428ee4e4e26b62fd2f9764ce389d0e2"
116
116
  }
@@ -1,25 +1,31 @@
1
1
  import _ from "lodash/fp";
2
2
  import React from "react";
3
+ import PropTypes from "prop-types";
4
+ import { isKeyHotkey } from "is-hotkey";
3
5
  import isUrl from "is-url";
6
+ import { injectIntl } from "react-intl";
4
7
  import { Editor, getEventTransfer } from "slate-react";
5
8
  import { Value } from "slate";
6
- import { isKeyHotkey } from "is-hotkey";
7
9
  import { Menu, Icon } from "semantic-ui-react";
10
+ import { validUrl } from "../services/validation";
11
+ import SafeLink from "./SafeLink";
8
12
 
9
13
  const DEFAULT_NODE = "paragraph";
10
14
 
11
15
  const isBoldHotkey = isKeyHotkey("mod+b");
12
16
  const isItalicHotkey = isKeyHotkey("mod+i");
13
17
  const isUnderlinedHotkey = isKeyHotkey("mod+u");
14
- //const isCodeHotkey = isKeyHotkey("mod+`");
15
18
 
16
- function wrapLink(change, href) {
17
- change.wrapInline({
18
- type: "link",
19
- data: { href }
20
- });
19
+ function wrapLink(change, value) {
20
+ const href = validUrl(value);
21
+ if (href) {
22
+ change.wrapInline({
23
+ type: "link",
24
+ data: { href },
25
+ });
21
26
 
22
- change.moveToEnd();
27
+ change.moveToEnd();
28
+ }
23
29
  }
24
30
 
25
31
  function unwrapLink(change) {
@@ -37,27 +43,27 @@ export class RichTextEditor extends React.Component {
37
43
  {
38
44
  object: "block",
39
45
  type: "paragraph",
40
- nodes: []
41
- }
42
- ]
43
- }
46
+ nodes: [],
47
+ },
48
+ ],
49
+ },
44
50
  }
45
51
  : value;
46
52
 
47
53
  this.state = { value: Value.fromJSON(json) };
48
54
  }
49
55
 
50
- hasMark = type => {
56
+ hasMark = (type) => {
51
57
  const { value } = this.state;
52
- return value.activeMarks.some(mark => mark.type == type);
58
+ return value.activeMarks.some((mark) => mark.type == type);
53
59
  };
54
60
 
55
- hasBlock = type => {
61
+ hasBlock = (type) => {
56
62
  const { value } = this.state;
57
- return value.blocks.some(node => node.type == type);
63
+ return value.blocks.some((node) => node.type == type);
58
64
  };
59
65
 
60
- ref = editor => {
66
+ ref = (editor) => {
61
67
  this.editor = editor;
62
68
  };
63
69
 
@@ -79,7 +85,7 @@ export class RichTextEditor extends React.Component {
79
85
  <div
80
86
  style={{
81
87
  border: "1px solid rgba(34, 36, 38, 0.15)",
82
- borderRadius: "0.28571429rem"
88
+ borderRadius: "0.28571429rem",
83
89
  }}
84
90
  >
85
91
  <Menu>
@@ -143,7 +149,7 @@ export class RichTextEditor extends React.Component {
143
149
  <Menu.Item
144
150
  icon
145
151
  disabled={!isActive}
146
- onMouseDown={event => this.onClickMark(event, type)}
152
+ onMouseDown={(event) => this.onClickMark(event, type)}
147
153
  >
148
154
  <Icon name={icon} />
149
155
  </Menu.Item>
@@ -152,7 +158,7 @@ export class RichTextEditor extends React.Component {
152
158
 
153
159
  renderBlockButton = (type, icon) => {
154
160
  const {
155
- value: { document, blocks }
161
+ value: { document, blocks },
156
162
  } = this.state;
157
163
  const parent =
158
164
  blocks.size != 0 ? document.getParent(blocks.first().key) : null;
@@ -164,7 +170,7 @@ export class RichTextEditor extends React.Component {
164
170
  <Menu.Item
165
171
  icon
166
172
  disabled={!isActive}
167
- onMouseDown={event => this.onClickBlock(event, type)}
173
+ onMouseDown={(event) => this.onClickBlock(event, type)}
168
174
  >
169
175
  <Icon name={icon} />
170
176
  </Menu.Item>
@@ -178,7 +184,7 @@ export class RichTextEditor extends React.Component {
178
184
  <Menu.Item
179
185
  icon
180
186
  disabled={!isActive}
181
- onMouseDown={event => this.onClickLink(event, type)}
187
+ onMouseDown={(event) => this.onClickLink(event, type)}
182
188
  >
183
189
  <Icon name={icon} />
184
190
  </Menu.Item>
@@ -210,17 +216,10 @@ export class RichTextEditor extends React.Component {
210
216
  case "numbered-list":
211
217
  return <ol {...attributes}> {children}</ol>;
212
218
  case "link":
213
- const { data } = node;
214
- const href = data.get("href");
215
219
  return (
216
- <a
217
- {...attributes}
218
- href={href}
219
- rel="noopener noreferrer"
220
- target="_blank"
221
- >
220
+ <SafeLink href={node.data?.get("href")} {...attributes}>
222
221
  {children}
223
- </a>
222
+ </SafeLink>
224
223
  );
225
224
  default:
226
225
  return next();
@@ -249,23 +248,16 @@ export class RichTextEditor extends React.Component {
249
248
 
250
249
  switch (node.type) {
251
250
  case "link": {
252
- const { data } = node;
253
- const href = data.get("href");
251
+ const props = _.omit(["ref"])(attributes);
254
252
  return (
255
- <a
256
- {...attributes}
257
- href={href}
258
- rel="noopener noreferrer"
259
- target="_blank"
260
- >
253
+ <SafeLink href={node.data?.get("href")} {...props}>
261
254
  {children}
262
- </a>
255
+ </SafeLink>
263
256
  );
264
257
  }
265
258
 
266
- default: {
259
+ default:
267
260
  return next();
268
- }
269
261
  }
270
262
  };
271
263
 
@@ -338,8 +330,11 @@ export class RichTextEditor extends React.Component {
338
330
  }
339
331
  } else {
340
332
  const isList = this.hasBlock("list-item");
341
- const isType = value.blocks.some(block => {
342
- return !!document.getClosest(block.key, parent => parent.type == type);
333
+ const isType = value.blocks.some((block) => {
334
+ return !!document.getClosest(
335
+ block.key,
336
+ (parent) => parent.type == type
337
+ );
343
338
  });
344
339
 
345
340
  if (isList && isType) {
@@ -361,10 +356,10 @@ export class RichTextEditor extends React.Component {
361
356
 
362
357
  hasLinks = () => {
363
358
  const { value } = this.state;
364
- return value.inlines.some(inline => inline.type == "link");
359
+ return value.inlines.some((inline) => inline.type == "link");
365
360
  };
366
361
 
367
- onClickLink = event => {
362
+ onClickLink = (event) => {
368
363
  event.preventDefault();
369
364
 
370
365
  const { editor } = this;
@@ -388,4 +383,13 @@ export class RichTextEditor extends React.Component {
388
383
  };
389
384
  }
390
385
 
391
- export default RichTextEditor;
386
+ RichTextEditor.propTypes = {
387
+ label: PropTypes.string,
388
+ name: PropTypes.string,
389
+ onChange: PropTypes.func,
390
+ readOnly: PropTypes.bool,
391
+ required: PropTypes.bool,
392
+ value: PropTypes.object,
393
+ };
394
+
395
+ export default injectIntl(RichTextEditor);
@@ -0,0 +1,32 @@
1
+ import React from "react";
2
+ import PropTypes from "prop-types";
3
+ import { validUrl } from "../services/validation";
4
+
5
+ export const SafeLink = ({ className, href, children, ...props }) =>
6
+ validUrl(href) ? (
7
+ <a
8
+ {...props}
9
+ className={className}
10
+ href={href}
11
+ rel="noopener noreferrer"
12
+ target="_blank"
13
+ >
14
+ {children}
15
+ </a>
16
+ ) : (
17
+ <a
18
+ className={className}
19
+ title={href}
20
+ style={{ color: "red", backgroundColor: "yellow" }}
21
+ >
22
+ {children}
23
+ </a>
24
+ );
25
+
26
+ SafeLink.propTypes = {
27
+ href: PropTypes.string,
28
+ children: PropTypes.node,
29
+ className: PropTypes.string,
30
+ };
31
+
32
+ export default SafeLink;
@@ -0,0 +1,18 @@
1
+ import React from "react";
2
+ import { render } from "@truedat/test/render";
3
+ import SafeLink from "../SafeLink";
4
+
5
+ describe("<SafeLink />", () => {
6
+ it("renders an href if url is http or https", () => {
7
+ const { queryByRole } = render(<SafeLink href="https://truedat.com" />);
8
+ expect(queryByRole("link")).toBeInTheDocument();
9
+ });
10
+
11
+ ["javascript:foo", "not_a_valid_url"].forEach((href) => {
12
+ it("renders non-navigable span if url is potentially unsafe", () => {
13
+ const { queryByRole, queryByTitle } = render(<SafeLink href={href} />);
14
+ expect(queryByRole("link")).not.toBeInTheDocument();
15
+ expect(queryByTitle(href)).toBeInTheDocument();
16
+ });
17
+ });
18
+ });
@@ -30,6 +30,7 @@ import QualityMenu from "./QualityMenu";
30
30
  import Redirector from "./Redirector";
31
31
  import RichTextEditor from "./RichTextEditor";
32
32
  import RouteListener from "./RouteListener";
33
+ import SafeLink from "./SafeLink";
33
34
  import ScrollToTop from "./ScrollToTop";
34
35
  import SearchInput from "./SearchInput";
35
36
  import SearchMenu from "./SearchMenu";
@@ -76,6 +77,7 @@ export {
76
77
  Redirector,
77
78
  RichTextEditor,
78
79
  RouteListener,
80
+ SafeLink,
79
81
  ScrollToTop,
80
82
  SearchInput,
81
83
  SearchMenu,
@@ -0,0 +1,15 @@
1
+ import { validUrl } from "../validation";
2
+
3
+ describe("validUrl", () => {
4
+ it("returns url if schema is http or https", () => {
5
+ ["https://www.truedat.io", "http://www.truedat.io"].forEach((url) => {
6
+ expect(validUrl(url)).toBe(url);
7
+ });
8
+ });
9
+
10
+ ["javascript:foo", "not_a_valid_url"].forEach((url) => {
11
+ it("returns false if url is potentially unsafe", () => {
12
+ expect(validUrl(url)).toBe(false);
13
+ });
14
+ });
15
+ });
@@ -0,0 +1,10 @@
1
+ export const validUrl = (value) => {
2
+ try {
3
+ const url = new URL(value);
4
+ return url.protocol === "http:" || url.protocol === "https:"
5
+ ? value
6
+ : false;
7
+ } catch (_) {
8
+ return false;
9
+ }
10
+ };