diff --git a/app/extensions/brave/about-flash.html b/app/extensions/brave/about-flash.html new file mode 100644 index 00000000000..6e8e84b2346 --- /dev/null +++ b/app/extensions/brave/about-flash.html @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + +
+ + diff --git a/app/extensions/brave/content/scripts/blockFlash.js b/app/extensions/brave/content/scripts/blockFlash.js index 9b6ca4d8396..9e7d57b364e 100644 --- a/app/extensions/brave/content/scripts/blockFlash.js +++ b/app/extensions/brave/content/scripts/blockFlash.js @@ -2,7 +2,6 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this file, * You can obtain one at http://mozilla.org/MPL/2.0/. */ - function blockFlashDetection () { const handler = { length: 0, @@ -10,15 +9,15 @@ function blockFlashDetection () { namedItem: () => { return null }, refresh: () => {} } - Navigator.prototype.__defineGetter__('plugins', () => { return handler }) - Navigator.prototype.__defineGetter__('mimeTypes', () => { return handler }) + window.Navigator.prototype.__defineGetter__('plugins', () => { return handler }) + window.Navigator.prototype.__defineGetter__('mimeTypes', () => { return handler }) } function getBlockFlashPageScript () { return '(' + Function.prototype.toString.call(blockFlashDetection) + '());' } -if (!window.location.search || - !window.location.search.includes('brave_flash_allowed')) { +if (chrome.contentSettings.flashActive != 'allow' || + chrome.contentSettings.flashEnabled != 'allow') { executeScript(getBlockFlashPageScript()) } diff --git a/app/extensions/brave/content/scripts/flashListener.js b/app/extensions/brave/content/scripts/flashListener.js index f3d255bde90..a577d0a1514 100644 --- a/app/extensions/brave/content/scripts/flashListener.js +++ b/app/extensions/brave/content/scripts/flashListener.js @@ -15,7 +15,7 @@ }) } // Some pages insert the password form into the DOM after it's loaded - var observer = new MutationObserver(function (mutations) { + var observer = new window.MutationObserver(function (mutations) { mutations.forEach(function (mutation) { if (mutation.addedNodes.length) { replaceAdobeLinks() @@ -29,3 +29,131 @@ }) }, 1000) })() + +const placeholderUrl = 'chrome-extension://mnojpmjdmbbfmejpflffifhffcmidifd/about-flash.html' + +/** + * Whether a src is a .swf file. + * If so, returns the origin of the file. Otherwise returns false. + * @param {string} src + * @return {boolean|string} + */ +function isSWF (src) { + if (!src) { + return false + } + let a = document.createElement('a') + a.href = src + if (a.pathname && a.pathname.toLowerCase().endsWith('.swf')) { + return a.origin + } else { + return false + } +} + +/** + * Gets all Flash object descendants of an element. + * Reference: + * https://helpx.adobe.com/flash/kb/flash-object-embed-tag-attributes.html + * @param {Element} elem - HTML element to search + * @return {Array.} + */ +function getFlashObjects (elem) { + let results = [] // Array.<{element: Element, origin: string}> + Array.from(elem.getElementsByTagName('embed')).forEach((el) => { + let origin = isSWF(el.getAttribute('src')) + if (origin) { + results.push({ + element: el, + origin + }) + } + }) + + Array.from(elem.getElementsByTagName('object')).forEach((el) => { + // Skip objects that are contained in other flash objects + /* + for (let i = 0; i < results.length; i++) { + if (results[i].element.contains(el)) { + return + } + } + */ + let origin = isSWF(el.getAttribute('data')) + if (origin) { + results.push({ + element: el, + origin + }) + } else { + // See example at + // https://helpx.adobe.com/animate/kb/object-tag-syntax.html + Array.from(el.getElementsByTagName('param')).forEach((param) => { + let name = param.getAttribute('name') + let origin = isSWF(param.getAttribute('value')) + if (name && ['movie', 'src'].includes(name.toLowerCase()) && + origin) { + results.push({ + element: el, + origin + }) + } + }) + } + }) + return results +} + +/** + * Inserts Flash placeholders. + * @param {Element} elem - HTML element to search + */ +function insertFlashPlaceholders (elem) { + const minWidth = 200 + const minHeight = 100 + let flashObjects = getFlashObjects(elem) + flashObjects.forEach((obj) => { + let el = obj.element + let pluginRect = el.getBoundingClientRect() + let height = el.getAttribute('height') || pluginRect.height + let width = el.getAttribute('width') || pluginRect.width + if (height > minHeight && width > minWidth) { + let parent = el.parentNode + if (!parent) { + return + } + let iframe = document.createElement('iframe') + iframe.setAttribute('sandbox', 'allow-scripts') + let hash = window.location.origin + if (chrome.contentSettings.flashEnabled == 'allow') { + hash = hash + '#flashEnabled' + } + iframe.setAttribute('src', [placeholderUrl, hash].join('#')) + iframe.setAttribute('style', `width: ${width}px; height: ${height}px`) + parent.replaceChild(iframe, el) + } else { + // Note when elements are too small so we can improve the heuristic. + console.log('got too-small Flash element', obj, height, width) + } + }) +} + +var observer = new window.MutationObserver(function (mutations) { + mutations.forEach(function (mutation) { + if (mutation.addedNodes) { + Array.from(mutation.addedNodes).forEach((node) => { + insertFlashPlaceholders(node) + }) + } + }) +}) + +if (chrome.contentSettings.flashActive != 'allow' || + chrome.contentSettings.flashEnabled != 'allow') { + setTimeout(() => { + insertFlashPlaceholders(document.documentElement) + observer.observe(document.documentElement, { + childList: true + }) + }, 1000) +} diff --git a/app/extensions/brave/img/bravePluginAlert.png b/app/extensions/brave/img/bravePluginAlert.png new file mode 100644 index 00000000000..342794c1f0d Binary files /dev/null and b/app/extensions/brave/img/bravePluginAlert.png differ diff --git a/app/extensions/brave/js/about-flash.js b/app/extensions/brave/js/about-flash.js new file mode 100644 index 00000000000..6175471b8ca --- /dev/null +++ b/app/extensions/brave/js/about-flash.js @@ -0,0 +1,5 @@ +function initBraveryDefaultsListener (e) { + window.initBraveryDefaults = e.detail + window.removeEventListener('bravery-defaults-updated', initBraveryDefaultsListener) +} +window.addEventListener('bravery-defaults-updated', initBraveryDefaultsListener) diff --git a/app/extensions/brave/locales/en-US/app.properties b/app/extensions/brave/locales/en-US/app.properties index 561a6a01815..26ea085c468 100644 --- a/app/extensions/brave/locales/en-US/app.properties +++ b/app/extensions/brave/locales/en-US/app.properties @@ -138,9 +138,12 @@ permissionWebMidi=use web MIDI permissionDisableCursor=disable your mouse cursor permissionFullscreen=use fullscreen mode permissionExternal=open an external application - tabsSuggestionTitle=Tabs bookmarksSuggestionTitle=Bookmarks historySuggestionTitle=History searchSuggestionTitle=Search topSiteSuggestionTitle=Top Site +flashTitle=Flash Object Blocked +flashRightClick=Right-click to run Adobe Flash +flashSubtext=from {{source}} on {{site}}. +flashExpirationText=Approvals reset 7 days after last visit. diff --git a/app/extensions/brave/locales/en-US/menu.properties b/app/extensions/brave/locales/en-US/menu.properties index 0f45af0dce4..a2cdefc8aee 100644 --- a/app/extensions/brave/locales/en-US/menu.properties +++ b/app/extensions/brave/locales/en-US/menu.properties @@ -121,3 +121,5 @@ autoHideMenuBar=Menu Bar updateChannel=Update Channel licenseText=This software uses libraries from the FFmpeg project under the LGPLv2.1 lookupSelection=Look Up “{{selectedVariable}}” +allowFlashOnce=Allow once +allowFlashAlways=Allow for 1 week diff --git a/app/extensions/brave/locales/en-US/preferences.properties b/app/extensions/brave/locales/en-US/preferences.properties index 92688c214c0..706accdde4f 100644 --- a/app/extensions/brave/locales/en-US/preferences.properties +++ b/app/extensions/brave/locales/en-US/preferences.properties @@ -83,6 +83,9 @@ midiSysexPermission=Use web MIDI pointerLockPermission=Disable your mouse cursor fullscreenPermission=Fullscreen access openExternalPermission=Open external applications +flash=Run Adobe Flash Player +flashAllowOnce=Allow once +flashAllowAlways=Allow until {{time}} alwaysAllow=Always allow alwaysDeny=Always deny appearanceSettings=Appearance settings: diff --git a/app/index.js b/app/index.js index e95747be787..9514af7cc30 100644 --- a/app/index.js +++ b/app/index.js @@ -180,7 +180,7 @@ let loadAppStatePromise = SessionStore.loadAppState().catch(() => { return SessionStore.defaultAppState() }) -let flashEnabled = false +let flashInitialized = false // Some settings must be set right away on startup, those settings should be handled here. loadAppStatePromise.then((initialState) => { @@ -191,7 +191,7 @@ loadAppStatePromise.then((initialState) => { if (initialState.flash && initialState.flash.enabled === true) { if (flash.init()) { // Flash was initialized successfully - flashEnabled = true + flashInitialized = true return } } @@ -346,7 +346,7 @@ app.on('ready', () => { // For tests we always want to load default app state const loadedPerWindowState = initialState.perWindowState delete initialState.perWindowState - initialState.flashEnabled = flashEnabled + initialState.flashInitialized = flashInitialized appActions.setState(Immutable.fromJS(initialState)) return loadedPerWindowState }).then((loadedPerWindowState) => { @@ -392,8 +392,12 @@ app.on('ready', () => { appActions.changeSetting(key, value) }) - ipcMain.on(messages.CHANGE_SITE_SETTING, (e, hostPattern, key, value) => { - appActions.changeSiteSetting(hostPattern, key, value) + ipcMain.on(messages.CHANGE_SITE_SETTING, (e, hostPattern, key, value, temp) => { + appActions.changeSiteSetting(hostPattern, key, value, temp) + }) + + ipcMain.on(messages.REMOVE_SITE_SETTING, (e, hostPattern, key) => { + appActions.removeSiteSetting(hostPattern, key) }) ipcMain.on(messages.SET_CLIPBOARD, (e, text) => { diff --git a/app/locale.js b/app/locale.js index 807e745f8ae..31c35f2bfe2 100644 --- a/app/locale.js +++ b/app/locale.js @@ -28,6 +28,8 @@ var rendererIdentifiers = function () { 'copyLinkAddress', 'copyEmailAddress', 'saveLinkAs', + 'allowFlashOnce', + 'allowFlashAlways', 'openInNewWindow', 'openInNewSessionTab', 'openInNewPrivateTab', diff --git a/app/sessionStore.js b/app/sessionStore.js index be95a90ea84..a47433214b0 100644 --- a/app/sessionStore.js +++ b/app/sessionStore.js @@ -206,13 +206,21 @@ module.exports.cleanAppData = (data) => { // Delete temp site settings data.temporarySiteSettings = {} // Delete Flash state since this is checked on startup - delete data.flashEnabled + delete data.flashInitialized // We used to store a huge list of IDs but we didn't use them. // Get rid of them here. delete data.windows if (data.perWindowState) { data.perWindowState.forEach(module.exports.cleanSessionData) } + // Delete expired Flash approvals + let now = Date.now() + for (var host in data.siteSettings) { + let expireTime = data.siteSettings[host].flash + if (typeof expireTime === 'number' && expireTime < now) { + delete data.siteSettings[host].flash + } + } } /** diff --git a/docs/appActions.md b/docs/appActions.md index 9bcd4e60735..7f65f767d4e 100644 --- a/docs/appActions.md +++ b/docs/appActions.md @@ -228,6 +228,18 @@ Change a hostPattern's config +### removeSiteSetting(hostPattern, key) + +Removes a site setting + +**Parameters** + +**hostPattern**: `string`, The host pattern to update the config for + +**key**: `string`, The config key to update + + + ### showMessageBox(detail) Shows a message box in the notification bar diff --git a/docs/state.md b/docs/state.md index 6eec7956efc..875bf35f240 100644 --- a/docs/state.md +++ b/docs/state.md @@ -47,7 +47,8 @@ AppStore safeBrowsing: boolean, noScript: boolean, httpsEverywhere: boolean, - fingerprintingProtection: boolean + fingerprintingProtection: boolean, + flash: number, // approval expiration time } }, temporarySiteSettings: { @@ -328,7 +329,7 @@ WindowStore maxHeight: number, // the maximum height of the popup window src: string, // the src for the popup window webview }, - flashEnabled: boolean, // Whether flash is installed and enabled. Cleared on shutdown. + flashInitialized: boolean, // Whether flash was initialized successfully. Cleared on shutdown. cleanedOnShutdown: boolean, // whether app data was successfully cleared on shutdown } ``` diff --git a/js/about/aboutActions.js b/js/about/aboutActions.js index 2dadae4afd3..3ece6a6a0af 100644 --- a/js/about/aboutActions.js +++ b/js/about/aboutActions.js @@ -56,6 +56,22 @@ const AboutActions = { window.dispatchEvent(event) }, + /** + * Dispatches an event to the renderer process to remove a site setting + * + * @param {string} hostPattern - host pattern of site + * @param {string} key - The settings key to change the value on + */ + removeSiteSetting: function (hostPattern, key) { + const event = new window.CustomEvent(messages.CHANGE_SITE_SETTING, { + detail: { + hostPattern, + key + } + }) + window.dispatchEvent(event) + }, + /** * Loads a URL in a new frame in a safe way. * It is important that it is not a simple anchor because it should not diff --git a/js/about/entry.js b/js/about/entry.js index 04e43de251e..d5774bf2137 100644 --- a/js/about/entry.js +++ b/js/about/entry.js @@ -31,6 +31,8 @@ switch (getBaseUrl(getSourceAboutUrl(window.location.href))) { case 'about:error': element = require('./errorPage') break + case 'about:flash': + element = require('./flashPlaceholder') } if (element) { @@ -41,4 +43,3 @@ if (element) { component.setState(e.detail) }) } - diff --git a/js/about/flashPlaceholder.js b/js/about/flashPlaceholder.js new file mode 100644 index 00000000000..d702522362e --- /dev/null +++ b/js/about/flashPlaceholder.js @@ -0,0 +1,79 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const React = require('react') +const ImmutableComponent = require('../components/immutableComponent') +const messages = require('../constants/messages') + +require('../../less/about/flash.less') + +const isDarwin = window.navigator.platform === 'MacIntel' + +class FlashPlaceholder extends ImmutableComponent { + // TODO: Show placeholder telling user how to enable flash if it's not + constructor () { + super() + const braveryDefaults = window.initBraveryDefaults + this.onContextMenu = this.onContextMenu.bind(this) + this.state = { + flashEnabled: braveryDefaults && braveryDefaults.flash ? braveryDefaults.flash.enabled : this.flashEnabled + } + window.addEventListener(messages.BRAVERY_DEFAULTS_UPDATED, (e) => { + this.setState({ + flashEnabled: e.detail && e.detail.flash && e.detail.flash.enabled + }) + }) + } + + get origin () { + // XXX: This is not necessarily the source of the flash, since the + // untrusted page can change the URL fragment. However, the user is + // aware what source they are approving for. + let parts = window.location.href.split('#') + if (parts && parts[1]) { + return parts[1] + } else { + return null + } + } + + get flashEnabled () { + // messages.BRAVERY_DEFAULTS_UPDATED is not received if this is loaded in an + // iframe, which it usually is. as a workaround, get the flash enabled + // state from the parent via an anchor string. + let parts = window.location.href.split('#') + if (parts && parts[2]) { + return parts[2] === 'flashEnabled' + } else { + return false + } + } + + onContextMenu (e) { + if (!this.state.flashEnabled) { + e.preventDefault() + } + } + + render () { + const flashEnabled = this.state.flashEnabled + // TODO: Localization doesn't work due to CORS error from inside iframe + const cmd = isDarwin ? 'Control-Click' : 'Right-Click' + const flashRightClick = flashEnabled ? `${cmd} to run Adobe Flash Player` : 'Adobe Flash has been blocked.' + const flashExpirationText = flashEnabled ? 'For your security, approvals are limited to 1 week.' : null + const flashSubtext = flashEnabled ? `on ${this.origin || 'this site'}.` : 'To run Flash, enable it in Preferences > Security.' + return
+
+ +
{flashRightClick}
+
{flashSubtext}
+
+
+ {flashExpirationText} +
+
+ } +} + +module.exports = diff --git a/js/about/preferences.js b/js/about/preferences.js index 5b49421eb4d..2ce1b565704 100644 --- a/js/about/preferences.js +++ b/js/about/preferences.js @@ -35,14 +35,16 @@ const hintCount = 3 require('../../less/about/preferences.less') require('../../node_modules/font-awesome/css/font-awesome.css') -const permissionNames = ['mediaPermission', - 'geolocationPermission', - 'notificationsPermission', - 'midiSysexPermission', - 'pointerLockPermission', - 'fullscreenPermission', - 'openExternalPermission' -] +const permissionNames = { + 'mediaPermission': 'boolean', + 'geolocationPermission': 'boolean', + 'notificationsPermission': 'boolean', + 'midiSysexPermission': 'boolean', + 'pointerLockPermission': 'boolean', + 'fullscreenPermission': 'boolean', + 'openExternalPermission': 'boolean', + 'flash': 'number' +} const changeSetting = (cb, key, e) => { if (e.target.type === 'checkbox') { @@ -205,7 +207,7 @@ class SyncTab extends ImmutableComponent { class SitePermissionsPage extends React.Component { hasEntryForPermission (name) { return this.props.siteSettings.some((value) => { - return value.get ? typeof value.get(name) === 'boolean' : false + return value.get ? typeof value.get(name) === permissionNames[name] : false }) } @@ -213,8 +215,8 @@ class SitePermissionsPage extends React.Component { // Check whether there is at least one permission set return this.props.siteSettings.some((value) => { if (value && value.get) { - for (let i = 0; i < permissionNames.length; i++) { - if (typeof value.get(permissionNames[i]) === 'boolean') { + for (let name in permissionNames) { + if (typeof value.get(name) === permissionNames[name]) { return true } } @@ -224,16 +226,16 @@ class SitePermissionsPage extends React.Component { } deletePermission (name, hostPattern) { - aboutActions.changeSiteSetting(hostPattern, name, null) + aboutActions.removeSiteSetting(hostPattern, name) } render () { return this.isPermissionsNonEmpty() - ?
+ ?
    { - permissionNames.map((name) => + Object.keys(permissionNames).map((name) => this.hasEntryForPermission(name) ?
  • @@ -244,12 +246,30 @@ class SitePermissionsPage extends React.Component { return null } const granted = value.get(name) - if (typeof granted === 'boolean') { + if (typeof granted === permissionNames[name]) { + let statusText + let statusArgs + if (name === 'flash') { + // Show the number of days/hrs/min til expiration + if (granted === 1) { + // Flash is allowed just one time + statusText = 'flashAllowOnce' + } else { + statusText = 'flashAllowAlways' + statusArgs = { + time: new Date(granted).toLocaleString() + } + } + } else { + statusText = granted ? 'alwaysAllow' : 'alwaysDeny' + } return
    {hostPattern + ': '} - +
    } return null @@ -318,7 +338,6 @@ class PrivacyTab extends ImmutableComponent { -
} } @@ -354,6 +373,7 @@ class SecurityTab extends ImmutableComponent { : }
+
} } @@ -570,10 +590,10 @@ class AboutPreferences extends React.Component { tab = break case preferenceTabs.PRIVACY: - tab = + tab = break case preferenceTabs.SECURITY: - tab = + tab = break case preferenceTabs.BRAVERY: tab = diff --git a/js/actions/appActions.js b/js/actions/appActions.js index 6312ce00596..3ac75c172b1 100644 --- a/js/actions/appActions.js +++ b/js/actions/appActions.js @@ -266,6 +266,19 @@ const appActions = { }) }, + /** + * Removes a site setting + * @param {string} hostPattern - The host pattern to update the config for + * @param {string} key - The config key to update + */ + removeSiteSetting: function (hostPattern, key) { + AppDispatcher.dispatch({ + actionType: AppConstants.APP_REMOVE_SITE_SETTING, + hostPattern, + key + }) + }, + /** * Shows a message box in the notification bar * @param {{message: string, buttons: Array., options: Object}} detail diff --git a/js/components/frame.js b/js/components/frame.js index e8de03a26eb..5f72f0ecb76 100644 --- a/js/components/frame.js +++ b/js/components/frame.js @@ -29,6 +29,7 @@ const { aboutUrls, isSourceAboutUrl, isTargetAboutUrl, getTargetAboutUrl, getBas const { isFrameError } = require('../lib/errorUtil') const locale = require('../l10n') const appConfig = require('../constants/appConfig') +const { getSiteSettingsForHostPattern } = require('../state/siteSettings') class Frame extends ImmutableComponent { constructor () { @@ -39,8 +40,6 @@ class Frame extends ImmutableComponent { this.onFocus = this.onFocus.bind(this) // Maps notification message to its callback this.notificationCallbacks = {} - // Hosts for which Flash is allowed to be detected - this.flashAllowedHosts = {} // Change to DNT requires restart this.doNotTrack = getSetting(settings.DO_NOT_TRACK) } @@ -72,6 +71,8 @@ class Frame extends ImmutableComponent { this.webview.send(messages.PASSWORD_SITE_DETAILS_UPDATED, this.props.allSiteSettings.filter((setting) => setting.get('savePasswords') === false).toJS()) } + } else if (location === 'about:flash') { + this.webview.send(messages.BRAVERY_DEFAULTS_UPDATED, this.props.braveryDefaults) } // send state to about pages @@ -91,12 +92,44 @@ class Frame extends ImmutableComponent { return !!(hack && hack.allowRunningInsecureContent) } - allowRunningPlugins () { - let host = urlParse(this.props.frame.get('location')).host - return !!(host && this.flashAllowedHosts[host]) + allowRunningPlugins (url) { + if (!this.props.flashInitialized) { + return false + } + const origin = url ? siteUtil.getOrigin(url) : this.origin + if (!origin) { + return false + } + // Check for at least one CtP allowed on this origin + if (!this.props.allSiteSettings) { + return false + } + const activeSiteSettings = getSiteSettingsForHostPattern(this.props.allSiteSettings, + origin) + if (activeSiteSettings && typeof activeSiteSettings.get('flash') === 'number') { + return true + } + return false + } + + expireFlash (origin) { + // Expired Flash settings should be deleted when the webview is + // navigated or closed. + const activeSiteSettings = getSiteSettingsForHostPattern(this.props.allSiteSettings, + origin) + if (activeSiteSettings && typeof activeSiteSettings.get('flash') === 'number') { + if (activeSiteSettings.get('flash') < Date.now()) { + // Expired entry. Remove it. + appActions.removeSiteSetting(origin, 'flash') + } + } } - updateWebview (cb) { + componentWillUnmount () { + this.expireFlash(this.origin) + } + + updateWebview (cb, newSrc) { // lazy load webview if (!this.webview && !this.props.isActive && !this.props.isPreview && // allow force loading of new frames @@ -110,6 +143,7 @@ class Frame extends ImmutableComponent { let src = this.props.frame.get('src') let location = this.props.frame.get('location') + newSrc = newSrc || src // Create the webview dynamically because React doesn't whitelist all // of the attributes we need @@ -161,8 +195,10 @@ class Frame extends ImmutableComponent { this.webview.allowRunningPlugins = true } - if (!guestInstanceId || src !== 'about:blank') { - this.webview.setAttribute('src', isSourceAboutUrl(src) ? getTargetAboutUrl(src) : src) + if (!guestInstanceId || newSrc !== 'about:blank') { + // XXX: Should webview src always be set to location, not src? Location + // works for flash CtP, src loads the wrong URL. + this.webview.setAttribute('src', isSourceAboutUrl(newSrc) ? getTargetAboutUrl(newSrc) : newSrc) } if (webviewAdded) { @@ -244,7 +280,16 @@ class Frame extends ImmutableComponent { this.updateAboutDetails() } - if (this.shouldCreateWebview() || this.props.frame.get('src') !== prevProps.frame.get('src')) { + // For cross-origin navigation, clear temp Flash approvals + const prevOrigin = siteUtil.getOrigin(prevProps.frame.get('location')) + if (this.origin !== prevOrigin) { + this.expireFlash(prevOrigin) + } + + if (this.webview && !!this.webview.allowRunningPlugins !== this.allowRunningPlugins()) { + // Flash has been allowed. The location should be reloaded, not the src. + this.updateWebview(cb, this.props.frame.get('location')) + } else if (this.shouldCreateWebview() || this.props.frame.get('src') !== prevProps.frame.get('src')) { this.updateWebview(cb) } else { if (this.runOnDomReady) { @@ -465,46 +510,47 @@ class Frame extends ImmutableComponent { method.apply(this, e.args) }) - const interceptFlash = (url) => { + const interceptFlash = (adobeUrl) => { this.webview.stop() // Generate a random string that is unlikely to collide. Not // cryptographically random. const nonce = Math.random().toString() - if (this.props.flashEnabled) { - const parsedUrl = urlParse(this.props.frame.get('location')) - const host = parsedUrl.host - if (!host) { + if (this.props.flashInitialized) { + if (!this.origin) { return } - const message = `Allow ${host} to run Flash Player?` + const message = `Allow ${this.origin} to run Flash Player?` // Show Flash notification bar appActions.showMessageBox({ buttons: [locale.translation('deny'), locale.translation('allow')], message, options: { - nonce + nonce, + persist: true } }) - this.notificationCallbacks[message] = (buttonIndex) => { + this.notificationCallbacks[message] = (buttonIndex, persist) => { if (buttonIndex === 1) { - this.flashAllowedHosts[host] = true - parsedUrl.search = parsedUrl.search || 'brave_flash_allowed' - if (!parsedUrl.search.includes('brave_flash_allowed')) { - parsedUrl.search = parsedUrl.search + '&brave_flash_allowed' + if (persist) { + appActions.changeSiteSetting(this.origin, 'flash', Date.now() + 7 * 24 * 1000 * 3600) + } else { + appActions.changeSiteSetting(this.origin, 'flash', 1) } - windowActions.loadUrl(this.props.frame, parsedUrl.format()) } else { appActions.hideMessageBox(message) + if (persist) { + // TODO: Never show this message again on this domain? + } } } } else { ipc.send(messages.SHOW_FLASH_INSTALLED_MESSAGE) - windowActions.loadUrl(this.props.frame, url) + windowActions.loadUrl(this.props.frame, adobeUrl) } - ipc.once(messages.NOTIFICATION_RESPONSE + nonce, (e, msg, buttonIndex) => { + ipc.once(messages.NOTIFICATION_RESPONSE + nonce, (e, msg, buttonIndex, persist) => { const cb = this.notificationCallbacks[msg] if (cb) { - cb(buttonIndex) + cb(buttonIndex, persist) } }) } @@ -513,22 +559,14 @@ class Frame extends ImmutableComponent { const parsedUrl = urlParse(e.url) // Instead of telling person to install Flash, ask them if they want to // run Flash if it's installed. - const currentUrl = urlParse(this.props.frame.get('location')) - if ((e.url.includes('//get.adobe.com/flashplayer') || - e.url.includes('//www.adobe.com/go/getflashplayer')) && - ['http:', 'https:'].includes(currentUrl.protocol) && - !currentUrl.hostname.includes('.adobe.com')) { - interceptFlash(e.url) - } - // Make sure a page that is trying to run Flash is actually allowed - if (parsedUrl.search && parsedUrl.search.includes('brave_flash_allowed')) { - if (!(parsedUrl.host in this.flashAllowedHosts)) { - this.webview.stop() - parsedUrl.search = parsedUrl.search.replace(/(\?|&)?brave_flash_allowed/, '') - windowActions.loadUrl(this.props.frame, parsedUrl.format()) - } - } if (e.isMainFrame && !e.isErrorPage && !e.isFrameSrcDoc) { + const currentUrl = urlParse(this.props.frame.get('location')) + if ((e.url.includes('//get.adobe.com/flashplayer') || + e.url.includes('//www.adobe.com/go/getflashplayer')) && + ['http:', 'https:'].includes(currentUrl.protocol) && + !currentUrl.hostname.includes('.adobe.com')) { + interceptFlash(e.url) + } windowActions.onWebviewLoadStart(this.props.frame, e.url) const isSecure = parsedUrl.protocol === 'https:' && !this.allowRunningInsecureContent() windowActions.setSecurityState(this.props.frame, { @@ -601,6 +639,7 @@ class Frame extends ImmutableComponent { loadStart(e) }) this.webview.addEventListener('load-start', (e) => { + // XXX: loadstart probably does not need to be called twice anymore. loadStart(e) }) this.webview.addEventListener('did-navigate', (e) => { @@ -710,8 +749,7 @@ class Frame extends ImmutableComponent { } get origin () { - const parsedUrl = urlParse(this.props.frame.get('location')) - return `${parsedUrl.protocol}//${parsedUrl.host}` + return siteUtil.getOrigin(this.props.frame.get('location')) } onFocus () { diff --git a/js/components/main.js b/js/components/main.js index f2e5355c8ac..a9e6527e409 100644 --- a/js/components/main.js +++ b/js/components/main.js @@ -809,7 +809,7 @@ class Main extends ImmutableComponent { .includes(siteTags.BOOKMARK_FOLDER)) || new Immutable.Map() : null} passwords={this.props.appState.get('passwords')} - flashEnabled={this.props.appState.get('flashEnabled')} + flashInitialized={this.props.appState.get('flashInitialized')} allSiteSettings={allSiteSettings} frameSiteSettings={this.frameSiteSettings(frame.get('location'))} enableNoScript={this.enableNoScript(this.frameSiteSettings(frame.get('location')))} diff --git a/js/components/notificationBar.js b/js/components/notificationBar.js index d46f1ba3cb5..79df00fe6ab 100644 --- a/js/components/notificationBar.js +++ b/js/components/notificationBar.js @@ -13,7 +13,8 @@ class NotificationItem extends ImmutableComponent { const nonce = this.props.detail.get('options').get('nonce') if (nonce) { ipc.emit(messages.NOTIFICATION_RESPONSE + nonce, {}, - this.props.detail.get('message'), buttonIndex) + this.props.detail.get('message'), + buttonIndex, this.checkbox ? this.checkbox.checked : false) } else { ipc.send(messages.NOTIFICATION_RESPONSE, this.props.detail.get('message'), buttonIndex, this.checkbox ? this.checkbox.checked : false) diff --git a/js/constants/appConfig.js b/js/constants/appConfig.js index 04d2f664e30..00242df10ed 100644 --- a/js/constants/appConfig.js +++ b/js/constants/appConfig.js @@ -1,6 +1,7 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this file, * You can obtain one at http://mozilla.org/MPL/2.0/. */ +const { getTargetAboutUrl } = require('../lib/appUrlUtil') // BRAVE_UPDATE_HOST should be set to the host name for the auto-updater server const updateHost = process.env.BRAVE_UPDATE_HOST || 'https://brave-laptop-updates.global.ssl.fastly.net' @@ -29,7 +30,8 @@ module.exports = { enabled: false }, flash: { - enabled: false + enabled: false, + url: getTargetAboutUrl('about:flash') }, adblock: { url: 'https://s3.amazonaws.com/adblock-data/{version}/ABPFilterParserData.dat', diff --git a/js/constants/appConstants.js b/js/constants/appConstants.js index b69b419672a..c3f049885b4 100644 --- a/js/constants/appConstants.js +++ b/js/constants/appConstants.js @@ -26,6 +26,7 @@ const AppConstants = { APP_SET_UPDATE_STATUS: _, APP_CHANGE_SETTING: _, APP_CHANGE_SITE_SETTING: _, + APP_REMOVE_SITE_SETTING: _, APP_SHOW_MESSAGE_BOX: _, /** @param {Object} detail */ APP_HIDE_MESSAGE_BOX: _, /** @param {string} message */ APP_ADD_WORD: _, /** @param {string} word, @param {boolean} learn */ diff --git a/js/constants/messages.js b/js/constants/messages.js index d766ddfe6e9..21038a81bfe 100644 --- a/js/constants/messages.js +++ b/js/constants/messages.js @@ -115,6 +115,7 @@ const messages = { // About pages from contentScript CHANGE_SETTING: _, CHANGE_SITE_SETTING: _, + REMOVE_SITE_SETTING: _, NEW_FRAME: _, MOVE_SITE: _, OPEN_DOWNLOAD_PATH: _, diff --git a/js/contextMenus.js b/js/contextMenus.js index c72c0c0b718..4bb5731bcb9 100644 --- a/js/contextMenus.js +++ b/js/contextMenus.js @@ -674,6 +674,22 @@ const showDefinitionMenuItem = (selectionText) => { function mainTemplateInit (nodeProps, frame) { const template = [] + if (nodeProps.frameURL && nodeProps.frameURL.startsWith('chrome-extension://mnojpmjdmbbfmejpflffifhffcmidifd/about-flash.html')) { + const pageOrigin = siteUtil.getOrigin(nodeProps.pageURL) + template.push({ + label: locale.translation('allowFlashOnce'), + click: () => { + appActions.changeSiteSetting(pageOrigin, 'flash', 1) + } + }, { + label: locale.translation('allowFlashAlways'), + click: () => { + const expirationTime = Date.now() + 7 * 24 * 3600 * 1000 + appActions.changeSiteSetting(pageOrigin, 'flash', expirationTime) + } + }) + return template + } if (nodeProps.linkURL !== '') { template.push(openInNewTabMenuItem(nodeProps.linkURL, frame.get('isPrivate'), frame.get('partitionNumber'), frame.get('key')), diff --git a/js/lib/appUrlUtil.js b/js/lib/appUrlUtil.js index f8fc3780c50..fc66c7552ed 100644 --- a/js/lib/appUrlUtil.js +++ b/js/lib/appUrlUtil.js @@ -67,6 +67,7 @@ module.exports.aboutUrls = new Immutable.Map({ 'about:certerror': module.exports.getAppUrl('about-certerror.html'), 'about:safebrowsing': module.exports.getAppUrl('about-safebrowsing.html'), 'about:passwords': module.exports.getAppUrl('about-passwords.html'), + 'about:flash': module.exports.getAppUrl('about-flash.html'), 'about:error': module.exports.getAppUrl('about-error.html') }) diff --git a/js/state/contentSettings.js b/js/state/contentSettings.js index 6506b75dac1..a33ad96703b 100644 --- a/js/state/contentSettings.js +++ b/js/state/contentSettings.js @@ -79,6 +79,14 @@ const getContentSettingsFromSiteSettings = (appState) => { canvasFingerprinting: [{ setting: braveryDefaults.fingerprintingProtection ? 'block' : 'allow', primaryPattern: '*' + }], + flashEnabled: [{ + setting: braveryDefaults.flash ? 'allow' : 'block', + primaryPattern: '*' + }], + flashActive: [{ + setting: 'block', + primaryPattern: '*' }] } @@ -103,6 +111,9 @@ const getContentSettingsFromSiteSettings = (appState) => { if (hostSetting.adControl) { addContentSettings(contentSettings.adInsertion, hostPattern, '*', hostSetting.adControl === 'showBraveAds' ? 'allow' : 'block') } + if (typeof hostSetting.flash === 'number') { + addContentSettings(contentSettings.flashActive, hostPattern, '*', 'allow') + } // these should always be the last rules so they take precendence over the others if (hostSetting.shieldsUp === false) { @@ -124,6 +135,11 @@ const doAction = (action) => { updateTrigger('content_settings', action.temporary) }) break + case AppConstants.APP_REMOVE_SITE_SETTING: + AppDispatcher.waitFor([AppStore.dispatchToken], () => { + updateTrigger('content_settings', action.temporary) + }) + break case AppConstants.APP_SET_RESOURCE_ENABLED: AppDispatcher.waitFor([AppStore.dispatchToken], () => { updateTrigger() diff --git a/js/state/siteSettings.js b/js/state/siteSettings.js index 62edfcb77d4..401698a6f22 100644 --- a/js/state/siteSettings.js +++ b/js/state/siteSettings.js @@ -200,3 +200,17 @@ module.exports.mergeSiteSetting = (siteSettings, hostPattern, key, value) => */ module.exports.removeSiteSettings = (siteSettings, hostPattern) => siteSettings.delete(hostPattern) + +/** + * Removes one site setting for the specified hostPattern. + * @param {Object} siteSettings - The top level app state site settings indexed by hostPattern. + * @param {string} hostPattern - The host pattern to remove all settings for. + * @param {string} key - The site setting name + */ +module.exports.removeSiteSetting = (siteSettings, hostPattern, key) => { + if (siteSettings.get(hostPattern)) { + return siteSettings.set(hostPattern, siteSettings.get(hostPattern).delete(key)) + } else { + return siteSettings + } +} diff --git a/js/stores/appStore.js b/js/stores/appStore.js index 0bf33a0d59f..adb1d967651 100644 --- a/js/stores/appStore.js +++ b/js/stores/appStore.js @@ -454,6 +454,11 @@ const handleAppAction = (action) => { appState = appState.set(propertyName, siteSettings.mergeSiteSetting(appState.get(propertyName), action.hostPattern, action.key, action.value)) break + case AppConstants.APP_REMOVE_SITE_SETTING: + let newSiteSettings = siteSettings.removeSiteSetting(appState.get('siteSettings'), + action.hostPattern, action.key) + appState = appState.set('siteSettings', newSiteSettings) + break case AppConstants.APP_SHOW_MESSAGE_BOX: let notifications = appState.get('notifications') appState = appState.set('notifications', notifications.push(Immutable.fromJS(action.detail))) diff --git a/less/about/flash.less b/less/about/flash.less new file mode 100644 index 00000000000..dc465ecf980 --- /dev/null +++ b/less/about/flash.less @@ -0,0 +1,32 @@ +@import "./common.less"; + +.flashMainContent { + flex-direction: column; + display: flex; + justify-content: center; + align-items: center; + font-size: 13pt; + position: relative; + top: 45%; + transform: translateY(-50%); +} + +#flashRightClick { + font-weight: bold; +} + +.flashSubtext { + color: #666; +} + +.flashFooter { + position: absolute; + width: 100%; + bottom: 25px; + text-align: center; + color: #666; +} + +#appContainer { + background: linear-gradient(to bottom, #fff 0%, #aaa 100%); +} diff --git a/less/about/preferences.less b/less/about/preferences.less index 4f84d5cb412..0f0aaa84fec 100644 --- a/less/about/preferences.less +++ b/less/about/preferences.less @@ -229,6 +229,10 @@ input[type="checkbox"][disabled] { margin: 20px; } +#sitePermissionsPage { + padding-top: 20px; +} + .permissionAction { cursor: pointer; color: @braveOrange;