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;