Skip to content

Commit 278e3a2

Browse files
committed
Implement Scratch.canDownload modal
1 parent 7dc5610 commit 278e3a2

6 files changed

Lines changed: 211 additions & 4 deletions

File tree

‎package-lock.json‎

Lines changed: 1 addition & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
.file-name {
2+
font-family: monospace;
3+
user-select: text;
4+
word-wrap: break-word;
5+
}
6+
.file-name::before {
7+
content: '"';
8+
}
9+
.file-name::after {
10+
content: '"';
11+
}
12+
13+
.name {
14+
15+
}
16+
17+
.dot {
18+
19+
}
20+
21+
.extension {
22+
text-decoration: underline;
23+
}
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
import React from 'react';
2+
import PropTypes from 'prop-types';
3+
import {FormattedMessage} from 'react-intl';
4+
import {APP_NAME} from '../../lib/brand.js';
5+
import styles from './download.css';
6+
7+
const DEFINITELY_EXECUTABLE = [
8+
// Entries should be lowercase and without leading period.
9+
// We use this list to show a stronger security warning; it is not otherwise load-bearing for security.
10+
// Thus a file extension missing from this list is a bug we want to fix, but not a security bug that
11+
// would be eligible for a bounty.
12+
13+
// Windows executable formats
14+
'exe',
15+
'msi',
16+
'msix',
17+
'msixbundle',
18+
'com',
19+
'scf',
20+
'scr',
21+
'sct',
22+
'dll',
23+
'appx',
24+
'appxbundle',
25+
'reg',
26+
27+
// Mac executable formats
28+
'dmg',
29+
'pkg',
30+
31+
// Unix executable formats
32+
'so',
33+
'a',
34+
35+
// Cross-platform executable formats
36+
'jar',
37+
38+
// Browser extensions
39+
'crx',
40+
'xpi',
41+
42+
// Shortcuts
43+
'url',
44+
'webloc',
45+
'lnk',
46+
47+
// Windows scripting languages
48+
'bat',
49+
'ps1',
50+
'vbs',
51+
'vbe',
52+
'wsf',
53+
'wsc',
54+
55+
// Unix scripting languages
56+
'sh',
57+
58+
// Common languages with interpreters that could be executed by double clicking on the file
59+
'js',
60+
'py'
61+
];
62+
63+
/**
64+
* @param {string} name Name of file
65+
* @returns {boolean} True indicates definitely dangerous. False does not mean safe.
66+
*/
67+
const isDefinitelyExecutable = name => {
68+
const parts = name.split('.');
69+
const extension = parts.length > 1 ? parts.pop().toLowerCase() : null;
70+
return extension !== null && DEFINITELY_EXECUTABLE.includes(extension);
71+
};
72+
73+
const FileName = props => {
74+
const MAX_NAME_LENGTH = 80;
75+
const MAX_EXTENSION_LENGTH = 30;
76+
77+
const parts = props.name.split('.');
78+
let extension = parts.length > 1 ? parts.pop() : null;
79+
let name = parts.join('.');
80+
81+
if (name.length > MAX_NAME_LENGTH) {
82+
name = `${name.substring(0, MAX_NAME_LENGTH)}[...]`;
83+
}
84+
if (extension && extension.length > MAX_EXTENSION_LENGTH) {
85+
extension = `[...]${extension.substring(extension.length - MAX_EXTENSION_LENGTH)}`;
86+
}
87+
88+
if (extension === null) {
89+
return (
90+
<span className={styles.fileName}>
91+
{props.name}
92+
</span>
93+
);
94+
}
95+
96+
return (
97+
<span className={styles.fileName}>
98+
<span className={styles.name}>
99+
{name}
100+
</span>
101+
<span className={styles.dot}>
102+
{'.'}
103+
</span>
104+
<span className={styles.extension}>
105+
{extension}
106+
</span>
107+
</span>
108+
);
109+
};
110+
111+
FileName.propTypes = {
112+
name: PropTypes.string.isRequired
113+
};
114+
115+
const DownloadModal = props => (
116+
<div>
117+
<p>
118+
<FormattedMessage
119+
// eslint-disable-next-line max-len
120+
defaultMessage="The project wants to download a file to your computer. It will be saved as {name} in your downloads folder."
121+
description="Part of modal when a project attempts to save a file to someone's downloads folder"
122+
id="tw.download.file"
123+
values={{
124+
name: (
125+
<FileName name={props.name} />
126+
)
127+
}}
128+
/>
129+
</p>
130+
131+
<p>
132+
<FormattedMessage
133+
// eslint-disable-next-line max-len
134+
defaultMessage="This file has not been reviewed by the {APP_NAME} developers."
135+
description="Part of modal when a project attempts to save a file to someone's downloads folder."
136+
id="tw.download.danger"
137+
values={{
138+
APP_NAME
139+
}}
140+
/>
141+
</p>
142+
143+
{isDefinitelyExecutable(props.name) && (
144+
<p>
145+
<FormattedMessage
146+
// eslint-disable-next-line max-len
147+
defaultMessage="This is an executable file format that may contain malicious code if you run it."
148+
description="Part of modal when a project attempts to save a file to someone's downloads folder."
149+
id="tw.download.executable"
150+
values={{
151+
APP_NAME
152+
}}
153+
/>
154+
</p>
155+
)}
156+
</div>
157+
);
158+
159+
DownloadModal.propTypes = {
160+
name: PropTypes.string.isRequired
161+
};
162+
163+
export default DownloadModal;

