diff --git a/src/components/tw-security-manager-modal/fileEdit.css b/src/components/tw-security-manager-modal/fileEdit.css new file mode 100644 index 00000000000..2379225aaca --- /dev/null +++ b/src/components/tw-security-manager-modal/fileEdit.css @@ -0,0 +1,23 @@ +.file-name { + font-family: monospace; + user-select: text; + word-wrap: break-word; +} +.file-name::before { + content: '"'; +} +.file-name::after { + content: '"'; +} + +.name { + +} + +.dot { + +} + +.extension { + text-decoration: underline; +} diff --git a/src/components/tw-security-manager-modal/fileEdit.jsx b/src/components/tw-security-manager-modal/fileEdit.jsx new file mode 100644 index 00000000000..8b26a78b963 --- /dev/null +++ b/src/components/tw-security-manager-modal/fileEdit.jsx @@ -0,0 +1,192 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import {FormattedMessage} from 'react-intl'; +import {APP_NAME} from '../../lib/brand.js'; +import styles from './fileEdit.css'; + +const DEFINITELY_EXECUTABLE = [ + // Entries should be lowercase and without leading period. + // We use this list to show a stronger security warning; it is not otherwise load-bearing for security. + // Thus a file extension missing from this list is a bug we want to fix, but not a security bug that + // would be eligible for a bounty. + + // Windows executable formats + 'exe', + 'msi', + 'msix', + 'msixbundle', + 'com', + 'scf', + 'scr', + 'sct', + 'dll', + 'appx', + 'appxbundle', + 'reg', + 'iso', + 'drv', + 'sys', + + // Mac executable formats + 'app', + 'dmg', + 'pkg', + + // Unix executable formats + 'so', + 'a', + 'run', + 'appimage', + 'deb', + 'rpm', + 'snap', + 'flatpakref', + + // Cross-platform executable formats + 'jar', + + // Browser extensions + 'crx', + 'xpi', + + // Shortcuts + 'url', + 'webloc', + 'inetloc', + 'lnk', + + // Windows scripting languages + 'bat', + 'cmd', + 'ps1', + 'psm1', + 'asp', + 'vbs', + 'vbe', + 'ws', + 'wsf', + 'wsc', + 'ahk', + + // Microsoft Office macros + 'docm', + 'dotm', + 'xlm', + 'xlsm', + 'xltm', + 'xla', + 'xlam', + 'pptm', + 'potm', + 'ppsm', + 'sldm', + + // Unix scripting languages + 'sh', + + // Common cross-platform languages with interpreters that could be executed by double clicking on the file + 'js', + 'py' +]; + +/** + * @param {string} name Name of file + * @returns {boolean} True indicates definitely dangerous. False does not mean safe. + */ +const isDefinitelyExecutable = name => { + const parts = name.split('.'); + const extension = parts.length > 1 ? parts.pop().toLowerCase() : null; + return extension !== null && DEFINITELY_EXECUTABLE.includes(extension); +}; + +const FileName = props => { + const MAX_NAME_LENGTH = 80; + const MAX_EXTENSION_LENGTH = 30; + + const parts = props.name.split('.'); + let extension = parts.length > 1 ? parts.pop() : null; + let name = parts.join('.'); + + if (name.length > MAX_NAME_LENGTH) { + name = `${name.substring(0, MAX_NAME_LENGTH)}[...]`; + } + if (extension && extension.length > MAX_EXTENSION_LENGTH) { + extension = `[...]${extension.substring(extension.length - MAX_EXTENSION_LENGTH)}`; + } + + if (extension === null) { + return ( + + {props.name} + + ); + } + + return ( + + + {name} + + + {'.'} + + + {extension} + + + ); +}; + +FileName.propTypes = { + name: PropTypes.string.isRequired +}; + +const DownloadModal = props => ( +
+

+ + ) + }} + /> +

+ +

+ +

+ + {isDefinitelyExecutable(props.name) && ( +

+ +

+ )} +
+); + +DownloadModal.propTypes = { + name: PropTypes.string.isRequired +}; + +export default DownloadModal; diff --git a/src/containers/tw-security-manager.jsx b/src/containers/tw-security-manager.jsx index be326af564b..4396fab844d 100644 --- a/src/containers/tw-security-manager.jsx +++ b/src/containers/tw-security-manager.jsx @@ -139,7 +139,8 @@ const SECURITY_MANAGER_METHODS = [ 'canNotify', 'canGeolocate', 'canEmbed', - 'canDownload' + 'canDownload', + 'canEditFile' ]; class TWSecurityManagerComponent extends React.Component { @@ -430,6 +431,17 @@ class TWSecurityManagerComponent extends React.Component { }); } + async canEditFile (name) { + const parsed = parseURL(url, FETCHABLE_PROTOCOLS); + if (!parsed) { + return false; + } + const {showModal} = await this.acquireModalLock(); + return showModal(SecurityModals.fileEdit, { + name + }); + } + render () { if (this.state.type) { return ( diff --git a/src/lib/tw-security-manager-constants.js b/src/lib/tw-security-manager-constants.js index d4b349b4cb1..54a78916c77 100644 --- a/src/lib/tw-security-manager-constants.js +++ b/src/lib/tw-security-manager-constants.js @@ -9,7 +9,8 @@ const SecurityModals = { Notify: 'Notify', Geolocate: 'Geolocate', Embed: 'Embed', - Download: 'Download' + Download: 'Download', + fileEdit: 'fileEdit', }; export default SecurityModals;