‎src/components/tw-security-manager-modal/security-manager-modal.jsx‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import ReadClipboard from './read-clipboard.jsx';
1414
import Notify from './notify.jsx';
1515
import Geolocate from './geolocate.jsx';
1616
import Embed from './embed.jsx';
17+
import Download from './download.jsx';
1718
import DelayedMountPropertyHOC from './delayed-mount-property-hoc.jsx';
1819
import styles from './security-manager-modal.css';
1920

@@ -56,6 +57,8 @@ const SecurityManagerModalComponent = props => (
5657
<Geolocate {...props.data} />
5758
) : props.type === SecurityModals.Embed ? (
5859
<Embed {...props.data} />
60+
) : props.type === SecurityModals.Download ? (
61+
<Download {...props.data} />
5962
) : null}
6063

6164
<Box className={styles.buttons}>

‎src/containers/tw-security-manager.jsx‎

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,8 @@ const SECURITY_MANAGER_METHODS = [
138138
'canReadClipboard',
139139
'canNotify',
140140
'canGeolocate',
141-
'canEmbed'
141+
'canEmbed',
142+
'canDownload'
142143
];
143144

144145
class TWSecurityManagerComponent extends React.Component {
@@ -412,6 +413,23 @@ class TWSecurityManagerComponent extends React.Component {
412413
return allowed;
413414
}
414415

416+
/**
417+
* @param {string} url URL to download
418+
* @param {string} name Name to download as
419+
* @returns {Promise<boolean>} True if allowed
420+
*/
421+
async canDownload (url, name) {
422+
const parsed = parseURL(url, FETCHABLE_PROTOCOLS);
423+
if (!parsed) {
424+
return false;
425+
}
426+
const {showModal} = await this.acquireModalLock();
427+
return showModal(SecurityModals.Download, {
428+
url,
429+
name
430+
});
431+
}
432+
415433
render () {
416434
if (this.state.type) {
417435
return (

‎src/lib/tw-security-manager-constants.js‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ const SecurityModals = {
88
ReadClipboard: 'ReadClipboard',
99
Notify: 'Notify',
1010
Geolocate: 'Geolocate',
11-
Embed: 'Embed'
11+
Embed: 'Embed',
12+
Download: 'Download'
1213
};
1314

1415
export default SecurityModals;

0 commit comments

Comments
 (0)