diff --git a/.npmrc b/.npmrc index e77c744f1b3..0e33a9703c0 100644 --- a/.npmrc +++ b/.npmrc @@ -1,7 +1,7 @@ runtime = electron target_arch = x64 -brave_electron_version = 5.0.2 -chromedriver_version = 2.33 -target = v5.0.2 +brave_electron_version = 6.0.12 +chromedriver_version = 2.36 +target = v6.0.12 disturl=https://brave-laptop-binaries.s3.amazonaws.com/atom-shell/dist/ build_from_source = true diff --git a/.travis.yml b/.travis.yml index 90095b2dd4c..316aa6788c3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -21,18 +21,35 @@ notifications: slack: secure: bDwO2uce5JAZvjrvWj4+/+yEXJAIK4O0RcgUWvZ2IMbi7Q9I89Mw40JmkLWL6x2gWZwxr8+FoLtErJA7RVrsfImjrX+NmMyAB7AydLdrBJtkLozNnuacnhcnBRyp1gGCa1ymxCEXGbgC6onAD3kiJJhggr70T+2lu3IuJYXENhc= env: - - CXX=g++-4.8 NODE_ENV=test TEST_DIR=lint - - CXX=g++-4.8 NODE_ENV=test TEST_DIR=unit - - CXX=g++-4.8 NODE_ENV=test TEST_DIR=codecov - - CXX=g++-4.8 NODE_ENV=test TEST_DIR=about - - CXX=g++-4.8 NODE_ENV=test TEST_DIR=app - - CXX=g++-4.8 NODE_ENV=test TEST_DIR=bookmark-components - - CXX=g++-4.8 NODE_ENV=test TEST_DIR=bravery-components - - CXX=g++-4.8 NODE_ENV=test TEST_DIR=contents - - CXX=g++-4.8 NODE_ENV=test TEST_DIR=misc-components - - CXX=g++-4.8 NODE_ENV=test TEST_DIR=navbar-components - - CXX=g++-4.8 NODE_ENV=test TEST_DIR=tab-components - - CXX=g++-4.8 NODE_ENV=test TEST_DIR=performance ARTIFACTS_REGION=us-east-1 + global: + - CXX=g++-4.8 + - NODE_ENV=test + matrix: + - TEST_DIR=lint + - TEST_DIR=unit + - TEST_DIR=codecov + - TEST_DIR=security + - TEST_DIR=about + - TEST_DIR=app + - TEST_DIR=bookmark-components + - TEST_DIR=bravery-components + - TEST_DIR=contents + - TEST_DIR=misc-components + - TEST_DIR=navbar-components + - TEST_DIR=tab-components + - TEST_DIR=performance ARTIFACTS_REGION=us-east-1 +matrix: + fast_finish: true + allow_failures: + - env: TEST_DIR=about + - env: TEST_DIR=app + - env: TEST_DIR=bookmark-components + - env: TEST_DIR=bravery-components + - env: TEST_DIR=contents + - env: TEST_DIR=misc-components + - env: TEST_DIR=navbar-components + - env: TEST_DIR=tab-components + - env: TEST_DIR=performance ARTIFACTS_REGION=us-east-1 addons: apt: sources: diff --git a/CHANGELOG.md b/CHANGELOG.md index 86a44e2a242..42aed62ad89 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,161 @@ # Changelog +## [0.22.721](https://github.com/brave/browser-laptop/releases/tag/v0.22.721dev) + + - Fixed partner subscription page no longer active tab on initial launch. ([#14220](https://github.com/brave/browser-laptop/issues/14220)) + - Fixed tabs opened via links with target attribute not always rendering. ([#14159](https://github.com/brave/browser-laptop/issues/14159)) + - Fixed fingerprinting protection breaks the Uphold verification process. ([#14152](https://github.com/brave/browser-laptop/issues/14152)) + - Fixed toggling shields breaks the security indicator on URLs that include a hash. ([#14231](https://github.com/brave/browser-laptop/issues/14231)) + - Fixed opening extension background pages from the URL bar causes Brave to crash. ([#14079](https://github.com/brave/browser-laptop/issues/14079)) + - Fixed tabs becoming unresponsive when detaching into separate window. ([#14031](https://github.com/brave/browser-laptop/issues/14031)) + - Fixed disabling TorrentViewer doesn't disable torrent handling. ([#10767](https://github.com/brave/browser-laptop/issues/10767)) + - Upgraded to muon 6.0.12. ([#14154](https://github.com/brave/browser-laptop/issues/14154)) + - Upgraded to Chromium 66.0.3359.170. ([#14125](https://github.com/brave/browser-laptop/issues/14125)) + +## [0.22.714](https://github.com/brave/browser-laptop/releases/tag/v0.22.714dev) + + - Improved performance for browser startup and general UI, especially with a high number of tabs opened. ([#13216](https://github.com/brave/browser-laptop/issues/13216)) + - Added "Include site in Brave Payments" right-click item for history and bookmarks. ([#6547](https://github.com/brave/browser-laptop/issues/6547)) + - Added notification when contribution is pushed back due to minimum 30min browsing time not being met. ([#14000](https://github.com/brave/browser-laptop/issues/14000)) + - Added an option to disable WebRTC. ([#13668](https://github.com/brave/browser-laptop/issues/13668)) + - Added a reminder notification to backup Payments wallet when funds are added for the first time. ([#13425](https://github.com/brave/browser-laptop/issues/13425)) + - Added HSTS fingerprinting mitigation. ([#12223](https://github.com/brave/browser-laptop/issues/12223)) + - Added error under Payments when connection to the ledger server fails. ([#13972](https://github.com/brave/browser-laptop/issues/13972)) + - Fixed performance issues when closing tab towards the beginning of a tab strip. ([#14081](https://github.com/brave/browser-laptop/issues/14081)) + - Fixed bookmark bar staying visible after all bookmarks have been removed. ([#14047](https://github.com/brave/browser-laptop/issues/14047)) + - Fixed crash on macOS when selecting "Share" under the file menu when there's no windows opened. ([#13928](https://github.com/brave/browser-laptop/issues/13928)) + - Fixed several broken cases when adding websites via the publisher toggle button. ([#13879](https://github.com/brave/browser-laptop/issues/13879)) + - Fixed ledger table sort based on contribution percentage. ([#13721](https://github.com/brave/browser-laptop/issues/13721)) + - Fixed active tabs sometimes appearing white and not loading during startup. ([#13679](https://github.com/brave/browser-laptop/issues/13679)) + - Fixed bookmark search. ([#13322](https://github.com/brave/browser-laptop/issues/13322)) + - Fixed background process not correctly terminating when closing Brave after importing data. ([#13422](https://github.com/brave/browser-laptop/issues/13422)) + - Fixed background process not correctly terminating when closing Brave using titlebar after bookmarking. ([#13277](https://github.com/brave/browser-laptop/issues/13277)) + - Fixed PDFs not being added into Payments. ([#12792](https://github.com/brave/browser-laptop/issues/12792)) + - Fixed large balances not being displayed correctly under Payments. ([#12220](https://github.com/brave/browser-laptop/issues/12220)) + - Fixed loading icon being displayed in ad slots where ads have been removed on Marketwatch. ([#11730](https://github.com/brave/browser-laptop/issues/11730)) + - Fixed crash when using "Mute other tabs" with large profiles. ([#14058](https://github.com/brave/browser-laptop/issues/14058)) + - Fixed tabs become unmuted when dragged to a new window. ([#13979](https://github.com/brave/browser-laptop/issues/13979)) + - Fixed unable to open private tab using keyboard shortcut from a PDF tab. ([#4024](https://github.com/brave/browser-laptop/issues/4024)) + - Fixed escape key not closing the add funds dialog under Payments. ([#3800](https://github.com/brave/browser-laptop/issues/3800)) + +## [0.22.669](https://github.com/brave/browser-laptop/releases/tag/v0.22.669dev) + + - Fixed crashing when submitting online forms. ([#13947](https://github.com/brave/browser-laptop/issues/13947)) + - Improved accuracy of recorded time in Brave Payments when watching Twitch. ([#13828](https://github.com/brave/browser-laptop/issues/13828)) + - Fixed updates sometimes failing on macOS. ([#2184](https://github.com/brave/browser-laptop/issues/2184)) + - Fixed transactions not being recorded in ledger-state.json. ([#13544](https://github.com/brave/browser-laptop/issues/13544)) + - Fixed tabs and windows opening in minimized state when using Brave menu items while all windows have been minimized. ([#13865](https://github.com/brave/browser-laptop/issues/13865)) + - Fixed inability to open windows using the dock on macOS when there are no Brave windows opened. ([#13860](https://github.com/brave/browser-laptop/issues/13860)) + - Fixed inability to open hyperlinks on macOS when there are no Brave windows opened. ([#13859](https://github.com/brave/browser-laptop/issues/13859)) + - Fixed inability to open new tabs using the file menu on macOS when there are no Brave windows opened. ([#13689](https://github.com/brave/browser-laptop/issues/13689)) + - Upgraded to muon 5.2.7. ([#13964](https://github.com/brave/browser-laptop/issues/13964)) + - Upgraded to Chromium 66.0.3359.117. ([#13912](https://github.com/brave/browser-laptop/issues/13912)) + +## [0.22.22](https://github.com/brave/browser-laptop/releases/tag/v0.22.22dev) + + - Fixed missing static variable on macOS for first-run referral program installs. ([#13853](https://github.com/brave/browser-laptop/issues/13853)) + +## [0.22.21](https://github.com/brave/browser-laptop/releases/tag/v0.22.21dev) + + - Improvements to the referral program. ([#13754](https://github.com/brave/browser-laptop/issues/13754)) + - Reverted quarantine feature for macOS. ([#13826](https://github.com/brave/browser-laptop/issues/13826)) + +## [0.22.13](https://github.com/brave/browser-laptop/releases/tag/v0.22.13dev) + + - Added a new recovery dialog with information when detecting corrupted seed. ([#13424](https://github.com/brave/browser-laptop/issues/13424)) + - Added U2F support for various websites. ([#518](https://github.com/brave/browser-laptop/issues/518)) + - Added "no thanks" to Brave Payment notifications. ([#12758](https://github.com/brave/browser-laptop/issues/12758)) + - Added "always block" option to autoplay. ([#9755](https://github.com/brave/browser-laptop/issues/9755)) + - Added "in progress" status under Brave Payments when contribution in progress. ([#13423](https://github.com/brave/browser-laptop/issues/13423)) + - Added keyboard shortcuts to re-order tabs. ([#11310](https://github.com/brave/browser-laptop/issues/11310)) + - Added EV certificate status in URL bar. ([#791](https://github.com/brave/browser-laptop/issues/791)) + - Added new look for tab previews when hovering. ([#12454](https://github.com/brave/browser-laptop/issues/12454)) + - Optimized opening new windows. ([#12437](https://github.com/brave/browser-laptop/issues/12437)) + - Updated HTTPS Everywhere. ([#11598](https://github.com/brave/browser-laptop/issues/11598)) + - Disabled WebUSB API. ([#13374](https://github.com/brave/browser-laptop/issues/13374)) + - Fixed printing issues under Windows. ([#13546](https://github.com/brave/browser-laptop/issues/13546)) + - Fixed scrolling sometimes becoming stuck. ([#13580](https://github.com/brave/browser-laptop/issues/13580)) + - Fixed pinned site contributions so they are excluded from statistical voting. ([#13431](https://github.com/brave/browser-laptop/issues/13431)) + - Fixed Twitch not being added into the ledger table. ([#13597](https://github.com/brave/browser-laptop/issues/13597)) + - Fixed Twitch not being added into the ledger table when resuming after pausing a video. ([#13257](https://github.com/brave/browser-laptop/issues/13257)) + - Fixed adding remaining time into the ledger table when closing Twitch media. ([#13259](https://github.com/brave/browser-laptop/issues/13259)) + - Fixed downloaded files on macOS not being quarantined by default. ([#13088](https://github.com/brave/browser-laptop/issues/13088)) + - Fixed tab preview animation under Windows. ([#2869](https://github.com/brave/browser-laptop/issues/2869)) + - Fixed tab content should be discarded when memory usage is too high. ([#13210](https://github.com/brave/browser-laptop/issues/13210)) + - Fixed backing up wallet to file creates recovery file in AppData. ([#11419](https://github.com/brave/browser-laptop/issues/11419)) + - Fixed print dialog not being displayed when printing recovery keys. ([#7512](https://github.com/brave/browser-laptop/issues/7512)) + - Fixed save recovery file opens new tab. ([#7511](https://github.com/brave/browser-laptop/issues/7511)) + - Fixed crash when using overrides in developer tools. ([#13521](https://github.com/brave/browser-laptop/issues/13521)) + - Fixed incorrect origin being displayed on external application permission requests when using invalid URL protocols. ([#13471](https://github.com/brave/browser-laptop/issues/13471)) + - Fixed issues when resizing "Save Download As..." dialog under macOS. ([#13470](https://github.com/brave/browser-laptop/issues/13470)) + - Fixed save dialog causing lockup on macOS. ([#3313](https://github.com/brave/browser-laptop/issues/3313)) + - Fixed "Save Page..." dialog under macOS. ([#12628](https://github.com/brave/browser-laptop/issues/12628)) + - Fixed "Save As..." dialog appearing twice when offline. ([#3784](https://github.com/brave/browser-laptop/issues/3784)) + - Fixed "Save As..." dialog can't be closed with ESC key. ([#3864](https://github.com/brave/browser-laptop/issues/3864)) + - Fixed "Save As..." field not scrollable when downloading long filenames. ([#10924](https://github.com/brave/browser-laptop/issues/10924)) + - Fixed "Save Image..." randomly crashes the browser. ([#10653](https://github.com/brave/browser-laptop/issues/10653)) + - Fixed file picker not working under Windows. ([#9409](https://github.com/brave/browser-laptop/issues/9409)) + - Fixed file uploader not showing any image previews under Linux. ([#8854](https://github.com/brave/browser-laptop/issues/8854)) + - Fixed set file type filter when presenting a save dialog. ([#8096](https://github.com/brave/browser-laptop/issues/8096)) + - Fixed contribution notification displaying again after toggle payments off/on. ([#13287](https://github.com/brave/browser-laptop/issues/13287)) + - Fixed bad session state can result in permanently corrupted profile. ([#13261](https://github.com/brave/browser-laptop/issues/13261)) + - Fixed issues displaying long publisher names in the ledger table. ([#13195](https://github.com/brave/browser-laptop/issues/13195)) + - Fixed publishers should not make requests to Youtube, Twitch, etc in the main process. ([#13114](https://github.com/brave/browser-laptop/issues/13114)) + - Fixed top site tiles not being populated when updating profile from an older version of Brave. ([#12941](https://github.com/brave/browser-laptop/issues/12941)) + - Fixed theme color being incorrectly displayed due to transparency. ([#12803](https://github.com/brave/browser-laptop/issues/12803)) + - Fixed favicons being distorted. ([#12722](https://github.com/brave/browser-laptop/issues/12722)) + - Fixed tab background gradient visible when switching tabs. ([#12721](https://github.com/brave/browser-laptop/issues/12721)) + - Fixed network requests should use non-persistent session by default, ([#12469](https://github.com/brave/browser-laptop/issues/12469)) + - Fixed closing tab switches to second tab from root tab. ([#11981](https://github.com/brave/browser-laptop/issues/11981)) + - Fixed tabs settings options not working after the 0.19.48 update. ([#11526](https://github.com/brave/browser-laptop/issues/11526)) + - Fixed tab animations run again when tab is being moved. ([#11470](https://github.com/brave/browser-laptop/issues/11470)) + - Fixed "Save Torrent" button. ([#11452](https://github.com/brave/browser-laptop/issues/11452)) + - Fixed autocomplete search results displaying incorrect results when typing non-ASCII characters. ([#11297](https://github.com/brave/browser-laptop/issues/11297)) + - Fixed tab colour has weird fade in paint effect when switching. ([#11249](https://github.com/brave/browser-laptop/issues/11249)) + - Fixed default download path not working. ([#10260](https://github.com/brave/browser-laptop/issues/10260)) + - Fixed default download path should be set to the downloads folder. ([#9822](https://github.com/brave/browser-laptop/issues/9822)) + - Fixed IME results are shown before selection is made. ([#9139](https://github.com/brave/browser-laptop/issues/9139)) + - Fixed pinned tabs losing order after re-launch. ([#8543](https://github.com/brave/browser-laptop/issues/8543)) + - Fixed file download hangs if Brave does not have write permission to target directory. ([#7747](https://github.com/brave/browser-laptop/issues/7747)) + - Fixed file extensions not auto added to some downloads. ([#1985](https://github.com/brave/browser-laptop/issues/1985)) + - Upgraded to muon 5.1.2. ([#12429](https://github.com/brave/browser-laptop/issues/12429)) + - Upgraded to Chromium 65.0.3325.181. ([#13572](https://github.com/brave/browser-laptop/issues/13572)) + +## [0.21.24](https://github.com/brave/browser-laptop/releases/tag/v0.21.24dev) + + - Fixed bugs with ledger session corruption. ([#13330](https://github.com/brave/browser-laptop/issues/13330)) + - Fixed possible error when trying to save password. ([#13408](https://github.com/brave/browser-laptop/issues/13408)) + - Fixed tab refresh not releasing memory. ([#13273](https://github.com/brave/browser-laptop/issues/13273)) + - Upgraded to muon 4.9.3. ([#13357](https://github.com/brave/browser-laptop/issues/13357)) + - Upgraded to Chromium 65.0.3325.162. ([#13358](https://github.com/brave/browser-laptop/issues/13358)) + +## [0.21.18](https://github.com/brave/browser-laptop/releases/tag/v0.21.18dev) + + - Added the ability to pay Twitch publishers with BAT. ([#13139](https://github.com/brave/browser-laptop/issues/13139)) + - There are three things related to Twitch that will be fixed or improved later: i) Paused videos are currently counted for watch time, ii) favicons associated with streams are not shown in the payments panel when watching live video, iii) calculating watch time will be improved when seeking. + - Added the ability to sort publishers using the verified publisher column under payments. ([#10752](https://github.com/brave/browser-laptop/issues/10752)) + - Moved publishers that have been deleted from payments into a new dialog. ([#12833](https://github.com/brave/browser-laptop/issues/12833)) + - Fixed localization issue when backing up recovery key. ([#13311](https://github.com/brave/browser-laptop/issues/13311)) + - Fixed favicons not being consistently displayed under payments. ([#13281](https://github.com/brave/browser-laptop/issues/13281)) + - Fixed publisher pinning issue when brave profile has been corrupted. ([#13134](https://github.com/brave/browser-laptop/issues/13134)) + - Fixed the actions column under payments being clickable as it's not sortable. ([#13074](https://github.com/brave/browser-laptop/issues/13074)) + - Fixed token promotions notifying users who have disabled promotion notifications in the advanced settings. ([#13021](https://github.com/brave/browser-laptop/issues/13021)) + - Fixed "show notifications" under advanced settings being re-enabled when disabling/enabling payments. ([#12817](https://github.com/brave/browser-laptop/issues/12817)) + - Fixed publishers being auto included under payments even though "show only included sites" has been disabled. ([#12766](https://github.com/brave/browser-laptop/issues/12766)) + - Fixed incorrect decimal value being displayed when balance under payments is below a certain amount. ([#12666](https://github.com/brave/browser-laptop/issues/12666)) + - Fixed pinned publishers not always being displayed under payments. ([#12584](https://github.com/brave/browser-laptop/issues/12584)) + - Fixed "never include" wording under context menu when right clicking on publishers under payments. ([#12296](https://github.com/brave/browser-laptop/issues/12296)) + - Fixed incorrect amount being deducted from wallet during payment processing. ([#12183](https://github.com/brave/browser-laptop/issues/12183)) + - Fixed manually entered values for publishers under payments not being retained. ([#11238](https://github.com/brave/browser-laptop/issues/11238)) + +## [0.20.46](https://github.com/brave/browser-laptop/releases/tag/v0.20.46dev) + + - Fixed YouTube videos lagging while playing. ([#13079](https://github.com/brave/browser-laptop/issues/13079)) + - Fixed several performance issues. ([#13087](https://github.com/brave/browser-laptop/issues/13087)) + - Fixed about: pages not working after long usage. ([#12828](https://github.com/brave/browser-laptop/issues/12828)) + - Fixed back button not working after long usage. ([#13157](https://github.com/brave/browser-laptop/issues/13157)) + - Fixed reload button not working after long usage. ([#12333](https://github.com/brave/browser-laptop/issues/12333)) + ## [0.20.42](https://github.com/brave/browser-laptop/releases/tag/v0.20.42dev) - Fixed websites randomly being unpinned under Payments. ([#13102](https://github.com/brave/browser-laptop/issues/13102)) diff --git a/README.md b/README.md index 37a8389ec3a..b0e8c45f9e3 100644 --- a/README.md +++ b/README.md @@ -73,6 +73,13 @@ dnf install rpm-build dnf group install "Development Tools" "C Development Tools and Libraries" ```` +#### On Solus + +```` +sudo eopkg it -c system.devel gconf +```` + + ### Installation After installing the prerequisites: @@ -108,12 +115,12 @@ Some platforms are available as pre-configured VMs. See the [readme](https://git ### Running Brave -To run a development version of the browser requires a few steps. The easiest way is just to use two -terminals. One terminal can be used just to watch for changes to the code +Running a development version of the browser requires two steps. The easiest way is just to use two +terminals (or one terminal with two tabs). First, you'll need to start the watch process (which runs webpack and watches for changes to the code) npm run watch -Now actually run Brave in another terminal +Second, you can start the actual Brave process (in another terminal or tab) npm start diff --git a/app/browser/activeTabHistory.js b/app/browser/activeTabHistory.js index a3650face01..9569c9a5197 100644 --- a/app/browser/activeTabHistory.js +++ b/app/browser/activeTabHistory.js @@ -55,6 +55,18 @@ const api = { */ clearTabbedWindow: function (windowId) { activeTabsByWindow.delete(windowId) + }, + + /** + * Replace all intances of `oldTabId` with `newTabId` in + * active tab history for each window + */ + tabIdChanged: function (oldTabId, newTabId) { + for (const [windowId, windowActiveTabs] of activeTabsByWindow) { + if (windowActiveTabs && windowActiveTabs.length) { + activeTabsByWindow.set(windowId, windowActiveTabs.map(previousTabId => (previousTabId === oldTabId) ? newTabId : previousTabId)) + } + } } } diff --git a/app/browser/api/ledger.js b/app/browser/api/ledger.js index 1c24e2e3f97..358de7e1b88 100644 --- a/app/browser/api/ledger.js +++ b/app/browser/api/ledger.js @@ -24,21 +24,22 @@ const BigNumber = require('bignumber.js') const appActions = require('../../../js/actions/appActions') // State +const aboutPreferencesState = require('../../common/state/aboutPreferencesState') const ledgerState = require('../../common/state/ledgerState') const pageDataState = require('../../common/state/pageDataState') -const migrationState = require('../../common/state/migrationState') const updateState = require('../../common/state/updateState') // Constants const settings = require('../../../js/constants/settings') const messages = require('../../../js/constants/messages') +const ledgerStatuses = require('../../common/constants/ledgerStatuses') // Utils const config = require('../../../js/constants/buildConfig') const tabs = require('../../browser/tabs') const locale = require('../../locale') const getSetting = require('../../../js/settings').getSetting -const {fileUrl, getSourceAboutUrl, isSourceAboutUrl} = require('../../../js/lib/appUrlUtil') +const {getSourceAboutUrl, isSourceAboutUrl} = require('../../../js/lib/appUrlUtil') const urlParse = require('../../common/urlParse') const ruleSolver = require('../../extensions/brave/content/scripts/pageInformation') const request = require('../../../js/lib/request') @@ -49,6 +50,13 @@ const ledgerNotifications = require('./ledgerNotifications') const ledgerVideoCache = require('../../common/cache/ledgerVideoCache') const updater = require('../../updater') const promoCodeFirstRunStorage = require('../../promoCodeFirstRunStorage') +const appUrlUtil = require('../../../js/lib/appUrlUtil') +const urlutil = require('../../../js/lib/urlutil') +const windowState = require('../../common/state/windowState') +const {makeImmutable, makeJS, isList, isImmutable} = require('../../common/state/immutableUtil') +const siteHacks = require('../../siteHacks') +const UrlUtil = require('../../../js/lib/urlutil') +const promotionStatuses = require('../../common/constants/promotionStatuses') // Caching let locationDefault = 'NOOP' @@ -85,7 +93,6 @@ let verifiedTimeoutId = false let v2RulesetDB const v2RulesetPath = 'ledger-rulesV2.leveldb' const statePath = 'ledger-state.json' -const newClientPath = 'ledger-newstate.json' // Definitions const clientOptions = { @@ -99,7 +106,7 @@ const clientOptions = { environment: process.env.LEDGER_ENVIRONMENT || 'production' } -var platforms = { +const platforms = { 'darwin': 'osx', 'win32x64': 'winx64', 'win32ia32': 'winia32', @@ -136,7 +143,7 @@ signatureMax = Math.ceil(signatureMax * 1.5) if (ipc) { ipc.on(messages.LEDGER_PUBLISHER, (event, location) => { - if (!synopsis || event.sender.session === electron.session.fromPartition('default') || !tldjs.isValid(location)) { + if (!synopsis || event.sender.session === electron.session.fromPartition('default') || !tldjs.isValid(tldjs.getDomain(location))) { event.returnValue = {} return } @@ -180,11 +187,30 @@ const paymentPresent = (state, tabId, present) => { } appActions.onPromotionGet() + + state = checkSeed(state) getPublisherTimestamp(true) } else if (balanceTimeoutId) { clearTimeout(balanceTimeoutId) balanceTimeoutId = false } + + return state +} + +const checkSeed = (state) => { + const seed = ledgerState.getInfoProp(state, 'passphrase') + let localClient = client + + if (!localClient) { + localClient = require('bat-client') + } + + if (seed && localClient && !localClient.isValidPassPhrase(seed)) { + state = ledgerState.setAboutProp(state, 'status', ledgerStatuses.CORRUPTED_SEED) + } + + return state } const getPublisherTimestamp = (updateList) => { @@ -241,7 +267,7 @@ const onBootStateFile = (state) => { } if (client.sync(callback) === true) { - run(random.randomInt({min: ledgerUtil.milliseconds.minute, max: 10 * ledgerUtil.milliseconds.minute})) + run(state, random.randomInt({min: ledgerUtil.milliseconds.minute, max: 5 * ledgerUtil.milliseconds.minute})) } module.exports.getBalance(state) @@ -252,7 +278,7 @@ const onBootStateFile = (state) => { } const promptForRecoveryKeyFile = () => { - const defaultRecoveryKeyFilePath = path.join(electron.app.getPath('userData'), '/brave_wallet_recovery.txt') + const defaultRecoveryKeyFilePath = path.join(electron.app.getPath('downloads'), '/brave_wallet_recovery.txt') if (process.env.SPECTRON) { // skip the dialog for tests console.log(`for test, trying to recover keys from path: ${defaultRecoveryKeyFilePath}`) @@ -266,7 +292,7 @@ const promptForRecoveryKeyFile = () => { extensions: [['txt']], includeAllFiles: false }, (files) => { - return (files && files.length ? files[0] : null) + appActions.onFileRecoveryKeys((files && files.length ? files[0] : null)) }) } } @@ -317,6 +343,7 @@ const getPublisherData = (result, scorekeeper) => { verified: result.options.verified || false, exclude: result.options.exclude || false, publisherKey: result.publisherKey, + providerName: result.providerName, siteName: result.publisherKey, views: result.visits, duration: duration, @@ -325,7 +352,7 @@ const getPublisherData = (result, scorekeeper) => { minutesSpent: 0, secondsSpent: 0, faviconURL: result.faviconURL, - score: result.scores[scorekeeper], + score: result.scores ? result.scores[scorekeeper] : 0, pinPercentage: result.pinPercentage, weight: result.pinPercentage } @@ -437,7 +464,7 @@ const synopsisNormalizer = (state, changedPublisher, returnState = true, prune = dataPinned.push(getPublisherData(publisher, scorekeeper)) } else if (ledgerUtil.stickyP(state, publisher.publisherKey)) { // unpinned - unPinnedTotal += publisher.scores[scorekeeper] + unPinnedTotal += publisher.scores ? publisher.scores[scorekeeper] : 0 dataUnPinned.push(publisher) } else { // excluded @@ -490,9 +517,6 @@ const synopsisNormalizer = (state, changedPublisher, returnState = true, prune = publisher.weight = 0 return publisher }) - - // sync app store - state = ledgerState.changePinnedValues(state, dataPinned) } else if (dataUnPinned.length === 0 && pinnedTotal < 100) { // when you don't have any unpinned sites and pinned total is less then 100 % let changedObject = dataPinned.find(publisher => publisher.publisherKey === changedPublisher) @@ -507,9 +531,6 @@ const synopsisNormalizer = (state, changedPublisher, returnState = true, prune = dataPinned = module.exports.normalizePinned(dataPinned, pinnedTotal, 100, false) dataPinned = module.exports.roundToTarget(dataPinned, 100, 'pinPercentage') } - - // sync app store - state = ledgerState.changePinnedValues(state, dataPinned) } else { // unpinned publishers dataUnPinned = dataUnPinned.map((result) => { @@ -587,7 +608,7 @@ const inspectP = (db, path, publisher, property, key, callback) => { } // TODO rename function name -const verifiedP = (state, publisherKey, callback) => { +const verifiedP = (state, publisherKey, callback, lastUpdate) => { const clientCallback = (err, result) => { if (err) { console.error(`Error verifying publisher ${publisherKey}: `, err.toString()) @@ -596,7 +617,7 @@ const verifiedP = (state, publisherKey, callback) => { if (callback) { if (result) { - callback(null, result) + callback(null, result, lastUpdate) } else { callback(err, {}) } @@ -681,14 +702,17 @@ const excludeP = (publisherKey, callback) => { }) } -const addSiteVisit = (state, timestamp, location, tabId) => { - if (!synopsis) { +const addSiteVisit = (state, timestamp, location, tabId, manualAdd = false) => { + if (!synopsis || location == null) { return state } location = pageDataUtil.getInfoKey(location) - const locationData = ledgerState.getLocation(state, location) - const duration = new Date().getTime() - timestamp + + const minimumVisitTime = getSetting(settings.PAYMENTS_MINIMUM_VISIT_TIME) + const duration = manualAdd ? parseInt(minimumVisitTime) : new Date().getTime() - timestamp + const locationData = manualAdd ? Immutable.fromJS({ publisher: tldjs.getDomain(location) }) : ledgerState.getLocation(state, location) + if (_internal.verboseP) { console.log( `locations[${location}]=${JSON.stringify(locationData, null, 2)} ` + @@ -703,7 +727,7 @@ const addSiteVisit = (state, timestamp, location, tabId) => { let publisherKey = locationData.get('publisher') let revisitP = false - if (duration >= getSetting(settings.PAYMENTS_MINIMUM_VISIT_TIME)) { + if (duration >= minimumVisitTime) { if (!visitsByPublisher[publisherKey]) { visitsByPublisher[publisherKey] = {} } @@ -714,13 +738,13 @@ const addSiteVisit = (state, timestamp, location, tabId) => { } } - revisitP = visitsByPublisher[publisherKey][location].tabIds.indexOf(tabId) !== -1 + revisitP = manualAdd ? false : visitsByPublisher[publisherKey][location].tabIds.indexOf(tabId) !== -1 if (!revisitP) { visitsByPublisher[publisherKey][location].tabIds.push(tabId) } } - return saveVisit(state, publisherKey, { + return module.exports.saveVisit(state, publisherKey, { duration, revisited: revisitP }) @@ -747,6 +771,47 @@ const saveVisit = (state, publisherKey, options) => { return state } +const onVerifiedPStatus = (error, result, lastUpdate) => { + if (error || result == null) { + return + } + + if (!Array.isArray(result)) { + result = [result] + } + + if (result.length === 0) { + return + } + + const data = result.reduce((publishers, item) => { + if (item.err) { + return publishers + } + + const publisherKey = item.publisher + let verified = false + if (item && item.properties) { + verified = !!item.properties.verified + savePublisherOption(publisherKey, 'verified', verified) + } + + savePublisherOption(publisherKey, 'verifiedTimestamp', lastUpdate) + + publishers.push({ + publisherKey, + verified, + verifiedTimestamp: lastUpdate + }) + + return publishers + }, []) + + if (data && data.length > 0) { + appActions.onPublishersOptionUpdate(data) + } +} + const checkVerifiedStatus = (state, publisherKeys, publisherTimestamp) => { if (publisherKeys == null) { return state @@ -756,35 +821,22 @@ const checkVerifiedStatus = (state, publisherKeys, publisherTimestamp) => { publisherKeys = [publisherKeys] } - const checkKeys = [] const lastUpdate = parseInt(publisherTimestamp || ledgerState.getLedgerValue(state, 'publisherTimestamp')) - - publisherKeys.forEach(key => { + const checkKeys = publisherKeys.reduce((init, key) => { const lastPublisherUpdate = parseInt(ledgerState.getPublisherOption(state, key, 'verifiedTimestamp') || 0) if (lastUpdate > lastPublisherUpdate) { - checkKeys.push(key) + init.push(key) } - }) + + return init + }, []) if (checkKeys.length === 0) { return state } - state = module.exports.verifiedP(state, checkKeys, (error, result) => { - if (!error) { - const publisherKey = result.publisher - - if (result && result.properties && result.properties) { - const verified = !!result.properties.verified - appActions.onPublisherOptionUpdate(publisherKey, 'verified', verified) - savePublisherOption(publisherKey, 'verified', verified) - } - - appActions.onPublisherOptionUpdate(publisherKey, 'verifiedTimestamp', lastUpdate) - savePublisherOption(publisherKey, 'verifiedTimestamp', lastUpdate) - } - }) + state = module.exports.verifiedP(state, checkKeys, onVerifiedPStatus, lastUpdate) return state } @@ -798,19 +850,27 @@ const shouldTrackTab = (state, tabId) => { return !isPrivate && !tabFromState.isEmpty() && ledgerUtil.shouldTrackView(tabFromState) } -const addNewLocation = (state, location, tabId = tabState.TAB_ID_NONE, keepInfo = false) => { +const addNewLocation = (state, location, tabId = tabState.TAB_ID_NONE, keepInfo = false, manualAdd = false) => { // We always want to have the latest active tabId - const currentTabId = pageDataState.getLastActiveTabId(state) + const currentTabId = manualAdd ? tabId : pageDataState.getLastActiveTabId(state) state = pageDataState.setLastActiveTabId(state, tabId) - if (location === currentUrl) { + if (location === currentUrl && !manualAdd) { return state } // Save previous recorder page - if (currentUrl !== locationDefault && currentTabId != null && currentTabId !== tabState.TAB_ID_NONE) { - if (shouldTrackTab(state, currentTabId)) { - state = addSiteVisit(state, currentTimestamp, currentUrl, currentTabId) + if (currentUrl !== locationDefault && currentTabId != null && currentTabId !== tabState.TAB_ID_NONE && !manualAdd) { + if (module.exports.shouldTrackTab(state, currentTabId)) { + state = module.exports.addSiteVisit(state, currentTimestamp, currentUrl, currentTabId) + } + } + + if (manualAdd) { + const minimumVisits = getSetting(settings.PAYMENTS_MINIMUM_VISITS) + for (let v = 0; v < minimumVisits; v++) { + state = module.exports.addSiteVisit(state, currentTimestamp, location, currentTabId, manualAdd) } + return state } if (location === locationDefault && !keepInfo) { @@ -823,6 +883,17 @@ const addNewLocation = (state, location, tabId = tabState.TAB_ID_NONE, keepInfo return state } +const onFavIconReceived = (state, publisherKey, blob) => { + if (publisherKey == null) { + return state + } + + state = ledgerState.setPublishersProp(state, publisherKey, 'faviconURL', blob) + module.exports.savePublisherData(publisherKey, 'faviconURL', blob) + + return state +} + const getFavIcon = (state, publisherKey, page) => { let publisher = ledgerState.getPublisher(state, publisherKey) const protocol = page.get('protocol') @@ -894,7 +965,7 @@ const fetchFavIcon = (publisherKey, url, redirects) => { underscore.keys(fileTypes).forEach((fileType) => { if (matchP) return if ( - prefix.length >= fileTypes[fileType].length || + prefix.length < fileTypes[fileType].length || fileTypes[fileType].compare(prefix, 0, fileTypes[fileType].length) !== 0 ) { return @@ -909,10 +980,6 @@ const fetchFavIcon = (publisherKey, url, redirects) => { } else if (tail > 0 && (tail + 8 >= blob.length)) return appActions.onFavIconReceived(publisherKey, blob) - - if (synopsis.publishers && synopsis.publishers[publisherKey]) { - synopsis.publishers[publisherKey].faviconURL = blob - } }) } @@ -923,13 +990,21 @@ const pageDataChanged = (state, viewData = {}, keepInfo = false) => { let info = pageDataState.getLastInfo(state) const tabId = viewData.tabId || pageDataState.getLastActiveTabId(state) - const location = viewData.location || locationDefault + let location = viewData.location || locationDefault if (!synopsis) { state = addNewLocation(state, locationDefault, tabId) return state } + if (UrlUtil.isUrlPDF(location)) { + location = UrlUtil.getLocationIfPDF(location) + info = Immutable.fromJS({ + key: location, + location: location + }) + } + const realUrl = getSourceAboutUrl(location) || location if ( info.isEmpty() && @@ -1022,37 +1097,59 @@ const backupKeys = (state, backupAction) => { ] const message = messageLines.join(os.EOL) - const filePath = path.join(electron.app.getPath('userData'), '/brave_wallet_recovery.txt') + const defaultFilePath = path.join(electron.app.getPath('downloads'), '/brave_wallet_recovery.txt') const fs = require('fs') - fs.writeFile(filePath, message, (err) => { - if (err) { - console.error(err) - } else { - tabs.create({url: fileUrl(filePath)}, (webContents) => { - if (backupAction === 'print') { - webContents.print({silent: false, printBackground: false}) - } else { - webContents.downloadURL(fileUrl(filePath), true) - } - }) + + if (backupAction === 'print') { + tabs.create({url: appUrlUtil.aboutUrls.get('about:printkeys')}) + + // we do not check whether the user actually printed the backup word list + return aboutPreferencesState.setBackupStatus(state, true) + } + + const dialog = electron.dialog + const BrowserWindow = electron.BrowserWindow + + dialog.showDialog(BrowserWindow.getFocusedWindow(), { + type: 'select-saveas-file', + defaultPath: defaultFilePath, + extensions: [['txt']], + includeAllFiles: false + }, (files) => { + const file = files && files.length ? files[0] : null + if (file) { + try { + fs.writeFileSync(file, message) + appActions.onLedgerBackupSuccess() + } catch (e) { + console.error('Problem saving backup keys') + } } }) + return state +} + +const fileRecoveryKeys = (state, recoveryKeyFile) => { + if (!recoveryKeyFile) { + // user canceled from dialog, we abort without error + return state + } + + state = aboutPreferencesState.setRecoveryInProgress(state, true) + const result = loadKeysFromBackupFile(state, recoveryKeyFile) + const recoveryKey = result.recoveryKey || '' + state = result.state + + return recoverKeys(state, false, recoveryKey) } const recoverKeys = (state, useRecoveryKeyFile, key) => { let recoveryKey if (useRecoveryKeyFile) { - let recoveryKeyFile = promptForRecoveryKeyFile() - if (!recoveryKeyFile) { - // user canceled from dialog, we abort without error - return state - } - - const result = loadKeysFromBackupFile(state, recoveryKeyFile) - recoveryKey = result.recoveryKey || '' - state = result.state + promptForRecoveryKeyFile() + return state } if (!recoveryKey) { @@ -1062,10 +1159,11 @@ const recoverKeys = (state, useRecoveryKeyFile, key) => { if (typeof recoveryKey !== 'string') { // calling logError sets the error object state = logError(state, true, 'recoverKeys') - state = ledgerState.setRecoveryStatus(state, false) + state = aboutPreferencesState.setRecoveryStatus(state, false) return state } + state = aboutPreferencesState.setRecoveryBalanceRecalculated(state, false) client.recoverWallet(null, recoveryKey, (err, result) => { appActions.onWalletRecovery(err, result) appActions.onPromotionRemoval() @@ -1081,7 +1179,7 @@ const onWalletRecovery = (state, error, result) => { // if ledgerInfo.error is not null, the wallet info will not display in UI // logError sets ledgerInfo.error, so we must we clear it or UI will show an error state = logError(state, error.toString(), 'recoveryWallet') - state = ledgerState.setRecoveryStatus(state, false) + state = aboutPreferencesState.setRecoveryStatus(state, false) } else { // convert buffer to Uint8Array let seed = result && result.getIn(['properties', 'wallet', 'keyinfo', 'seed']) @@ -1093,24 +1191,37 @@ const onWalletRecovery = (state, error, result) => { state = ledgerState.setInfoProp(state, 'walletQR', Immutable.Map()) state = ledgerState.setInfoProp(state, 'addresses', Immutable.Map()) + const status = ledgerState.getAboutProp(state, 'status') + + if (status === ledgerStatuses.CORRUPTED_SEED) { + state = ledgerState.setAboutProp(state, 'status', '') + } + callback(error, result) if (balanceTimeoutId) { clearTimeout(balanceTimeoutId) } module.exports.getBalance(state) - state = ledgerState.setRecoveryStatus(state, true) + state = aboutPreferencesState.setRecoveryStatus(state, true) } return state } +const resetPublishers = (state) => { + state = ledgerState.resetPublishers(state) + synopsis.publishers = {} + + return state +} + const quit = (state) => { quitP = true state = addNewLocation(state, locationDefault) - if (!getSetting(settings.PAYMENTS_ENABLED) && getSetting(settings.SHUTDOWN_CLEAR_HISTORY)) { - state = ledgerState.resetSynopsis(state, true) + if (!getSetting(settings.PAYMENTS_ENABLED) && getSetting(settings.SHUTDOWN_CLEAR_PUBLISHERS)) { + resetPublishers(state) } return state @@ -1162,12 +1273,17 @@ const initSynopsis = (state) => { } const publishers = ledgerState.getPublishers(state) + for (let item of publishers) { const publisherKey = item[0] - excludeP(publisherKey, (unused, exclude) => { - appActions.onPublisherOptionUpdate(publisherKey, 'exclude', exclude) - savePublisherOption(publisherKey, 'exclude', exclude) - }) + const publisher = item[1] || Immutable.Map() + + if (!publisher.getIn(['options', 'exclude'])) { + excludeP(publisherKey, (unused, exclude) => { + appActions.onPublisherOptionUpdate(publisherKey, 'exclude', exclude) + savePublisherOption(publisherKey, 'exclude', exclude) + }) + } } state = updatePublisherInfo(state) @@ -1190,8 +1306,6 @@ const checkPromotions = () => { const enable = (state, paymentsEnabled) => { if (paymentsEnabled) { - state = checkBtcBatMigrated(state, paymentsEnabled) - if (!getSetting(settings.PAYMENTS_NOTIFICATION_TRY_PAYMENTS_DISMISSED)) { appActions.changeSetting(settings.PAYMENTS_NOTIFICATION_TRY_PAYMENTS_DISMISSED, true) } @@ -1199,7 +1313,6 @@ const enable = (state, paymentsEnabled) => { if (paymentsEnabled === getSetting(settings.PAYMENTS_ENABLED)) { // on start - if (togglePromotionTimeoutId) { clearTimeout(togglePromotionTimeoutId) } @@ -1208,7 +1321,7 @@ const enable = (state, paymentsEnabled) => { checkPromotions() }, random.randomInt({min: 10 * ledgerUtil.milliseconds.second, max: 15 * ledgerUtil.milliseconds.second})) } else if (paymentsEnabled) { - // on toggle + // toggle on if (togglePromotionTimeoutId) { clearTimeout(togglePromotionTimeoutId) } @@ -1220,6 +1333,9 @@ const enable = (state, paymentsEnabled) => { state = ledgerState.setActivePromotion(state, paymentsEnabled) getPromotion(state) + } else { + // toggle off + state = ledgerState.setPromotionProp(state, 'promotionStatus', null) } if (synopsis) { @@ -1353,12 +1469,84 @@ const clientprep = () => { _internal.verboseP = ledgerClient.prototype.boolion(process.env.LEDGER_PUBLISHER_VERBOSE) } +/** + * This callback that we do it in roundtrips + * + * @callback roundtripCallback + * @param {Error|null} error - error when doing a request, null if no errors + * @param {object} response - response object that we get from request.request + * @param {any} payload - data that we get from the request + */ + +/** + * Round trip for fetching data (scrap data from html) inside window process + * @param {object} params - contains params from roundtrip + * @param {string} params.url - url of the site that we want to scrap + * @param {string} params.verboseP - tells us if we want to log the process or not + * @param {roundtripCallback} callback - The callback that handles the response + */ +const roundTripFromWindow = (params, callback) => { + if (!callback) { + return + } + + if (!params || !params.url) { + if (params && params.verboseP) { + console.log(`We are missing url. Params ${JSON.stringify(params)}`) + } + + callback(new Error('Url is missing')) + return + } + + request.fetchPublisherInfo(params.url, { + method: 'GET', + responseType: 'text', + headers: { + 'content-type': 'application/json; charset=utf-8' + } + }, (error, body) => { + if (error) { + if (params.verboseP) { + console.log(`roundTripFromWindow error: ${error.toString()}`) + } + return callback(error) + } + + if (params.verboseP) { + console.log(`roundTripFromWindow success: ${JSON.stringify(body)}`) + } + + return callback(null, null, body) + }) +} + +/** + * Round trip function for executing actions + * from the bat libraries (mostly server calls) + * @param {object} params - params that are directly tied to request.request + * @param {string} params.server - server url + * @param {string} params.method - HTTP method (GET, PUT, etc) + * @param {object} params.payload - payload that we want to send to the server + * @param {object} params.headers - HTTP headers + * @param {string} params.path - relative path to requested url + * @param {boolean} params.binaryP - are we receiving raw payload back + * @param {object} options + * @param {boolean} options.verboseP - tells us if we want to log the process or not + * @param {object} options.headers - headers that are used in the request.request + * @param {string} options.server - server url + * @param {boolean} options.binaryP - are we receiving binary payload back + * @param {boolean} options.rawP - are we receiving raw payload back + * @param {boolean} options.scrapeP - are we doping scraping + * @param {boolean} options.windowP - do we want to run this request in the window process + * @param {roundtripCallback} callback - The callback that handles the response + */ const roundtrip = (params, options, callback) => { let parts = typeof params.server === 'string' ? urlParse(params.server) : typeof params.server !== 'undefined' ? params.server : typeof options.server === 'string' ? urlParse(options.server) : options.server - const binaryP = options.binaryP - const rawP = binaryP || options.rawP + const binaryP = options.binaryP || params.binaryP + const rawP = binaryP || options.rawP || options.scrapeP if (!params.method) params.method = 'GET' parts = underscore.extend(underscore.pick(parts, ['protocol', 'hostname', 'port']), @@ -1381,6 +1569,11 @@ const roundtrip = (params, options, callback) => { parts.pathname = parts.path } + if (options.windowP) { + roundTripFromWindow({url: urlFormat(parts), verboseP: options.verboseP}, callback) + return + } + options = { url: urlFormat(parts), method: params.method, @@ -1444,12 +1637,12 @@ const roundtrip = (params, options, callback) => { const observeTransactions = (state, transactions) => { if (!transactions) { - return + return state } const current = ledgerState.getInfoProp(state, 'transactions') if (current && current.size === transactions.length) { - return + return state } // Notify the user of new transactions. @@ -1457,10 +1650,13 @@ const observeTransactions = (state, transactions) => { if (transactions.length > 0) { const newestTransaction = transactions[0] if (newestTransaction && newestTransaction.contribution) { + state = ledgerState.setAboutProp(state, 'status', '') ledgerNotifications.showPaymentDone(newestTransaction.contribution.fiat) } } } + + return state } // TODO convert this function and related ones to immutable @@ -1472,7 +1668,7 @@ const getStateInfo = (state, parsedData) => { const info = parsedData.paymentInfo const then = new Date().getTime() - ledgerUtil.milliseconds.year - if (!parsedData.properties.wallet) { + if (!parsedData.properties || !parsedData.properties.wallet) { return state } @@ -1481,12 +1677,7 @@ const getStateInfo = (state, parsedData) => { } if (parsedData.properties && parsedData.properties.wallet && parsedData.properties.wallet.keyinfo) { - let seed = parsedData.properties.wallet.keyinfo.seed - if (!(seed instanceof Uint8Array)) { - seed = new Uint8Array(Object.values(seed)) - } - - parsedData.properties.wallet.keyinfo.seed = seed + parsedData.properties.wallet.keyinfo.seed = uintKeySeed(parsedData.properties.wallet.keyinfo.seed) } const newInfo = { @@ -1497,9 +1688,14 @@ const getStateInfo = (state, parsedData) => { reconcileStamp: parsedData.reconcileStamp } - let passphrase = ledgerClient.prototype.getWalletPassphrase(parsedData) - if (passphrase) { - newInfo.passphrase = passphrase.join(' ') + try { + let passphrase = ledgerClient.prototype.getWalletPassphrase(parsedData) + if (passphrase) { + newInfo.passphrase = passphrase.join(' ') + } + } catch (e) { + console.error(e) + state = ledgerState.setAboutProp(state, 'status', ledgerStatuses.CORRUPTED_SEED) } state = ledgerState.mergeInfoProp(state, newInfo) @@ -1533,7 +1729,7 @@ const getStateInfo = (state, parsedData) => { {ballots: ballots})) } - observeTransactions(state, transactions) + state = observeTransactions(state, transactions) return ledgerState.setInfoProp(state, 'transactions', Immutable.fromJS(transactions)) } @@ -1597,7 +1793,12 @@ const getPaymentInfo = (state) => { client.getWalletProperties(amount, currency, function (err, body) { if (err) { - console.error('getWalletProperties error: ' + err.toString()) + if (err.message) { + console.error('getWalletProperties error: ' + err.message) + } else { + console.error('getWalletProperties error: ' + err.toString()) + } + appActions.onWalletPropertiesError() return } @@ -1623,7 +1824,24 @@ const lockInContributionAmount = (state, balance) => { } } +const setNewTimeUntilReconcile = (newReconcileTime = null) => { + client.setTimeUntilReconcile(newReconcileTime, (err, stateResult) => { + if (err) return console.error('ledger setTimeUntilReconcile error: ' + err.toString()) + + if (!stateResult) { + return + } + + appActions.onTimeUntilReconcile(stateResult) + }) +} + const onWalletProperties = (state, body) => { + const currentStatus = ledgerState.getAboutProp(state, 'status') + if (currentStatus === ledgerStatuses.SERVER_PROBLEM) { + state = ledgerState.setAboutProp(state, 'status', '') + } + if (body == null) { return state } @@ -1646,6 +1864,9 @@ const onWalletProperties = (state, body) => { const balance = parseFloat(body.get('balance')) if (balance >= 0) { state = ledgerState.setInfoProp(state, 'balance', balance) + if (balance > 0) { + state = ledgerState.setInfoProp(state, 'userHasFunded', true) + } lockInContributionAmount(state, balance) } @@ -1668,15 +1889,33 @@ const onWalletProperties = (state, body) => { state = ledgerState.setInfoProp(state, 'currentRate', rate) } + // Grants + let probi = parseFloat(body.get('probi')) + let userFunded = null + if (!isNaN(probi)) { + userFunded = probi + let grants = body.get('grants') || Immutable.List() + if (!grants.isEmpty()) { + let grantTotal = 0 + grants = grants.map(grant => { + grantTotal += parseFloat(grant.get('probi')) + return { + amount: new BigNumber(grant.get('probi').toString()).dividedBy('1e18').toNumber(), + expirationDate: grant.get('expiryTime') + } + }) + state = ledgerState.setInfoProp(state, 'grants', grants) + userFunded = probi - grantTotal + } + + state = ledgerState.setInfoProp(state, 'userFunded', new BigNumber(userFunded.toString()).dividedBy('1e18').toNumber()) + } + // Probi - const probi = parseFloat(body.get('probi')) if (probi >= 0) { state = ledgerState.setInfoProp(state, 'probi', probi) - - const amount = info.get('balance') - - if (amount != null && rate) { - const bigProbi = new BigNumber(probi.toString()).dividedBy('1e18') + if (userFunded != null && rate) { + const bigProbi = new BigNumber(userFunded.toString()).dividedBy('1e18') const bigRate = new BigNumber(rate.toString()) const converted = bigProbi.times(bigRate).toNumber() @@ -1710,7 +1949,9 @@ const onWalletProperties = (state, body) => { } state = module.exports.generatePaymentData(state) - + if (state.getIn(['about', 'preferences']) != null && aboutPreferencesState.getRecoveryBalanceRecalulated(state) === false) { + state = aboutPreferencesState.setRecoveryBalanceRecalculated(state, true) + } return state } @@ -1774,10 +2015,13 @@ const uintKeySeed = (currentSeed) => { return currentSeed } + if (currentSeed instanceof Object) { + return new Uint8Array(Object.values(currentSeed)) + } + try { currentSeed.toJSON() - const seed = new Uint8Array(Object.values(currentSeed)) - currentSeed = seed + return new Uint8Array(Object.values(currentSeed)) } catch (err) { } return currentSeed @@ -1802,7 +2046,7 @@ const callback = (err, result, delayTime) => { if (!client) return if (typeof delayTime === 'undefined') { - delayTime = random.randomInt({min: ledgerUtil.milliseconds.minute, max: 10 * ledgerUtil.milliseconds.minute}) + delayTime = random.randomInt({min: ledgerUtil.milliseconds.minute, max: 5 * ledgerUtil.milliseconds.minute}) } } @@ -1818,6 +2062,10 @@ const onCallback = (state, result, delayTime) => { return state } + if (result.getIn(['currentReconcile', 'timestamp']) === 0) { + state = ledgerState.setAboutProp(state, 'status', ledgerStatuses.IN_PROGRESS) + } + const newAddress = result.getIn(['properties', 'wallet', 'addresses', 'BAT']) const oldAddress = ledgerState.getInfoProps(state).getIn(['addresses', 'BAT']) @@ -1884,19 +2132,6 @@ const onCallback = (state, result, delayTime) => { // persist the new ledger state muonWriter(statePath, regularResults) - // delete the temp file used during transition (if it still exists) - if (client && client.options && client.options.version === 'v2') { - const fs = require('fs') - fs.access(pathName(newClientPath), fs.FF_OK, (err) => { - if (err) { - return - } - fs.unlink(pathName(newClientPath), (err) => { - if (err) console.error('unlink error: ' + err.toString()) - }) - }) - } - run(state, delayTime) return state @@ -1904,6 +2139,7 @@ const onCallback = (state, result, delayTime) => { const onReferralCodeRead = (code) => { if (!code) { + fetchReferralHeaders() return } @@ -1932,7 +2168,7 @@ const onReferralInit = (err, response, body) => { } if (body && body.download_id) { - appActions.onReferralCodeRead(body.download_id, body.referral_code) + appActions.onReferralCodeRead(body) promoCodeFirstRunStorage .removePromoCode() .catch(error => { @@ -1948,6 +2184,63 @@ const onReferralInit = (err, response, body) => { } } +const onReferralRead = (state, body, activeWindowId) => { + body = makeImmutable(body) + + if (body.has('offer_page_url')) { + const url = body.get('offer_page_url') + if (urlutil.isURL(url)) { + if (activeWindowId === windowState.WINDOW_ID_NONE || !state.get('windowReady')) { + // write referralPage to state if initial window is not created/visible yet + state = updateState.setUpdateProp(state, 'referralPage', url) + } else { + // initial window exists and should be ready; create tab directly + appActions.createTabRequested({ + url, + windowId: activeWindowId, + active: true + }) + state = updateState.setUpdateProp(state, 'referralPage', null) + } + } + } + + if (body.has('headers')) { + const headers = body.get('headers') + state = updateState.setUpdateProp(state, 'referralHeaders', headers) + siteHacks.setReferralHeaders(headers) + } + + state = updateState.setUpdateProp(state, 'referralDownloadId', body.get('download_id')) + state = updateState.setUpdateProp(state, 'referralPromoCode', body.get('referral_code')) + + return state +} + +const fetchReferralHeaders = () => { + module.exports.roundtrip({ + server: referralServer, + method: 'GET', + path: '/promo/custom-headers' + }, {}, appActions.onFetchReferralHeaders) +} + +const onFetchReferralHeaders = (state, err, response, body) => { + if (err) { + if (clientOptions.verboseP) { + console.error(makeJS(err)) + } + return state + } + + if (body && isList(body)) { + state = updateState.setUpdateProp(state, 'referralHeaders', body) + siteHacks.setReferralHeaders(body) + } + + return state +} + const initialize = (state, paymentsEnabled) => { let fs @@ -1983,12 +2276,17 @@ const initialize = (state, paymentsEnabled) => { if (clientOptions.verboseP) { console.error('read error: ' + error.toString()) } + fetchReferralHeaders() }) + } else { + fetchReferralHeaders() } + // Get referral headers every day + setInterval(() => fetchReferralHeaders, (24 * ledgerUtil.milliseconds.hour)) + if (!paymentsEnabled) { client = null - newClient = false return ledgerState.resetInfo(state, true) } @@ -2044,6 +2342,22 @@ const getContributionAmount = (state) => { } const onInitRead = (state, parsedData) => { + const isBTC = parsedData && + parsedData.properties && + parsedData.properties.wallet && + parsedData.properties.wallet.keychains + + if (isBTC) { + const fs = require('fs') + fs.renameSync(pathName(statePath), pathName('ledger-state-btc.json')) + state = ledgerState.resetInfo(state) + clientprep() + client = ledgerClient(null, underscore.extend({roundtrip: roundtrip}, clientOptions), null) + parsedData = client.state + getPaymentInfo(state) + muonWriter(statePath, parsedData) + } + if (Array.isArray(parsedData.transactions)) { parsedData.transactions.sort((transaction1, transaction2) => { return transaction1.submissionStamp - transaction2.submissionStamp @@ -2073,20 +2387,12 @@ const onInitRead = (state, parsedData) => { // enables it again -> reconcileStamp is in the past. // In this case reset reconcileStamp to the future. try { - timeUntilReconcile = client.timeUntilReconcile(synopsis) + timeUntilReconcile = client.timeUntilReconcile(synopsis, onFuzzing) } catch (ex) {} let ledgerWindow = (ledgerState.getSynopsisOption(state, 'numFrames') - 1) * ledgerState.getSynopsisOption(state, 'frameSize') if (typeof timeUntilReconcile === 'number' && timeUntilReconcile < -ledgerWindow) { - client.setTimeUntilReconcile(null, (err, stateResult) => { - if (err) return console.error('ledger setTimeUntilReconcile error: ' + err.toString()) - - if (!stateResult) { - return - } - - appActions.onTimeUntilReconcile(stateResult) - }) + setNewTimeUntilReconcile() } } catch (ex) { console.error('ledger client creation error(1): ', ex) @@ -2112,12 +2418,15 @@ const onInitRead = (state, parsedData) => { module.exports.setPaymentInfo(contributionAmount) module.exports.getBalance(state) - // Show relevant browser notifications on launch - state = ledgerNotifications.onLaunch(state) - return state } +const onFuzzing = () => { + if (client && client.state) { + appActions.onLedgerFuzzing(client.state.reconcileStamp) + } +} + const onTimeUntilReconcile = (state, stateResult) => { state = getStateInfo(state, stateResult.toJS()) // TODO optimize muonWriter(statePath, stateResult) @@ -2127,7 +2436,7 @@ const onTimeUntilReconcile = (state, stateResult) => { const onLedgerFirstSync = (state, parsedData) => { if (client.sync(callback) === true) { - run(state, random.randomInt({min: ledgerUtil.milliseconds.minute, max: 10 * ledgerUtil.milliseconds.minute})) + run(state, random.randomInt({min: ledgerUtil.milliseconds.minute, max: 5 * ledgerUtil.milliseconds.minute})) } return cacheRuleSet(state, parsedData.ruleset) @@ -2150,7 +2459,11 @@ const run = (state, delayTime) => { let result = '' fields.forEach((field) => { - const max = (result.length > 0) ? 9 : 19 + const max = (result.length > 0) ? 45 : 19 + + if (!field) { + return + } if (typeof field !== 'string') field = field.toString() if (field.length < max) { @@ -2159,6 +2472,7 @@ const run = (state, delayTime) => { } else { field = field.substr(0, max) } + result += ' ' + field }) @@ -2184,20 +2498,50 @@ const run = (state, delayTime) => { }) } - if (typeof delayTime === 'undefined' || !client) { + if (state == null || typeof delayTime === 'undefined' || !client) { return } + const publishers = ledgerState.getAboutProp(state, 'synopsis') || Immutable.List() + if (isList(publishers) && publishers.isEmpty() && client.isReadyToReconcile(synopsis, onFuzzing)) { + setNewTimeUntilReconcile() + } + let winners const ballots = client.ballots() const data = (synopsis) && (ballots > 0) && synopsisNormalizer(state, null, false, true) + let map = null + if (data) { let weights = [] + map = {} + data.forEach((datum) => { - weights.push({publisher: datum.publisherKey, weight: datum.weight / 100.0}) + map[datum.publisherKey] = { + verified: datum.verified, + exclude: datum.exclude, + score: datum.score, + pinPercentage: datum.pinPercentage, + percentage: datum.percentage, + votes: 0, + weight: datum.weight + } + + weights.push({ + publisher: datum.publisherKey, + pinPercentage: datum.pinPercentage, + weight: datum.weight / 100.0 + }) + }) + + winners = synopsis.winners(ballots, weights) || [] + + winners.forEach((winner) => { + if (map[winner]) map[winner].votes++ }) - winners = synopsis.winners(ballots, weights) + + client.memo('run:ballots', Object.values(map)) } if (!winners) winners = [] @@ -2210,6 +2554,9 @@ const run = (state, delayTime) => { const result = client.vote(winner) if (result) stateData = result }) + if (!stateData && map) { + stateData = client.state + } if (stateData) muonWriter(statePath, stateData) } catch (ex) { console.error('ledger client error(2): ' + ex.toString() + (ex.stack ? ('\n' + ex.stack) : '')) @@ -2217,12 +2564,12 @@ const run = (state, delayTime) => { if (delayTime === 0) { try { - delayTime = client.timeUntilReconcile(synopsis) + delayTime = client.timeUntilReconcile(synopsis, onFuzzing) } catch (ex) { delayTime = false } if (delayTime === false) { - delayTime = random.randomInt({min: ledgerUtil.milliseconds.minute, max: 10 * ledgerUtil.milliseconds.minute}) + delayTime = random.randomInt({min: ledgerUtil.milliseconds.minute, max: 5 * ledgerUtil.milliseconds.minute}) } } @@ -2251,7 +2598,7 @@ const run = (state, delayTime) => { return } - if (client.isReadyToReconcile(synopsis)) { + if (client.isReadyToReconcile(synopsis, onFuzzing)) { client.reconcile(uuid.v4().toLowerCase(), callback) } } @@ -2271,7 +2618,7 @@ const onNetworkConnected = (state) => { } if (client.sync(callback) === true) { - const delayTime = random.randomInt({min: ledgerUtil.milliseconds.minute, max: 10 * ledgerUtil.milliseconds.minute}) + const delayTime = random.randomInt({min: ledgerUtil.milliseconds.minute, max: 5 * ledgerUtil.milliseconds.minute}) run(state, delayTime) } @@ -2337,6 +2684,31 @@ const migration = (state) => { return state } +const setPublishersOptions = (state, publishersArray) => { + if (!publishersArray || publishersArray.size === 0) { + return state + } + + publishersArray.forEach(publisherData => { + const publisherKey = publisherData.get('publisherKey') + + if (publisherKey == null) { + return state + } + + for (const data of publisherData) { + const prop = data[0] + const value = data[1] + if (prop !== 'publisherKey') { + state = ledgerState.setPublisherOption(state, publisherKey, prop, value) + module.exports.savePublisherOption(publisherKey, prop, value) + } + } + }) + + return state +} + // for synopsis variable handling only const deleteSynopsisPublisher = (publisherKey) => { delete synopsis.publishers[publisherKey] @@ -2347,174 +2719,77 @@ const saveOptionSynopsis = (prop, value) => { } const savePublisherOption = (publisherKey, prop, value) => { - if (synopsis && synopsis.publishers && synopsis.publishers[publisherKey] && synopsis.publishers[publisherKey].options) { - synopsis.publishers[publisherKey].options[prop] = value - } -} - -const savePublisherData = (publisherKey, prop, value) => { - if (synopsis && synopsis.publishers && synopsis.publishers[publisherKey]) { - synopsis.publishers[publisherKey][prop] = value - } -} - -const deleteSynopsis = () => { - synopsis.publishers = {} -} - -// fix for incorrectly persisted state (see #11585) -const yoDawg = (stateState) => { - while (stateState.hasOwnProperty('state') && stateState.state.persona) { - stateState = stateState.state + if (!synopsis || !synopsis.publishers || !publisherKey) { + return } - return stateState -} -const checkBtcBatMigrated = (state, paymentsEnabled) => { - if (!paymentsEnabled) { - return state + if (!synopsis.publishers[publisherKey]) { + synopsis.publishers[publisherKey] = {} } - // One time conversion of wallet - const isNewInstall = migrationState.isNewInstall(state) - const hasUpgradedWallet = migrationState.hasUpgradedWallet(state) - if (!isNewInstall && !hasUpgradedWallet) { - state = migrationState.setTransitionStatus(state, true) - module.exports.transitionWalletToBat() - } else { - state = migrationState.setTransitionStatus(state, false) + if (!synopsis.publishers[publisherKey].options) { + synopsis.publishers[publisherKey].options = {} } - return state -} - -let newClient = null -const getNewClient = () => { - return newClient + synopsis.publishers[publisherKey].options[prop] = value } -let busyRetryCount = 0 - -const transitionWalletToBat = () => { - let newPaymentId, result - - if (newClient === true) return - clientprep() - - if (!client) { - console.log('Client is not initialized, will try again') +const savePublisherData = (publisherKey, prop, value) => { + if (!synopsis || !synopsis.publishers || !publisherKey) { return } - // only attempt this transition if the wallet is v1 - if (client.options && client.options.version !== 'v1') { - // older versions incorrectly marked this for transition - // this will clean them up (no more bouncy ball) - appActions.onBitcoinToBatTransitioned() - return + if (!synopsis.publishers[publisherKey]) { + synopsis.publishers[publisherKey] = {} } - // Restore newClient from the file (if one exists) - if (!newClient) { - const fs = require('fs') - try { - fs.accessSync(pathName(newClientPath), fs.FF_OK) - fs.readFile(pathName(newClientPath), (error, data) => { - if (error) { - console.error(`ledger client: can't read ${newClientPath} to restore newClient`) - return - } - const parsedData = JSON.parse(data) - const state = yoDawg(parsedData) - newClient = ledgerClient(state.personaId, - underscore.extend(state.options, {roundtrip: module.exports.roundtrip}, clientOptions), - state) - transitionWalletToBat() - }) - return - } catch (err) {} - } + synopsis.publishers[publisherKey][prop] = value +} - // Create new client - if (!newClient) { - try { - newClient = ledgerClient(null, underscore.extend({roundtrip: module.exports.roundtrip}, clientOptions), null) - muonWriter(newClientPath, newClient.state) - } catch (ex) { - console.error('ledger client creation error(2): ', ex) - return - } +let currentMediaKey = null +const onMediaRequest = (state, xhr, type, details) => { + if (!xhr || type == null) { + return state } - newPaymentId = newClient.getPaymentId() - if (!newPaymentId) { - newClient.sync((err, result, delayTime) => { - if (err) { - return console.error('ledger client error(3): ' + JSON.stringify(err, null, 2) + (err.stack ? ('\n' + err.stack) : '')) - } - - if (typeof delayTime === 'undefined') delayTime = random.randomInt({ min: 1, max: 500 }) + const parsed = ledgerUtil.getMediaData(xhr, type, details) + if (parsed == null) { + return state + } - if (newClient) { - muonWriter(newClientPath, newClient.state) + if (Array.isArray(parsed)) { + parsed.forEach(data => { + if (data) { + state = module.exports.processMediaData(state, data, type, details) } - - setTimeout(() => transitionWalletToBat(), delayTime) }) - return + } else { + state = module.exports.processMediaData(state, parsed, type, details) } - if (client.busyP()) { - if (++busyRetryCount > 3) { - console.log('ledger client is currently busy; transition will be retried on next launch') - return - } - const delayTime = random.randomInt({ - min: ledgerUtil.milliseconds.minute, - max: 10 * ledgerUtil.milliseconds.minute - }) - console.log('ledger client is currently busy; transition will be retried shortly (this was attempt ' + busyRetryCount + ')') - setTimeout(() => transitionWalletToBat(), delayTime) - return + return state +} + +const processMediaData = (state, parsed, type, details) => { + let tabId = tabState.TAB_ID_NONE + if (details) { + tabId = details.get('tabId') } - appActions.onBitcoinToBatBeginTransition() + const mediaId = ledgerUtil.getMediaId(parsed, type) - try { - client.transition(newPaymentId, (err, properties) => { - if (err || !newClient) { - console.error('ledger client transition error: ', err) - } else { - result = newClient.transitioned(properties) - client = newClient - newClient = true - // NOTE: onLedgerCallback will save latest client to disk as ledger-state.json - appActions.onLedgerCallback(result, random.randomInt({ - min: ledgerUtil.milliseconds.minute, - max: 10 * ledgerUtil.milliseconds.minute - })) - appActions.onBitcoinToBatTransitioned() - ledgerNotifications.showBraveWalletUpdated() - getPublisherTimestamp() - } - }) - } catch (ex) { - console.error('exception during ledger client transition: ', ex) + if (clientOptions.loggingP) { + console.log('Media request', parsed, `Media id: ${mediaId}`) } -} -let currentMediaKey = null -const onMediaRequest = (state, xhr, type, tabId) => { - if (!xhr || type == null) { + if (mediaId == null) { return state } - const parsed = ledgerUtil.getMediaData(xhr, type) - const mediaId = ledgerUtil.getMediaId(parsed, type) const mediaKey = ledgerUtil.getMediaKey(mediaId, type) - let duration = ledgerUtil.getMediaDuration(parsed, type) + let duration = ledgerUtil.getMediaDuration(state, parsed, mediaKey, type) - if (mediaId == null || duration == null || mediaKey == null) { + if (duration == null || mediaKey == null) { return state } @@ -2526,6 +2801,9 @@ const onMediaRequest = (state, xhr, type, tabId) => { if (!ledgerPublisher) { ledgerPublisher = require('bat-publisher') } + if (clientOptions.loggingP) { + console.log('LOGGED EVENT', parsed, `Media id: ${mediaId}`, `Media key: ${mediaKey}`, `Duration: ${duration}ms (${duration / 1000}s)`) + } let revisited = true const activeTabId = tabState.getActiveTabId(state) @@ -2534,9 +2812,17 @@ const onMediaRequest = (state, xhr, type, tabId) => { currentMediaKey = mediaKey } + const stateData = ledgerUtil.generateMediaCacheData(state, parsed, type, mediaKey) const cache = ledgerVideoCache.getDataByVideoId(state, mediaKey) + if (clientOptions.loggingP) { + console.log('Media cache data: ', stateData.toJS()) + } if (!cache.isEmpty()) { + if (!stateData.isEmpty()) { + state = ledgerVideoCache.mergeCacheByVideoId(state, mediaKey, stateData) + } + const publisherKey = cache.get('publisher') const publisher = ledgerState.getPublisher(state, publisherKey) if (!publisher.isEmpty() && publisher.has('providerName')) { @@ -2548,6 +2834,10 @@ const onMediaRequest = (state, xhr, type, tabId) => { } } + if (!stateData.isEmpty()) { + state = ledgerVideoCache.setCacheByVideoId(state, mediaKey, stateData) + } + const options = underscore.extend({roundtrip: module.exports.roundtrip}, clientOptions) const mediaProps = { mediaId, @@ -2567,7 +2857,7 @@ const onMediaRequest = (state, xhr, type, tabId) => { if (_internal.verboseP) { console.log('\ngetPublisherFromMediaProps mediaProps=' + JSON.stringify(mediaProps, null, 2) + '\nresponse=' + - JSON.stringify(response, null, 2)) + JSON.stringify(response, null, 2)) } appActions.onLedgerMediaPublisher(mediaKey, response, duration, revisited) @@ -2608,10 +2898,10 @@ const onMediaPublisher = (state, mediaKey, response, duration, revisited) => { } } - synopsis.publishers[publisherKey].faviconName = faviconName - synopsis.publishers[publisherKey].faviconURL = faviconURL - synopsis.publishers[publisherKey].publisherURL = publisherURL - synopsis.publishers[publisherKey].providerName = providerName + savePublisherData(publisherKey, 'faviconName', faviconName) + savePublisherData(publisherKey, 'faviconURL', faviconURL) + savePublisherData(publisherKey, 'publisherURL', publisherURL) + savePublisherData(publisherKey, 'providerName', providerName) state = ledgerState.setPublishersProp(state, publisherKey, 'faviconName', faviconName) state = ledgerState.setPublishersProp(state, publisherKey, 'faviconURL', faviconURL) state = ledgerState.setPublishersProp(state, publisherKey, 'publisherURL', publisherURL) @@ -2625,7 +2915,7 @@ const onMediaPublisher = (state, mediaKey, response, duration, revisited) => { .set('publisher', publisherKey) // Add to cache - state = ledgerVideoCache.setCacheByVideoId(state, mediaKey, cacheObject) + state = ledgerVideoCache.mergeCacheByVideoId(state, mediaKey, cacheObject) state = module.exports.saveVisit(state, publisherKey, { duration, @@ -2663,7 +2953,45 @@ const getPromotion = (state) => { }) } -const claimPromotion = (state) => { +const getCaptcha = (state) => { + if (!client) { + return + } + + const promotion = ledgerState.getPromotion(state) + if (promotion.isEmpty()) { + return + } + + client.getPromotionCaptcha(promotion.get('promotionId'), (err, body) => { + if (err) { + console.error(`Problem getting promotion captcha ${err.toString()}`) + appActions.onCaptchaResponse(null) + } + + appActions.onCaptchaResponse(body) + }) +} + +const onCaptchaResponse = (state, body) => { + if (body == null) { + state = ledgerState.setPromotionProp(state, 'promotionStatus', promotionStatuses.CAPTCHA_ERROR) + return state + } + + const image = `data:image/jpeg;base64,${Buffer.from(body).toString('base64')}` + + state = ledgerState.setPromotionProp(state, 'captcha', image) + const currentStatus = ledgerState.getPromotionProp(state, 'promotionStatus') + + if (currentStatus !== promotionStatuses.CAPTCHA_ERROR) { + state = ledgerState.setPromotionProp(state, 'promotionStatus', promotionStatuses.CAPTCHA_CHECK) + } + + return state +} + +const claimPromotion = (state, x, y) => { if (!client) { return } @@ -2673,7 +3001,7 @@ const claimPromotion = (state) => { return } - client.setPromotion(promotion.get('promotionId'), (err, _, status) => { + client.setPromotion(promotion.get('promotionId'), {x, y}, (err, _, status) => { let param = null if (err) { console.error(`Problem claiming promotion ${err.toString()}`) @@ -2685,17 +3013,30 @@ const claimPromotion = (state) => { } const onPromotionResponse = (state, status) => { - if (status) { + if (status && isImmutable(status)) { if (status.get('statusCode') === 422) { // promotion already claimed - state = ledgerState.setPromotionProp(state, 'promotionStatus', 'expiredError') + state = ledgerState.setPromotionProp(state, 'promotionStatus', promotionStatuses.PROMO_EXPIRED) + } else if (status.get('statusCode') === 403) { + // captcha verification failed + state = ledgerState.setPromotionProp(state, 'promotionStatus', promotionStatuses.CAPTCHA_ERROR) + module.exports.getCaptcha(state) } else { // general error - state = ledgerState.setPromotionProp(state, 'promotionStatus', 'generalError') + state = ledgerState.setPromotionProp(state, 'promotionStatus', promotionStatuses.GENERAL_ERROR) } return state } + const currentStatus = ledgerState.getPromotionProp(state, 'promotionStatus') + + if ( + currentStatus === promotionStatuses.CAPTCHA_ERROR || + currentStatus === promotionStatuses.CAPTCHA_CHECK + ) { + state = ledgerState.setPromotionProp(state, 'promotionStatus', null) + } + ledgerNotifications.removePromotionNotification(state) state = ledgerState.setPromotionProp(state, 'claimedTimestamp', new Date().getTime()) @@ -2703,15 +3044,7 @@ const onPromotionResponse = (state, status) => { const minTimestamp = ledgerState.getPromotionProp(state, 'minimumReconcileTimestamp') if (minTimestamp > currentTimestamp) { - client.setTimeUntilReconcile(minTimestamp, (err, stateResult) => { - if (err) return console.error('ledger setTimeUntilReconcile error: ' + err.toString()) - - if (!stateResult) { - return - } - - appActions.onTimeUntilReconcile(stateResult) - }) + setNewTimeUntilReconcile(minTimestamp) } if (togglePromotionTimeoutId) { @@ -2733,9 +3066,7 @@ const onPublisherTimestamp = (state, oldTimestamp, newTimestamp) => { return } - publishers.forEach((publisher, key) => { - module.exports.checkVerifiedStatus(state, key, newTimestamp) - }) + module.exports.checkVerifiedStatus(state, Array.from(publishers.keys()), newTimestamp) } const checkReferralActivity = (state) => { @@ -2813,6 +3144,53 @@ const activityRoundTrip = (err, response, body) => { updater.checkForUpdate(false, true) } +const deleteWallet = (state) => { + state = ledgerState.deleteSynopsis(state) + state = state.setIn(['settings', settings.PAYMENTS_ENABLED], false) + + client = null + synopsis = null + + const fs = require('fs') + fs.access(pathName(statePath), fs.constants.F_OK, (err) => { + if (err) { + return + } + + fs.unlink(pathName(statePath), (err) => { + if (err) { + return console.error('read error: ' + err.toString()) + } + }) + }) + + return state +} + +const clearPaymentHistory = (state) => { + state = ledgerState.setInfoProp(state, 'transactions', Immutable.List()) + state = ledgerState.setInfoProp(state, 'ballots', Immutable.List()) + state = ledgerState.setInfoProp(state, 'batch', Immutable.Map()) + + const fs = require('fs') + const path = pathName(statePath) + try { + fs.accessSync(path, fs.constants.W_OK) + let data = fs.readFileSync(path) + data = JSON.parse(data) + if (data) { + data.transactions = [] + data.ballots = [] + data.batch = {} + muonWriter(statePath, data) + } + } catch (err) { + console.error(`Problem reading ${path} when clearing payment history`) + } + + return state +} + const getMethods = () => { const publicMethods = { backupKeys, @@ -2842,14 +3220,11 @@ const getMethods = () => { onNetworkConnected, migration, onInitRead, - deleteSynopsis, - transitionWalletToBat, - getNewClient, normalizePinned, roundToTarget, + onFavIconReceived, savePublisherData, pruneSynopsis, - checkBtcBatMigrated, onMediaRequest, onMediaPublisher, saveVisit, @@ -2857,12 +3232,25 @@ const getMethods = () => { claimPromotion, onPromotionResponse, getBalance, + fileRecoveryKeys, getPromotion, onPublisherTimestamp, checkVerifiedStatus, checkReferralActivity, + setPublishersOptions, referralCheck, - roundtrip + roundtrip, + onFetchReferralHeaders, + onReferralRead, + processMediaData, + addNewLocation, + addSiteVisit, + getCaptcha, + onCaptchaResponse, + shouldTrackTab, + deleteWallet, + resetPublishers, + clearPaymentHistory } let privateMethods = {} @@ -2871,7 +3259,6 @@ const getMethods = () => { privateMethods = { enable, addSiteVisit, - checkBtcBatMigrated, clearVisitsByPublisher: function () { visitsByPublisher = {} }, @@ -2882,9 +3269,6 @@ const getMethods = () => { setSynopsis: (data) => { synopsis = data }, - resetNewClient: () => { - newClient = false - }, getClient: () => { return client }, @@ -2906,7 +3290,11 @@ const getMethods = () => { activityRoundTrip, pathName, onReferralInit, - onReferralCodeRead + roundTripFromWindow, + onReferralCodeRead, + onVerifiedPStatus, + checkSeed, + shouldTrackTab } } diff --git a/app/browser/api/ledgerNotifications.js b/app/browser/api/ledgerNotifications.js index 3fdf8f78a01..5696aa5da48 100644 --- a/app/browser/api/ledgerNotifications.js +++ b/app/browser/api/ledgerNotifications.js @@ -11,8 +11,8 @@ const messages = require('../../../js/constants/messages') const settings = require('../../../js/constants/settings') // State +const aboutPreferencesState = require('../../common/state/aboutPreferencesState') const ledgerState = require('../../common/state/ledgerState') -const migrationState = require('../../common/state/migrationState') // Actions const appActions = require('../../../js/actions/appActions') @@ -28,16 +28,17 @@ const text = { addFunds: locale.translation('addFundsNotification'), tryPayments: locale.translation('notificationTryPayments'), reconciliation: locale.translation('reconciliationNotification'), - walletConvertedToBat: locale.translation('walletConvertedToBat') + backupKeys: locale.translation('backupKeys') } -const pollingInterval = 15 * ledgerUtil.milliseconds.minute // 15 * minutes +const pollingInterval = process.env.LEDGER_NOTIFICATION ? ledgerUtil.milliseconds.second * 5 : 15 * ledgerUtil.milliseconds.minute // 15 * minute default production let intervalTimeout const displayOptions = { style: 'greetingStyle', persist: false } const nextAddFundsTime = 3 * ledgerUtil.milliseconds.day +let backupNotifyInterval = process.env.LEDGER_NOTIFICATION ? [2 * ledgerUtil.milliseconds.minute, 5 * ledgerUtil.milliseconds.minute] : [7 * ledgerUtil.milliseconds.day, 14 * ledgerUtil.milliseconds.day] const sufficientBalanceToReconcile = (state) => { const balance = Number(ledgerState.getInfoProp(state, 'balance') || 0) @@ -45,11 +46,11 @@ const sufficientBalanceToReconcile = (state) => { const budget = ledgerState.getContributionAmount(state) return balance + unconfirmed >= budget } -const hasFunds = (state) => { - const balance = getSetting(settings.PAYMENTS_ENABLED) - ? Number(ledgerState.getInfoProp(state, 'balance') || 0) - : 0 - return balance > 0 + +const hasFunded = (state) => { + return getSetting(settings.PAYMENTS_ENABLED) + ? ledgerState.getInfoProp(state, 'userHasFunded') || false + : false } const shouldShowNotificationReviewPublishers = () => { const nextTime = getSetting(settings.PAYMENTS_NOTIFICATION_RECONCILE_SOON_TIMESTAMP) @@ -70,43 +71,13 @@ const init = () => { }, pollingInterval) } -const onLaunch = (state) => { - const enabled = getSetting(settings.PAYMENTS_ENABLED) - if (!enabled) { - return state - } - - const ledger = require('./ledger') - state = ledger.checkBtcBatMigrated(state, enabled) - - if (hasFunds(state)) { - // Don't bother processing the rest, which are only - if (!getSetting(settings.PAYMENTS_NOTIFICATIONS)) { - return state - } - - // Show one-time BAT conversion message: - // - if payments are enabled - // - user has a positive balance - // - this is an existing profile (new profiles will have firstRunTimestamp matching batMercuryTimestamp) - // - wallet has been transitioned - // - notification has not already been shown yet - // (see https://github.com/brave/browser-laptop/issues/11021) - const isNewInstall = migrationState.isNewInstall(state) - const hasUpgradedWallet = migrationState.hasUpgradedWallet(state) - const hasBeenNotified = migrationState.hasBeenNotified(state) - if (!isNewInstall && hasUpgradedWallet && !hasBeenNotified) { - module.exports.showBraveWalletUpdated() - } - } - - return state -} - const onInterval = (state) => { + if (process.env.LEDGER_NOTIFICATION) { + runDebugCounter() + } if (getSetting(settings.PAYMENTS_ENABLED)) { if (getSetting(settings.PAYMENTS_NOTIFICATIONS)) { - module.exports.showEnabledNotifications(state) + state = module.exports.showEnabledNotifications(state) } } else { module.exports.showDisabledNotifications(state) @@ -147,7 +118,7 @@ const onResponse = (message, buttonIndex, activeWindow) => { } else if (buttonIndex === 2 && activeWindow) { // Add funds: Open payments panel appActions.createTabRequested({ - url: 'about:preferences#payments', + url: 'about:preferences#payments?addFundsOverlayVisible', windowId: activeWindow.id }) } @@ -181,16 +152,16 @@ const onResponse = (message, buttonIndex, activeWindow) => { appActions.changeSetting(settings.PAYMENTS_NOTIFICATION_TRY_PAYMENTS_DISMISSED, true) break - case text.walletConvertedToBat: + case text.backupKeys: if (buttonIndex === 0) { - // Open backup modal + appActions.changeSetting(settings.PAYMENTS_NOTIFICATIONS, false) + } else if (buttonIndex === 2 && activeWindow) { appActions.createTabRequested({ url: 'about:preferences#payments?ledgerBackupOverlayVisible', windowId: activeWindow.id }) } break - default: return } @@ -219,6 +190,11 @@ const onDynamicResponse = (message, actionId, activeWindow) => { appActions.onPromotionRemind() break } + case 'noThanks': + { + appActions.changeSetting(settings.PAYMENTS_ALLOW_PROMOTIONS, false) + break + } } appActions.hideNotification(message) @@ -229,14 +205,17 @@ const onDynamicResponse = (message, actionId, activeWindow) => { * a day in the future and balance is too low. * 24 hours prior to reconciliation, show message asking user to review * their votes. + * + * If not time to reconcile, check to show backup notification () */ const showEnabledNotifications = (state) => { + const now = new Date().getTime() + let bootStamp = ledgerState.getInfoProp(state, 'bootStamp') const reconcileStamp = ledgerState.getInfoProp(state, 'reconcileStamp') if (!reconcileStamp) { - return + return state } - - if (reconcileStamp - new Date().getTime() < ledgerUtil.milliseconds.day) { + if (reconcileStamp - now < ledgerUtil.milliseconds.day) { if (sufficientBalanceToReconcile(state)) { if (shouldShowNotificationReviewPublishers()) { const reconcileFrequency = ledgerState.getInfoProp(state, 'reconcileFrequency') @@ -245,11 +224,38 @@ const showEnabledNotifications = (state) => { } else if (shouldShowNotificationAddFunds()) { showAddFunds() } - } else if (reconcileStamp - new Date().getTime() < 2 * ledgerUtil.milliseconds.day) { + } else if (reconcileStamp - now < 2 * ledgerUtil.milliseconds.day) { if (sufficientBalanceToReconcile(state) && (shouldShowNotificationReviewPublishers())) { - showReviewPublishers(new Date().getTime() + ledgerUtil.milliseconds.day) + showReviewPublishers(now + ledgerUtil.milliseconds.day) + } + } else if (hasFunded(state) && !aboutPreferencesState.hasBeenBackedUp(state)) { + const backupNotifyCount = aboutPreferencesState.getPreferencesProp(state, 'backupNotifyCount') || 0 + const backupNotifyTimestamp = aboutPreferencesState.getPreferencesProp(state, 'backupNotifyTimestamp') || backupNotifyInterval[0] + if (!bootStamp) { + bootStamp = now + state = ledgerState.setInfoProp(state, 'bootStamp', bootStamp) + } + if (now - bootStamp > backupNotifyTimestamp) { + const nextTime = backupNotifyTimestamp + getNextBackupNotification(state, backupNotifyCount + 1, backupNotifyInterval) // set next time to notify case for remind later + state = aboutPreferencesState.setPreferencesProp(state, 'backupNotifyCount', (backupNotifyCount + 1)) + state = aboutPreferencesState.setPreferencesProp(state, 'backupNotifyTimestamp', nextTime) + module.exports.showBackupKeys(nextTime) + } + if (process.env.LEDGER_NOTIFICATION) { + watchNotificationTimers(now, bootStamp, backupNotifyCount, backupNotifyTimestamp) + } + } + return state +} + +const getNextBackupNotification = (state, count, interval) => { + if (count >= (interval && interval.constructor === Array ? interval.length : 0)) { + if (process.env.LEDGER_NOTIFICATION) { + return ledgerUtil.milliseconds.minute } + return ledgerUtil.milliseconds.month } + return interval[count] } const showDisabledNotifications = (state) => { @@ -272,6 +278,20 @@ const showDisabledNotifications = (state) => { } } +const showBackupKeys = (nextTime) => { + appActions.showNotification({ + position: 'global', + greeting: text.hello, + message: text.backupKeys, + buttons: [ + {text: locale.translation('turnOffNotifications')}, + {text: locale.translation('updateLater')}, + {text: locale.translation('backupKeysNow'), className: 'primaryButton'} + ], + options: displayOptions + }) +} + const showReviewPublishers = (nextTime) => { appActions.changeSetting(settings.PAYMENTS_NOTIFICATION_RECONCILE_SOON_TIMESTAMP, nextTime) @@ -324,27 +344,6 @@ const showPaymentDone = (transactionContributionFiat) => { }) } -const showBraveWalletUpdated = () => { - appActions.onBitcoinToBatNotified() - - appActions.showNotification({ - position: 'global', - greeting: text.hello, - message: text.walletConvertedToBat, - // Learn More. - buttons: [ - {text: locale.translation('walletConvertedBackup')}, - {text: locale.translation('walletConvertedDismiss')} - ], - options: { - style: 'greetingStyle', - persist: false, - advancedLink: 'https://brave.com/faq-payments/#brave-payments', - advancedText: locale.translation('walletConvertedLearnMore') - } - }) -} - const onPromotionReceived = (state) => { const promotion = ledgerState.getPromotionNotification(state) @@ -373,6 +372,13 @@ const showPromotionNotification = (state) => { const data = notification.toJS() data.position = 'global' + if (data.buttons) { + data.buttons.unshift({ + text: locale.translation('noThanks'), + buttonActionId: 'noThanks' + }) + } + appActions.showNotification(data) } @@ -386,6 +392,16 @@ const removePromotionNotification = (state) => { appActions.hideNotification(notification.get('message')) } +const watchNotificationTimers = (now, bootStamp, backupNotifyCount, backupNotifyTimestamp) => { // for testing + console.log('now - bootstamp: ' + (now - bootStamp)) + console.log('count: ' + backupNotifyCount) + console.log('backupNotifyTimestamp: ' + backupNotifyTimestamp) +} + +const runDebugCounter = () => { + console.log(new Date().getTime() / ledgerUtil.milliseconds.second) +} + if (ipc) { ipc.on(messages.NOTIFICATION_RESPONSE, (e, message, buttonIndex, checkbox, index, buttonActionId) => { if (buttonActionId) { @@ -409,15 +425,16 @@ const getMethods = () => { const publicMethods = { showPaymentDone, init, - onLaunch, - showBraveWalletUpdated, onInterval, onPromotionReceived, removePromotionNotification, showDisabledNotifications, showEnabledNotifications, onIntervalDynamic, - showPromotionNotification + showPromotionNotification, + showBackupKeys, + hasFunded, + getNextBackupNotification } let privateMethods = {} diff --git a/app/browser/api/textCalc.js b/app/browser/api/textCalc.js deleted file mode 100644 index 7525acca8f4..00000000000 --- a/app/browser/api/textCalc.js +++ /dev/null @@ -1,147 +0,0 @@ -/* 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 Immutable = require('immutable') - -// Actions -const appActions = require('../../../js/actions/appActions') - -// Constant -const siteTags = require('../../../js/constants/siteTags') - -// Utils -const tabs = require('../../browser/tabs') -const {makeImmutable} = require('../../common/state/immutableUtil') - -// Styles -const globalStyles = require('../../renderer/components/styles/global') - -const fontSize = globalStyles.spacing.bookmarksItemFontSize -const fontFamily = globalStyles.typography.default.family - -const calcText = (item, type) => { - let title = type === siteTags.BOOKMARK - ? item.get('title') || item.get('location') - : item.get('title') - - if (title && title.length === 0) { - return - } - - title = title - .replace(/'/g, '!') - .replace(/\\"/g, '!') - .replace(/\\\\/g, '//') - - const param = ` - (function() { - let ctx = document.createElement('canvas').getContext('2d') - ctx.font = '${fontSize} ${fontFamily}' - const width = ctx.measureText('${title}').width - - return width - })() - ` - - tabs.executeScriptInBackground(param, (err, url, result) => { - if (err) { - throw err - } - - if (type === siteTags.BOOKMARK) { - appActions.onBookmarkWidthChanged(Immutable.fromJS([ - { - key: item.get('key'), - parentFolderId: item.get('parentFolderId'), - width: result[0] - } - ])) - } else { - appActions.onBookmarkFolderWidthChanged(Immutable.fromJS([ - { - key: item.get('key'), - parentFolderId: item.get('parentFolderId'), - width: result[0] - } - ])) - } - }) -} - -const calcTextList = (list) => { - const take = 500 - list = makeImmutable(list) - - if (list.size === 0) { - return - } - - let paramList = JSON.stringify(list.take(take)) - .replace(/'/g, '!') - .replace(/\\"/g, '!') - .replace(/\\\\/g, '//') - - const param = ` - (function() { - const ctx = document.createElement('canvas').getContext('2d') - ctx.font = '${fontSize} ${fontFamily}' - const bookmarks = [] - const folders = [] - const list = JSON.parse('${paramList}') - - list.forEach(item => { - if (item.type === '${siteTags.BOOKMARK}') { - bookmarks.push({ - key: item.key, - parentFolderId: item.parentFolderId, - width: ctx.measureText(item.title || item.location).width - }) - } else { - folders.push({ - key: item.key, - parentFolderId: item.parentFolderId, - width: ctx.measureText(item.title).width - }) - } - }) - - const result = { - bookmarks: bookmarks, - folders: folders - } - - return JSON.stringify(result) - })() - ` - - tabs.executeScriptInBackground(param, (err, url, result) => { - if (err) { - console.error('Error in executeScriptInBackground (textCalcUtil.js)') - } - - if (result[0]) { - const data = JSON.parse(result[0]) - if (data.bookmarks.length > 0) { - appActions.onBookmarkWidthChanged(Immutable.fromJS(data.bookmarks)) - } - - if (data.folders.length > 0) { - appActions.onBookmarkFolderWidthChanged(Immutable.fromJS(data.folders)) - } - } else { - console.error('Error, cant parse bookmarks in executeScriptInBackground') - } - - list = list.skip(take) - - if (list.size > 0) { - calcTextList(list) - } - }) -} - -module.exports = { - calcText, - calcTextList -} diff --git a/app/browser/bookmarksExporter.js b/app/browser/bookmarksExporter.js index 6923549207e..0d5a676eaec 100644 --- a/app/browser/bookmarksExporter.js +++ b/app/browser/bookmarksExporter.js @@ -43,7 +43,7 @@ const showDialog = (state) => { personal = createBookmarkArray(state) other = createBookmarkArray(state, -1, false) try { - fs.writeFileSync(fileName, createBookmarkHTML(personal, other)) + fs.writeFileSync(fileNames[0], createBookmarkHTML(personal, other)) } catch (e) { console.log('Error exporting bookmarks: ', e) } diff --git a/app/browser/isThirdPartyHost.js b/app/browser/isThirdPartyHost.js index 89e473ba82f..dc4957cde28 100644 --- a/app/browser/isThirdPartyHost.js +++ b/app/browser/isThirdPartyHost.js @@ -3,22 +3,28 @@ * You can obtain one at http://mozilla.org/MPL/2.0/. */ const getBaseDomain = require('../../js/lib/baseDomain').getBaseDomain +const ip = require('ip') /** - * baseContextHost {string} - The base host to check against - * testHost {string} - The host to check + * Checks if two hosts are third party. Subdomains count as first-party to the + * parent domain. Uses hostname (no port). + * @param {host1} string - First hostname to compare + * @param {host2} string - Second hostname to compare */ -const isThirdPartyHost = (baseContextHost, testHost) => { - // TODO: Always return true if these are IP addresses that aren't the same - if (!testHost || !baseContextHost) { +const isThirdPartyHost = (host1, host2) => { + if (!host1 || !host2) { return true } - const documentDomain = getBaseDomain(baseContextHost) - if (testHost.length > documentDomain.length) { - return (testHost.substr(testHost.length - documentDomain.length - 1) !== '.' + documentDomain) - } else { - return (testHost !== documentDomain) + if (host1 === host2) { + return false } + + if (ip.isV4Format(host1) || ip.isV4Format(host2)) { + // '127.0.0.1' and '::7f00:1' are actually equal, but ignore such cases for now + return host1 !== host2 + } + + return getBaseDomain(host1) !== getBaseDomain(host2) } module.exports = isThirdPartyHost diff --git a/app/browser/menu.js b/app/browser/menu.js index c532c7dd322..a148be6858f 100644 --- a/app/browser/menu.js +++ b/app/browser/menu.js @@ -39,6 +39,8 @@ const bookmarkUtil = require('../common/lib/bookmarkUtil') const isDarwin = platformUtil.isDarwin() const isLinux = platformUtil.isLinux() const isWindows = platformUtil.isWindows() +const {templateUrls} = require('./share') +const {getAllRendererWindows} = require('./windows') let appMenu = null let closedFrames = new Immutable.OrderedMap() @@ -215,19 +217,6 @@ const createViewSubmenu = () => { } }, CommonMenu.separatorMenuItem, - /* - { - label: locale.translation('toolbars'), - visible: false - submenu: [ - {label: 'Favorites Bar', accelerator: 'Alt+CmdOrCtrl+B'}, - {label: 'Tab Bar'}, - {label: 'Address Bar', accelerator: 'Alt+CmdOrCtrl+A'}, - {label: 'Tab Previews', accelerator: 'Alt+CmdOrCtrl+P'} - ] - }, - CommonMenu.separatorMenuItem, - */ { label: locale.translation('stop'), accelerator: isDarwin ? 'Cmd+.' : 'Esc', @@ -238,31 +227,6 @@ const createViewSubmenu = () => { CommonMenu.reloadPageMenuItem(), CommonMenu.cleanReloadMenuItem(), CommonMenu.separatorMenuItem, - /* - { - label: locale.translation('readingView'), - visible: false, - accelerator: 'Alt+CmdOrCtrl+R' - }, { - label: locale.translation('tabManager'), - visible: false, - accelerator: 'Alt+CmdOrCtrl+M' - }, - CommonMenu.separatorMenuItem, - { - label: locale.translation('textEncoding'), - visible: false - submenu: [ - {label: 'Autodetect', submenu: []}, - CommonMenu.separatorMenuItem, - {label: 'Unicode'}, - {label: 'Western'}, - CommonMenu.separatorMenuItem, - {label: 'etc...'} - ] - }, - CommonMenu.separatorMenuItem, - */ { label: locale.translation('toggleDeveloperTools'), accelerator: isDarwin ? 'Cmd+Alt+I' : 'Ctrl+Shift+I', @@ -330,14 +294,6 @@ const createHistorySubmenu = () => { } }, CommonMenu.separatorMenuItem, - /* - { - label: locale.translation('showAllHistory'), - accelerator: 'CmdOrCtrl+Y', - visible: false - }, - CommonMenu.separatorMenuItem, - */ { label: locale.translation('clearBrowsingData'), accelerator: 'Shift+CmdOrCtrl+Delete', @@ -414,6 +370,15 @@ const createBookmarksSubmenu = (state) => { submenu = submenu.concat(bookmarks) } + const otherBookmarks = menuUtil.createOtherBookmarkTemplateItems(state) + if (otherBookmarks.length > 0) { + submenu.push(CommonMenu.separatorMenuItem) + submenu.push({ + label: locale.translation('otherBookmarks'), + submenu: otherBookmarks + }) + } + return submenu } @@ -540,6 +505,13 @@ const createDebugSubmenu = (state) => { click: function (menuItem, browserWindow, e) { appActions.changeSetting(settings.DEBUG_ALLOW_MANUAL_TAB_DISCARD, menuItem.checked) } + }, { + label: 'Display tab identifiers', + type: 'checkbox', + checked: !!getSetting(settings.DEBUG_VERBOSE_TAB_INFO), + click: function (menuItem, browserWindow, e) { + appActions.changeSetting(settings.DEBUG_VERBOSE_TAB_INFO, menuItem.checked) + } } ] } @@ -633,21 +605,29 @@ const createMenu = (state) => { } } -const setMenuItemChecked = (state, label, checked) => { - // Update electron menu (Mac / Linux) +const setMenuItemAttribute = (state, label, key, value) => { const systemMenuItem = menuUtil.getMenuItem(appMenu, label) - systemMenuItem.checked = checked + systemMenuItem[key] = value // Update in-memory menu template (Windows) if (isWindows) { const oldTemplate = state.getIn(['menu', 'template']) - const newTemplate = menuUtil.setTemplateItemChecked(oldTemplate, label, checked) + const newTemplate = menuUtil.setTemplateItemAttribute(oldTemplate, label, key, value) if (newTemplate) { appActions.setMenubarTemplate(newTemplate) } } } +const updateShareMenuItems = (state, enabled) => { + for (let key of Object.keys(templateUrls)) { + const siteName = menuUtil.extractSiteName(key) + const l10nId = key === 'email' ? 'emailPageLink' : 'sharePageLink' + const label = locale.translation(l10nId, {siteName: siteName}) + setMenuItemAttribute(state, label, 'enabled', enabled) + } +} + const doAction = (state, action) => { switch (action.actionType) { case appConstants.APP_SET_STATE: @@ -659,17 +639,31 @@ const doAction = (state, action) => { const frame = frameStateUtil.getFrameByTabId(state, action.tabId) if (frame) { currentLocation = frame.location - setMenuItemChecked(state, locale.translation('bookmarkPage'), isCurrentLocationBookmarked(state)) + setMenuItemAttribute(state, locale.translation('bookmarkPage'), 'checked', isCurrentLocationBookmarked(state)) + } + break + } + case appConstants.APP_WINDOW_CLOSED: + case appConstants.APP_WINDOW_CREATED: + { + const windowCount = getAllRendererWindows().length + if (action.actionType === appConstants.APP_WINDOW_CLOSED && windowCount === 0) { + updateShareMenuItems(state, false) + } else if (action.actionType === appConstants.APP_WINDOW_CREATED && windowCount === 1) { + updateShareMenuItems(state, true) } break } case appConstants.APP_CHANGE_SETTING: if (action.key === settings.SHOW_BOOKMARKS_TOOLBAR) { // Update the checkbox next to "Bookmarks Toolbar" (Bookmarks menu) - setMenuItemChecked(state, locale.translation('bookmarksToolbar'), action.value) + setMenuItemAttribute(state, locale.translation('bookmarksToolbar'), 'checked', action.value) } if (action.key === settings.DEBUG_ALLOW_MANUAL_TAB_DISCARD) { - setMenuItemChecked(state, 'Allow manual tab discarding', action.value) + setMenuItemAttribute(state, 'Allow manual tab discarding', 'checked', action.value) + } + if (action.key === settings.DEBUG_VERBOSE_TAB_INFO) { + setMenuItemAttribute(state, 'Display tab identifiers', 'checked', action.value) } break case windowConstants.WINDOW_UNDO_CLOSED_FRAME: diff --git a/app/browser/reducers/autoplayReducer.js b/app/browser/reducers/autoplayReducer.js index 9e256db5bfe..ae99ded28c8 100644 --- a/app/browser/reducers/autoplayReducer.js +++ b/app/browser/reducers/autoplayReducer.js @@ -36,8 +36,8 @@ const showAutoplayMessageBox = (state, tabId) => { appActions.showNotification({ buttons: [ - {text: locale.translation('allow')}, - {text: locale.translation('deny')} + {text: locale.translation('deny')}, + {text: locale.translation('allow')} ], message, frameOrigin: origin, @@ -50,7 +50,7 @@ const showAutoplayMessageBox = (state, tabId) => { notificationCallbacks[tabId] = (e, msg, buttonIndex, persist) => { if (msg === message) { appActions.hideNotification(message) - if (buttonIndex === 0) { + if (buttonIndex === 1) { appActions.changeSiteSetting(origin, 'autoplay', true) if (tab && !tab.isDestroyed()) { tab.reload() diff --git a/app/browser/reducers/bookmarkFoldersReducer.js b/app/browser/reducers/bookmarkFoldersReducer.js index 9bc48632343..3c728b3e5e4 100644 --- a/app/browser/reducers/bookmarkFoldersReducer.js +++ b/app/browser/reducers/bookmarkFoldersReducer.js @@ -7,17 +7,15 @@ const Immutable = require('immutable') // State const bookmarksState = require('../../common/state/bookmarksState') const bookmarkFoldersState = require('../../common/state/bookmarkFoldersState') -const bookmarkToolbarState = require('../../common/state/bookmarkToolbarState') // Constants const appConstants = require('../../../js/constants/appConstants') -const siteTags = require('../../../js/constants/siteTags') const {STATE_SITES} = require('../../../js/constants/stateConstants') // Utils const {makeImmutable} = require('../../common/state/immutableUtil') const syncUtil = require('../../../js/state/syncUtil') -const textCalc = require('../../browser/api/textCalc') +const bookmarkUtil = require('../../common/lib/bookmarkUtil') const bookmarkFolderUtil = require('../../common/lib/bookmarkFoldersUtil') const bookmarkFoldersReducer = (state, action, immutableAction) => { @@ -40,12 +38,10 @@ const bookmarkFoldersReducer = (state, action, immutableAction) => { state = syncUtil.updateObjectCache(state, folderDetails, STATE_SITES.BOOKMARK_FOLDERS) folderList = folderList.push(folderDetails) }) - textCalc.calcTextList(folderList) } else { const folderDetails = bookmarkFolderUtil.buildFolder(folder, bookmarkFoldersState.getFolders(state)) state = bookmarkFoldersState.addFolder(state, folderDetails, closestKey) state = syncUtil.updateObjectCache(state, folderDetails, STATE_SITES.BOOKMARK_FOLDERS) - textCalc.calcText(folderDetails, siteTags.BOOKMARK_FOLDER) } break } @@ -66,12 +62,6 @@ const bookmarkFoldersReducer = (state, action, immutableAction) => { state = bookmarkFoldersState.editFolder(state, key, oldFolder, folder) state = syncUtil.updateObjectCache(state, folder, STATE_SITES.BOOKMARK_FOLDERS) - const folderDetails = bookmarkFoldersState.getFolder(state, key) - textCalc.calcText(folderDetails, siteTags.BOOKMARK_FOLDER) - - if (folder.has('parentFolderId') && oldFolder.get('parentFolderId') !== folder.get('parentFolderId')) { - state = bookmarkToolbarState.setToolbars(state) - } break } @@ -83,8 +73,6 @@ const bookmarkFoldersReducer = (state, action, immutableAction) => { break } - const oldFolder = bookmarkFoldersState.getFolder(state, key) - state = bookmarkFoldersState.moveFolder( state, key, @@ -95,13 +83,7 @@ const bookmarkFoldersReducer = (state, action, immutableAction) => { const destinationDetail = bookmarksState.findBookmark(state, action.get('destinationKey')) state = syncUtil.updateObjectCache(state, destinationDetail, STATE_SITES.BOOKMARK_FOLDERS) - if ( - destinationDetail.get('parentFolderId') === 0 || - action.get('destinationKey') === 0 || - oldFolder.get('parentFolderId') === 0 - ) { - state = bookmarkToolbarState.setToolbars(state) - } + bookmarkUtil.closeToolbarIfEmpty(state) break } case appConstants.APP_REMOVE_BOOKMARK_FOLDER: @@ -118,34 +100,12 @@ const bookmarkFoldersReducer = (state, action, immutableAction) => { state = bookmarkFoldersState.removeFolder(state, key) state = syncUtil.updateObjectCache(state, folder, STATE_SITES.BOOKMARK_FOLDERS) }) - state = bookmarkToolbarState.setToolbars(state) } else { const folder = bookmarkFoldersState.getFolder(state, folderKey) state = bookmarkFoldersState.removeFolder(state, folderKey) state = syncUtil.updateObjectCache(state, folder, STATE_SITES.BOOKMARK_FOLDERS) - if (folder.get('parentFolderId') === 0) { - state = bookmarkToolbarState.setToolbars(state) - } - } - break - } - case appConstants.APP_ON_BOOKMARK_FOLDER_WIDTH_CHANGED: - { - if (action.get('folderList', Immutable.List()).isEmpty()) { - break - } - - let updateToolbar = false - action.get('folderList').forEach(item => { - state = bookmarkFoldersState.setWidth(state, item.get('key'), item.get('width')) - if (item.get('parentFolderId') === 0) { - updateToolbar = true - } - }) - - if (updateToolbar) { - state = bookmarkToolbarState.setToolbars(state) } + bookmarkUtil.closeToolbarIfEmpty(state) break } } diff --git a/app/browser/reducers/bookmarkToolbarReducer.js b/app/browser/reducers/bookmarkToolbarReducer.js deleted file mode 100644 index cc75a48dcd8..00000000000 --- a/app/browser/reducers/bookmarkToolbarReducer.js +++ /dev/null @@ -1,44 +0,0 @@ -/* 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 Immutable = require('immutable') - -// Constants -const appConstants = require('../../../js/constants/appConstants') - -// State -const bookmarksState = require('../../common/state/bookmarksState') -const bookmarkFoldersState = require('../../common/state/bookmarkFoldersState') - -// Util -const {makeImmutable} = require('../../common/state/immutableUtil') -const textCalc = require('../../browser/api/textCalc') - -const bookmarkToolbarReducer = (state, action, immutableAction) => { - action = immutableAction || makeImmutable(action) - switch (action.get('actionType')) { - case appConstants.APP_SET_STATE: - { - // update session for 0.21.x version - const bookmarks = bookmarksState.getBookmarks(state) - let list = Immutable.List() - if (bookmarks.first() && !bookmarks.first().has('width')) { - list = bookmarks.toList() - } - - const bookmarkFolders = bookmarkFoldersState.getFolders(state) - if (bookmarkFolders.first() && !bookmarkFolders.first().has('width')) { - list = list.concat(bookmarkFolders.toList()) - } - - if (!list.isEmpty()) { - textCalc.calcTextList(list) - } - } - break - } - return state -} - -module.exports = bookmarkToolbarReducer diff --git a/app/browser/reducers/bookmarksReducer.js b/app/browser/reducers/bookmarksReducer.js index aed8d035b84..135bfed90be 100644 --- a/app/browser/reducers/bookmarksReducer.js +++ b/app/browser/reducers/bookmarksReducer.js @@ -6,11 +6,9 @@ const Immutable = require('immutable') // State const bookmarksState = require('../../common/state/bookmarksState') -const bookmarkToolbarState = require('../../common/state/bookmarkToolbarState') // Constants const appConstants = require('../../../js/constants/appConstants') -const siteTags = require('../../../js/constants/siteTags') const {STATE_SITES} = require('../../../js/constants/stateConstants') // Utils @@ -18,7 +16,6 @@ const {makeImmutable} = require('../../common/state/immutableUtil') const syncUtil = require('../../../js/state/syncUtil') const bookmarkUtil = require('../../common/lib/bookmarkUtil') const bookmarkLocationCache = require('../../common/cache/bookmarkLocationCache') -const textCalc = require('../../browser/api/textCalc') const bookmarksReducer = (state, action, immutableAction) => { action = immutableAction || makeImmutable(action) @@ -44,12 +41,10 @@ const bookmarksReducer = (state, action, immutableAction) => { state = syncUtil.updateObjectCache(state, bookmarkDetail, STATE_SITES.BOOKMARKS) bookmarkList = bookmarkList.push(bookmarkDetail) }) - textCalc.calcTextList(bookmarkList) } else { const bookmarkDetail = bookmarkUtil.buildBookmark(state, bookmark) state = bookmarksState.addBookmark(state, bookmarkDetail, closestKey, !isLeftSide) state = syncUtil.updateObjectCache(state, bookmarkDetail, STATE_SITES.BOOKMARKS) - textCalc.calcText(bookmarkDetail, siteTags.BOOKMARK) } state = bookmarkUtil.updateActiveTabBookmarked(state) @@ -72,12 +67,8 @@ const bookmarksReducer = (state, action, immutableAction) => { const bookmarkDetail = bookmarkUtil.buildEditBookmark(oldBookmark, bookmark) state = bookmarksState.editBookmark(state, oldBookmark, bookmarkDetail) state = syncUtil.updateObjectCache(state, bookmark, STATE_SITES.BOOKMARKS) - textCalc.calcText(bookmarkDetail, siteTags.BOOKMARK) state = bookmarkUtil.updateActiveTabBookmarked(state) - if (oldBookmark.get('parentFolderId') !== bookmarkDetail.get('parentFolderId')) { - state = bookmarkToolbarState.setToolbars(state) - } break } case appConstants.APP_MOVE_BOOKMARK: @@ -88,8 +79,6 @@ const bookmarksReducer = (state, action, immutableAction) => { break } - const oldBookmark = bookmarksState.getBookmark(state, key) - state = bookmarksState.moveBookmark( state, key, @@ -100,14 +89,7 @@ const bookmarksReducer = (state, action, immutableAction) => { const destinationDetail = bookmarksState.findBookmark(state, action.get('destinationKey')) state = syncUtil.updateObjectCache(state, destinationDetail, STATE_SITES.BOOKMARKS) - - if ( - destinationDetail.get('parentFolderId') === 0 || - action.get('destinationKey') === 0 || - oldBookmark.get('parentFolderId') === 0 - ) { - state = bookmarkToolbarState.setToolbars(state) - } + bookmarkUtil.closeToolbarIfEmpty(state) break } case appConstants.APP_REMOVE_BOOKMARK: @@ -121,34 +103,11 @@ const bookmarksReducer = (state, action, immutableAction) => { action.get('bookmarkKey', Immutable.List()).forEach((key) => { state = bookmarksState.removeBookmark(state, key) }) - state = bookmarkToolbarState.setToolbars(state) } else { - const bookmark = bookmarksState.getBookmark(state, bookmarkKey) state = bookmarksState.removeBookmark(state, bookmarkKey) - if (bookmark.get('parentFolderId') === 0) { - state = bookmarkToolbarState.setToolbars(state) - } } state = bookmarkUtil.updateActiveTabBookmarked(state) - break - } - case appConstants.APP_ON_BOOKMARK_WIDTH_CHANGED: - { - if (action.get('bookmarkList', Immutable.List()).isEmpty()) { - break - } - - let updateToolbar = false - action.get('bookmarkList').forEach(item => { - state = bookmarksState.setWidth(state, item.get('key'), item.get('width')) - if (item.get('parentFolderId') === 0) { - updateToolbar = true - } - }) - - if (updateToolbar) { - state = bookmarkToolbarState.setToolbars(state) - } + bookmarkUtil.closeToolbarIfEmpty(state) break } } diff --git a/app/browser/reducers/historyReducer.js b/app/browser/reducers/historyReducer.js index 9802f702f5d..8f53dc5a841 100644 --- a/app/browser/reducers/historyReducer.js +++ b/app/browser/reducers/historyReducer.js @@ -19,6 +19,7 @@ const messages = require('../../../js/constants/messages') const settings = require('../../../js/constants/settings') // Utils +const urlParse = require('../../common/urlParse') const {makeImmutable} = require('../../common/state/immutableUtil') const {remove} = require('../../common/lib/siteSuggestions') const syncUtil = require('../../../js/state/syncUtil') @@ -126,6 +127,24 @@ const historyReducer = (state, action, immutableAction) => { break } + case appConstants.APP_REMOVE_HISTORY_DOMAIN: { + const domain = action.get('domain') + + if (!domain) { + break + } + + historyState.getSites(state).forEach(historySite => { + if (urlParse(historySite.get('location')).hostname === domain) { + state = historyState.removeSite(state, historySite.get('key')) + } + }) + + state = aboutHistoryState.setHistory(state, historyState.getSites(state)) + + break + } + case appConstants.APP_POPULATE_HISTORY: { state = aboutHistoryState.setHistory(state, historyState.getSites(state)) diff --git a/app/browser/reducers/ledgerReducer.js b/app/browser/reducers/ledgerReducer.js index 31bc8045074..9e8c611f0df 100644 --- a/app/browser/reducers/ledgerReducer.js +++ b/app/browser/reducers/ledgerReducer.js @@ -4,22 +4,27 @@ const Immutable = require('immutable') const {BrowserWindow} = require('electron') +const {getWebContents} = require('../webContentsCache') // Constants const appConstants = require('../../../js/constants/appConstants') const windowConstants = require('../../../js/constants/windowConstants') const settings = require('../../../js/constants/settings') +const tabActionConstants = require('../../common/constants/tabAction') +const ledgerStatuses = require('../../common/constants/ledgerStatuses') // State const ledgerState = require('../../common/state/ledgerState') const pageDataState = require('../../common/state/pageDataState') -const migrationState = require('../../common/state/migrationState') const updateState = require('../../common/state/updateState') +const aboutPreferencesState = require('../../common/state/aboutPreferencesState') +const tabState = require('../../common/state/tabState') // Utils +const windows = require('../windows') const ledgerApi = require('../../browser/api/ledger') const ledgerNotifications = require('../../browser/api/ledgerNotifications') -const {makeImmutable} = require('../../common/state/immutableUtil') +const {makeImmutable, makeJS} = require('../../common/state/immutableUtil') const getSetting = require('../../../js/settings').getSetting const ledgerReducer = (state, action, immutableAction) => { @@ -43,16 +48,24 @@ const ledgerReducer = (state, action, immutableAction) => { } case appConstants.APP_BACKUP_KEYS: { - ledgerApi.backupKeys(state, action.get('backupAction')) + state = ledgerApi.backupKeys(state, action.get('backupAction')) break } case appConstants.APP_RECOVER_WALLET: { - state = ledgerApi.recoverKeys( - state, - action.get('useRecoveryKeyFile'), - action.get('recoveryKey') - ) + const recoveryKey = action.get('recoveryKey') + const useRecoveryKeyFile = action.get('useRecoveryKeyFile') + + if (!useRecoveryKeyFile) { + state = aboutPreferencesState.setRecoveryInProgress(state, true) + } + + state = ledgerApi.recoverKeys(state, useRecoveryKeyFile, recoveryKey) + break + } + case appConstants.APP_ON_FILE_RECOVERY_KEYS: + { + state = ledgerApi.fileRecoveryKeys(state, action.get('file')) break } case appConstants.APP_SHUTTING_DOWN: @@ -62,12 +75,15 @@ const ledgerReducer = (state, action, immutableAction) => { } case appConstants.APP_ON_CLEAR_BROWSING_DATA: { - const defaults = state.get('clearBrowsingDataDefaults') + const defaults = state.get('clearBrowsingDataDefaults') || Immutable.Map() const temp = state.get('tempClearBrowsingData', Immutable.Map()) const clearData = defaults ? defaults.merge(temp) : temp - if (clearData.get('browserHistory') && !getSetting(settings.PAYMENTS_ENABLED)) { - state = ledgerState.resetSynopsis(state) - ledgerApi.deleteSynopsis() + if (clearData.get('publishersClear')) { + state = ledgerApi.resetPublishers(state) + } + + if (clearData.get('paymentHistory')) { + state = ledgerApi.clearPaymentHistory(state) } break } @@ -146,21 +162,39 @@ const ledgerReducer = (state, action, immutableAction) => { state = ledgerApi.verifiedP(state, publisherKey) break } - case 'ledgerPinPercentage': - { - const value = action.get('value') - const publisher = ledgerState.getPublisher(state, publisherKey) - if (publisher.isEmpty() || publisher.get('pinPercentage') === value) { - break - } - state = ledgerState.setPublishersProp(state, publisherKey, 'pinPercentage', value) - ledgerApi.savePublisherData(publisherKey, 'pinPercentage', value) - state = ledgerApi.updatePublisherInfo(state, publisherKey) - break - } } break } + case appConstants.APP_ON_LEDGER_PIN_PUBLISHER: + { + const value = action.get('value') + const publisherKey = action.get('publisherKey') + const publisher = ledgerState.getPublisher(state, publisherKey) + + if (publisher.isEmpty() || publisher.get('pinPercentage') === value) { + break + } + + state = ledgerState.setPublishersProp(state, publisherKey, 'pinPercentage', value) + ledgerApi.savePublisherData(publisherKey, 'pinPercentage', value) + state = ledgerApi.updatePublisherInfo(state, publisherKey) + break + } + case appConstants.APP_ADD_PUBLISHER_TO_LEDGER: + { + const tabId = action.get('tabId') + const location = action.get('location') + + if (!location) { + break + } + + const passedTabId = tabId || tabState.TAB_ID_NONE + + state = ledgerApi.addNewLocation(state, location, passedTabId, false, true) + state = ledgerApi.pageDataChanged(state, {}, true) + break + } case appConstants.APP_REMOVE_SITE_SETTING: { const pattern = action.get('hostPattern') @@ -201,7 +235,7 @@ const ledgerReducer = (state, action, immutableAction) => { } case appConstants.APP_ON_FAVICON_RECEIVED: { - state = ledgerState.setPublishersProp(state, action.get('publisherKey'), 'faviconURL', action.get('blob')) + state = ledgerApi.onFavIconReceived(state, action.get('publisherKey'), action.get('blob')) state = ledgerApi.updatePublisherInfo(state) break } @@ -222,13 +256,14 @@ const ledgerReducer = (state, action, immutableAction) => { state = ledgerState.setPublisherOption(state, key, prop, value) break } + case appConstants.APP_ON_PUBLISHERS_OPTION_UPDATE: + { + state = ledgerApi.setPublishersOptions(state, action.get('publishersArray')) + break + } case appConstants.APP_ON_LEDGER_WALLET_CREATE: { ledgerApi.boot() - if (ledgerApi.getNewClient() === null) { - state = migrationState.setConversionTimestamp(state, new Date().getTime()) - state = migrationState.setTransitionStatus(state, false) - } break } case appConstants.APP_ON_BOOT_STATE_FILE: @@ -243,7 +278,7 @@ const ledgerReducer = (state, action, immutableAction) => { } case appConstants.APP_LEDGER_PAYMENTS_PRESENT: { - ledgerApi.paymentPresent(state, action.get('tabId'), action.get('present')) + state = ledgerApi.paymentPresent(state, action.get('tabId'), action.get('present')) break } case appConstants.APP_ON_ADD_FUNDS_CLOSED: @@ -293,7 +328,7 @@ const ledgerReducer = (state, action, immutableAction) => { } case appConstants.APP_ON_RESET_RECOVERY_STATUS: { - state = ledgerState.setRecoveryStatus(state, null) + state = aboutPreferencesState.setRecoveryStatus(state, null) state = ledgerState.setInfoProp(state, 'error', null) break } @@ -302,22 +337,6 @@ const ledgerReducer = (state, action, immutableAction) => { state = ledgerApi.onInitRead(state, action.parsedData) break } - case appConstants.APP_ON_BTC_TO_BAT_NOTIFIED: - { - state = migrationState.setNotifiedTimestamp(state, new Date().getTime()) - break - } - case appConstants.APP_ON_BTC_TO_BAT_BEGIN_TRANSITION: - { - state = migrationState.setTransitionStatus(state, true) - break - } - case appConstants.APP_ON_BTC_TO_BAT_TRANSITIONED: - { - state = migrationState.setConversionTimestamp(state, new Date().getTime()) - state = migrationState.setTransitionStatus(state, false) - break - } case appConstants.APP_ON_LEDGER_QR_GENERATED: { state = ledgerState.saveQRCode(state, action.get('currency'), action.get('image')) @@ -380,22 +399,20 @@ const ledgerReducer = (state, action, immutableAction) => { state = ledgerApi.pageDataChanged(state) break } - case windowConstants.WINDOW_GOT_RESPONSE_DETAILS: + case tabActionConstants.FINISH_NAVIGATION: { if (!getSetting(settings.PAYMENTS_ENABLED)) { break } - // Only capture response for the page (not sub resources, like images, JavaScript, etc) - if (action.getIn(['details', 'resourceType']) === 'mainFrame') { - const pageUrl = action.getIn(['details', 'newURL']) - - // create a page view event if this is a page load on the active tabId - const lastActiveTabId = pageDataState.getLastActiveTabId(state) - const tabId = action.get('tabId') + // create a page view event if this is a page load on the active tabId + const lastActiveTabId = pageDataState.getLastActiveTabId(state) + const tabId = action.get('tabId') + const tab = getWebContents(tabId) + if (tab && !tab.isDestroyed()) { if (!lastActiveTabId || tabId === lastActiveTabId) { state = ledgerApi.pageDataChanged(state, { - location: pageUrl, + location: tab.getURL(), tabId }) } @@ -417,9 +434,24 @@ const ledgerReducer = (state, action, immutableAction) => { state = ledgerNotifications.onPromotionReceived(state) break } + case appConstants.APP_ON_PROMOTION_CLICK: + { + ledgerApi.getCaptcha(state) + break + } + case appConstants.APP_ON_CAPTCHA_RESPONSE: + { + state = ledgerApi.onCaptchaResponse(state, action.get('body')) + break + } + case appConstants.APP_ON_CAPTCHA_CLOSE: + { + state = ledgerState.setPromotionProp(state, 'promotionStatus', null) + break + } case appConstants.APP_ON_PROMOTION_CLAIM: { - ledgerApi.claimPromotion(state) + ledgerApi.claimPromotion(state, action.get('x'), action.get('y')) break } case appConstants.APP_ON_PROMOTION_REMIND: @@ -429,7 +461,7 @@ const ledgerReducer = (state, action, immutableAction) => { } case appConstants.APP_ON_LEDGER_MEDIA_DATA: { - state = ledgerApi.onMediaRequest(state, action.get('url'), action.get('type'), action.get('tabId')) + state = ledgerApi.onMediaRequest(state, action.get('url'), action.get('type'), action.get('details')) break } case appConstants.APP_ON_PRUNE_SYNOPSIS: @@ -471,8 +503,7 @@ const ledgerReducer = (state, action, immutableAction) => { } case appConstants.APP_ON_REFERRAL_CODE_READ: { - state = updateState.setUpdateProp(state, 'referralDownloadId', action.get('downloadId')) - state = updateState.setUpdateProp(state, 'referralPromoCode', action.get('promoCode')) + state = ledgerApi.onReferralRead(state, action.get('body'), windows.getActiveWindowId()) break } case appConstants.APP_ON_REFERRAL_CODE_FAIL: @@ -485,6 +516,20 @@ const ledgerReducer = (state, action, immutableAction) => { state = ledgerApi.checkReferralActivity(state) break } + case appConstants.APP_ON_FETCH_REFERRAL_HEADERS: + { + state = ledgerApi.onFetchReferralHeaders(state, action.get('error'), action.get('response'), action.get('body')) + break + } + case appConstants.APP_ON_LEDGER_FUZZING: + { + state = ledgerState.setAboutProp(state, 'status', ledgerStatuses.FUZZING) + const newStamp = parseInt(action.get('newStamp')) + if (!isNaN(newStamp) && newStamp > 0) { + state = ledgerState.setInfoProp(state, 'reconcileStamp', newStamp) + } + break + } case appConstants.APP_ON_REFERRAL_ACTIVITY: { state = updateState.setUpdateProp(state, 'referralTimestamp', new Date().getTime()) @@ -503,6 +548,28 @@ const ledgerReducer = (state, action, immutableAction) => { ) break } + case appConstants.APP_ON_WALLET_PROPERTIES_ERROR: + { + state = ledgerState.setAboutProp(state, 'status', ledgerStatuses.SERVER_PROBLEM) + break + } + case appConstants.APP_ON_LEDGER_BACKUP_SUCCESS: + { + state = aboutPreferencesState.setBackupStatus(state, true) + break + } + case appConstants.APP_ON_WALLET_DELETE: + { + state = ledgerApi.deleteWallet(state) + break + } + case appConstants.APP_ON_PUBLISHER_TOGGLE_UPDATE: + { + const viewData = makeJS(action.get('viewData')) + state = ledgerApi.pageDataChanged(state, {}, true) + state = ledgerApi.pageDataChanged(state, viewData, true) + break + } } return state } diff --git a/app/browser/reducers/passwordManagerReducer.js b/app/browser/reducers/passwordManagerReducer.js index 00ac942e89e..6b2d3a0edf3 100644 --- a/app/browser/reducers/passwordManagerReducer.js +++ b/app/browser/reducers/passwordManagerReducer.js @@ -50,9 +50,9 @@ const savePassword = (username, origin, tabId) => { }) } - const webContents = getWebContents(tabId) - passwordCallbacks[message] = (buttonIndex) => { + const webContents = getWebContents(tabId) + delete passwordCallbacks[message] appActions.hideNotification(message) @@ -61,13 +61,17 @@ const savePassword = (username, origin, tabId) => { return } if (buttonIndex === 2) { - // never save - webContents.neverSavePassword() + if (webContents && !webContents.isDestroyed()) { + // never save + webContents.neverSavePassword() + } return } - // save password - webContents.savePassword() + if (webContents && !webContents.isDestroyed()) { + // save password + webContents.savePassword() + } } } @@ -96,18 +100,22 @@ const updatePassword = (username, origin, tabId) => { }) } - const webContents = getWebContents(tabId) - passwordCallbacks[message] = (buttonIndex) => { + const webContents = getWebContents(tabId) + delete passwordCallbacks[message] appActions.hideNotification(message) if (buttonIndex === 0) { - webContents.updatePassword() + if (webContents && !webContents.isDestroyed()) { + webContents.updatePassword() + } return } - // never save - webContents.noUpdatePassword() + if (webContents && !webContents.isDestroyed()) { + // never save + webContents.noUpdatePassword() + } } } diff --git a/app/browser/reducers/pinnedSitesReducer.js b/app/browser/reducers/pinnedSitesReducer.js index e717dfe23eb..7ff2c57bb69 100644 --- a/app/browser/reducers/pinnedSitesReducer.js +++ b/app/browser/reducers/pinnedSitesReducer.js @@ -16,15 +16,17 @@ const {STATE_SITES} = require('../../../js/constants/stateConstants') const {makeImmutable} = require('../../common/state/immutableUtil') const syncUtil = require('../../../js/state/syncUtil') const pinnedSitesUtil = require('../../common/lib/pinnedSitesUtil') +const {shouldDebugTabEvents} = require('../../cmdLine') const pinnedSitesReducer = (state, action, immutableAction) => { action = immutableAction || makeImmutable(action) switch (action.get('actionType')) { case appConstants.APP_TAB_UPDATED: { + const tabId = action.getIn(['tabValue', 'tabId']) + // has this tab just been pinned or un-pinned? if (action.getIn(['changeInfo', 'pinned']) != null) { const pinned = action.getIn(['changeInfo', 'pinned']) - const tabId = action.getIn(['tabValue', 'tabId']) const tab = tabState.getByTabId(state, tabId) if (!tab) { console.warn('Trying to pin a tabId which does not exist:', tabId, 'tabs: ', state.get('tabs').toJS()) @@ -38,6 +40,39 @@ const pinnedSitesReducer = (state, action, immutableAction) => { state = pinnedSitesState.removePinnedSite(state, siteDetail) } state = syncUtil.updateObjectCache(state, siteDetail, STATE_SITES.PINNED_SITES) + } else if (action.getIn(['changeInfo', 'index']) != null && tabState.isTabPinned(state, tabId)) { + // The tab index changed and tab is already pinned. + // We cannot rely on the index reported by muon as pinned tabs may not always start at 0, + // and each window may have a different index for each pinned tab, + // but we want the 'order' in pinnedSites to be sequential starting at 0. + // So, focus on the order of the tabs, and make our own index. + const windowId = tabState.getWindowId(state, tabId) + if (windowId == null || windowId === -1) { + break + } + let windowPinnedTabs = tabState.getPinnedTabsByWindowId(state, windowId) + if (!windowPinnedTabs) { + break + } + windowPinnedTabs = windowPinnedTabs.sort((a, b) => a.get('index') - b.get('index')) + const tab = tabState.getByTabId(state, tabId) + const windowPinnedTabIndex = windowPinnedTabs.findIndex(pinnedTab => pinnedTab === tab) + if (windowPinnedTabIndex === -1) { + console.error(`pinnedSitesReducer:APP_TAB_UPDATED: could not find tab ${tabId} as a pinned tab in tabState!`) + } + const siteKey = pinnedSitesUtil.getKey(pinnedSitesUtil.getDetailsFromTab(pinnedSitesState.getSites(state), tab)) + // update state for new order so pinned tabs order is persisted on restart + if (shouldDebugTabEvents) { + console.log(`Moving pinned site '${siteKey}' to order: ${windowPinnedTabIndex}`) + } + const newState = pinnedSitesState.moveSiteToNewOrder(state, siteKey, windowPinnedTabIndex, shouldDebugTabEvents) + // did anything change? + if (newState !== state) { + state = newState + // make sure it's synced + const newSite = pinnedSitesState.getSite(state, siteKey) + state = syncUtil.updateObjectCache(state, newSite, STATE_SITES.PINNED_SITES) + } } break } @@ -49,25 +84,6 @@ const pinnedSitesReducer = (state, action, immutableAction) => { } break } - case appConstants.APP_ON_PINNED_TAB_REORDER: - { - const siteKey = action.get('siteKey') - - if (siteKey == null) { - break - } - - state = pinnedSitesState.reOrderSite( - state, - siteKey, - action.get('destinationKey'), - action.get('prepend') - ) - - const newSite = pinnedSitesState.getSite(state, siteKey) - state = syncUtil.updateObjectCache(state, newSite, STATE_SITES.PINNED_SITES) - break - } } return state diff --git a/app/browser/reducers/siteSettingsReducer.js b/app/browser/reducers/siteSettingsReducer.js index e0a7be29800..eb69de0267a 100644 --- a/app/browser/reducers/siteSettingsReducer.js +++ b/app/browser/reducers/siteSettingsReducer.js @@ -57,6 +57,9 @@ const siteSettingsReducer = (state, action, immutableAction) => { if (action.get('skipSync')) { newEntry = newEntry.set('skipSync', true) } + if (action.get('key') === 'ledgerPaymentsShown') { + newEntry = newEntry.delete('ledgerPayments') + } newSiteSettings = newSiteSettings.set(hostPattern, newEntry) }) state = state.set(propertyName, newSiteSettings) diff --git a/app/browser/reducers/tabsReducer.js b/app/browser/reducers/tabsReducer.js index b2935dbffba..596995be691 100644 --- a/app/browser/reducers/tabsReducer.js +++ b/app/browser/reducers/tabsReducer.js @@ -4,44 +4,93 @@ 'use strict' -const appConfig = require('../../../js/constants/appConfig') -const appConstants = require('../../../js/constants/appConstants') -const tabs = require('../tabs') -const windows = require('../windows') -const {getWebContents} = require('../webContentsCache') const {BrowserWindow} = require('electron') +const Immutable = require('immutable') + +// Actions +const windowActions = require('../../../js/actions/windowActions') + +// State const tabState = require('../../common/state/tabState') +const windowState = require('../../common/state/windowState') const siteSettings = require('../../../js/state/siteSettings') const siteSettingsState = require('../../common/state/siteSettingsState') +const {frameOptsFromFrame} = require('../../../js/state/frameStateUtil') +const updateState = require('../../common/state/updateState') + +// Constants +const appConfig = require('../../../js/constants/appConfig') +const appConstants = require('../../../js/constants/appConstants') const windowConstants = require('../../../js/constants/windowConstants') -const windowActions = require('../../../js/actions/windowActions') +const webrtcConstants = require('../../../js/constants/webrtcConstants') +const dragTypes = require('../../../js/constants/dragTypes') +const tabActionConsts = require('../../common/constants/tabAction') +const appActions = require('../../../js/actions/appActions') +const settings = require('../../../js/constants/settings') + +// Utils +const tabs = require('../tabs') +const windows = require('../windows') +const {getWebContents} = require('../webContentsCache') const {makeImmutable} = require('../../common/state/immutableUtil') const {getFlashResourceId} = require('../../../js/flash') const {l10nErrorText} = require('../../common/lib/httpUtil') -const Immutable = require('immutable') -const dragTypes = require('../../../js/constants/dragTypes') -const tabActionConsts = require('../../common/constants/tabAction') const flash = require('../../../js/flash') -const {frameOptsFromFrame} = require('../../../js/state/frameStateUtil') const {isSourceAboutUrl, isTargetAboutUrl, isNavigatableAboutPage} = require('../../../js/lib/appUrlUtil') - -const WEBRTC_DEFAULT = 'default' -const WEBRTC_DISABLE_NON_PROXY = 'disable_non_proxied_udp' +const {shouldDebugTabEvents} = require('../../cmdLine') const getWebRTCPolicy = (state, tabId) => { + const webrtcSetting = state.getIn(['settings', settings.WEBRTC_POLICY]) + if (webrtcSetting && webrtcSetting !== webrtcConstants.default) { + // Global webrtc setting overrides fingerprinting shield setting + return webrtcSetting + } + const tabValue = tabState.getByTabId(state, tabId) if (tabValue == null) { - return WEBRTC_DEFAULT + return webrtcConstants.default } + const allSiteSettings = siteSettingsState.getAllSiteSettings(state, tabValue.get('incognito') === true) const tabSiteSettings = siteSettings.getSiteSettingsForURL(allSiteSettings, tabValue.get('url')) const activeSiteSettings = siteSettings.activeSettings(tabSiteSettings, state, appConfig) if (!activeSiteSettings || activeSiteSettings.fingerprintingProtection !== true) { - return WEBRTC_DEFAULT + return webrtcConstants.default } else { - return WEBRTC_DISABLE_NON_PROXY + return webrtcConstants.disableNonProxiedUdp + } +} + +function expireContentSettings (state, tabId, origin) { + // Expired Flash settings should be deleted when the webview is + // navigated or closed. Same for NoScript's allow-once option. + const tabValue = tabState.getByTabId(state, tabId) + const isPrivate = tabValue.get('incognito') === true + const allSiteSettings = siteSettingsState.getAllSiteSettings(state, isPrivate) + const tabSiteSettings = + siteSettings.getSiteSettingsForURL(allSiteSettings, tabValue.get('url')) + if (!tabSiteSettings) { + return + } + const originFlashEnabled = tabSiteSettings.get('flash') + const originWidevineEnabled = tabSiteSettings.get('widevine') + const originNoScriptEnabled = tabSiteSettings.get('noScript') + const originNoScriptExceptions = tabSiteSettings.get('noScriptExceptions') + if (typeof originFlashEnabled === 'number') { + if (originFlashEnabled < Date.now()) { + appActions.removeSiteSetting(origin, 'flash', isPrivate) + } + } + if (originWidevineEnabled === 0) { + appActions.removeSiteSetting(origin, 'widevine', isPrivate) + } + if (originNoScriptEnabled === 0) { + appActions.removeSiteSetting(origin, 'noScript', isPrivate) + } + if (originNoScriptExceptions) { + appActions.noScriptExceptionsAdded(origin, originNoScriptExceptions.map(value => value === 0 ? false : value)) } } @@ -52,7 +101,13 @@ const tabsReducer = (state, action, immutableAction) => { case tabActionConsts.START_NAVIGATION: { const tabId = action.get('tabId') + const originalOrigin = tabState.getVisibleOrigin(state, tabId) state = tabState.setNavigationState(state, tabId, action.get('navigationState')) + const newOrigin = tabState.getVisibleOrigin(state, tabId) + // For cross-origin navigation, clear temp approvals + if (originalOrigin !== newOrigin) { + expireContentSettings(state, tabId, originalOrigin) + } setImmediate(() => { tabs.setWebRTCIPHandlingPolicy(tabId, getWebRTCPolicy(state, tabId)) }) @@ -66,28 +121,62 @@ const tabsReducer = (state, action, immutableAction) => { }) break } + case tabActionConsts.FIND_IN_PAGE_REQUEST: + { + const tabId = tabState.resolveTabId(state, action.get('tabId')) + setImmediate(() => { + tabs.findInPage( + tabId, + action.get('searchString'), + action.get('caseSensitivity'), + action.get('forward'), + action.get('findNext') + ) + }) + break + } + case tabActionConsts.STOP_FIND_IN_PAGE_REQUEST: + { + const tabId = tabState.resolveTabId(state, action.get('tabId')) + setImmediate(() => { + tabs.stopFindInPage(tabId) + }) + break + } + case tabActionConsts.ZOOM_CHANGED: + { + const tabId = tabState.resolveTabId(state, action.get('tabId')) + const zoomPercent = action.get('zoomPercent') + state = tabState.setZoomPercent(state, tabId, zoomPercent) + break + } case appConstants.APP_SET_STATE: state = tabs.init(state, action) break case appConstants.APP_TAB_CREATED: state = tabState.maybeCreateTab(state, action) break - case appConstants.APP_TAB_ATTACHED: + case appConstants.APP_TAB_MOVED: state = tabs.updateTabsStateForAttachedTab(state, action.get('tabId')) break - case appConstants.APP_TAB_WILL_ATTACH: { - const tabId = action.get('tabId') - const tabValue = tabState.getByTabId(state, tabId) - if (!tabValue) { + case appConstants.APP_TAB_INSERTED_TO_TAB_STRIP: { + const windowId = action.get('windowId') + if (windowId == null) { break } - const oldWindowId = tabState.getWindowId(state, tabId) - state = tabs.updateTabsStateForWindow(state, oldWindowId) + const tabId = action.get('tabId') + state = tabState.setTabStripWindowId(state, tabId, windowId) + state = tabs.updateTabIndexesForWindow(state, windowId) break } - case appConstants.APP_TAB_MOVED: - state = tabs.updateTabsStateForAttachedTab(state, action.get('tabId')) + case appConstants.APP_TAB_DETACHED_FROM_TAB_STRIP: { + const windowId = action.get('windowId') + if (windowId == null) { + break + } + state = tabs.updateTabIndexesForWindow(state, windowId) break + } case appConstants.APP_TAB_DETACH_MENU_ITEM_CLICKED: { setImmediate(() => { const tabId = action.get('tabId') @@ -131,11 +220,24 @@ const tabsReducer = (state, action, immutableAction) => { const senderWindowId = action.getIn(['senderWindowId']) if (senderWindowId != null) { action = action.setIn(['createProperties', 'windowId'], senderWindowId) - } else if (BrowserWindow.getActiveWindow()) { - action = action.setIn(['createProperties', 'windowId'], BrowserWindow.getActiveWindow().id) + } else { + // no specified window, so use active one, or create one + const activeWindowId = windows.getActiveWindowId() + if (activeWindowId === windowState.WINDOW_ID_NONE) { + setImmediate(() => appActions.newWindow(action.get('createProperties'))) + // this action will get dispatched again + // once the new window is ready to have tabs + break + } + action = action.setIn(['createProperties', 'windowId'], activeWindowId) } } - + // option to focus the window the tab is being created in + const windowId = action.getIn(['createProperties', 'windowId']) + const shouldFocusWindow = action.get('focusWindow') + if (shouldFocusWindow && windowId) { + windows.focus(windowId) + } const url = action.getIn(['createProperties', 'url']) setImmediate(() => { if (action.get('activateIfOpen') || @@ -150,6 +252,19 @@ const tabsReducer = (state, action, immutableAction) => { state = tabState.maybeCreateTab(state, action) // tabs.debugTabs(state) break + case appConstants.APP_TAB_REPLACED: + if (action.get('isPermanent')) { + if (shouldDebugTabEvents) { + console.log('APP_TAB_REPLACED before') + tabs.debugTabs(state) + } + state = tabState.replaceTabValue(state, action.get('oldTabId'), action.get('newTabValue')) + if (shouldDebugTabEvents) { + console.log('APP_TAB_REPLACED after') + tabs.debugTabs(state) + } + } + break case appConstants.APP_TAB_CLOSE_REQUESTED: { let tabId = action.get('tabId') @@ -212,8 +327,10 @@ const tabsReducer = (state, action, immutableAction) => { // But still check for no tabId because on tab detach there's a dummy tabId const tabValue = tabState.getByTabId(state, tabId) if (tabValue) { + const lastOrigin = tabState.getVisibleOrigin(state, tabId) + expireContentSettings(state, tabId, lastOrigin) const windowIdOfTabBeingRemoved = tabState.getWindowId(state, tabId) - state = tabs.updateTabsStateForWindow(state, windowIdOfTabBeingRemoved) + state = tabs.updateTabIndexesForWindow(state, windowIdOfTabBeingRemoved) } state = tabState.removeTabByTabId(state, tabId) tabs.forgetTab(tabId) @@ -258,6 +375,14 @@ const tabsReducer = (state, action, immutableAction) => { tabs.setTabIndex(action.get('tabId'), action.get('index')) }) break + case appConstants.APP_TAB_SET_FULL_SCREEN: { + const isFullscreen = action.get('isFullScreen') + const tabId = action.get('tabId') + if (isFullscreen === true || isFullscreen === false) { + tabs.setFullScreen(tabId, isFullscreen) + } + break + } case appConstants.APP_TAB_TOGGLE_DEV_TOOLS: setImmediate(() => { tabs.toggleDevTools(action.get('tabId')) @@ -334,12 +459,19 @@ const tabsReducer = (state, action, immutableAction) => { } break } - case appConstants.APP_FRAME_CHANGED: - state = tabState.updateFrame(state, action) + case appConstants.APP_FRAMES_CHANGED: + for (const frameAction of action.get('frames').valueSeq()) { + state = tabState.updateFrame(state, frameAction, shouldDebugTabEvents) + } break + // TODO: convert window frame navigation status (load, error, etc) + // to browser actions with data on tab state. This reducer responds to + // actions from both at the moment (browser-side for certificate errors and + // renderer-side for load errors) until all can be refactored. case windowConstants.WINDOW_SET_FRAME_ERROR: + case tabActionConsts.SET_CONTENTS_ERROR: { - const tabId = action.getIn(['frameProps', 'tabId']) + const tabId = action.getIn(['frameProps', 'tabId']) || action.get('tabId') const tab = getWebContents(tabId) if (tab) { let currentIndex = tab.getCurrentEntryIndex() @@ -360,24 +492,40 @@ const tabsReducer = (state, action, immutableAction) => { } break case appConstants.APP_WINDOW_READY: { + // Get the window's id from the action or the sender if (!action.getIn(['createProperties', 'windowId'])) { const senderWindowId = action.getIn(['senderWindowId']) if (senderWindowId) { action = action.setIn(['createProperties', 'windowId'], senderWindowId) } } + // Show welcome tab in first window on first start, + // but not in the buffer window. + const windowId = action.getIn(['createProperties', 'windowId']) + const bufferWindow = windows.getBufferWindow() + if (!bufferWindow || bufferWindow.id !== windowId) { + const welcomeScreenProperties = { + url: 'about:welcome', + windowId + } + const shouldShowWelcomeScreen = state.getIn(['about', 'welcome', 'showOnLoad']) + if (shouldShowWelcomeScreen) { + setImmediate(() => tabs.create(welcomeScreenProperties)) + // We only need to run welcome screen once + state = state.setIn(['about', 'welcome', 'showOnLoad'], false) + } - const welcomeScreenProperties = { - 'url': 'about:welcome', - 'windowId': action.getIn(['createProperties', 'windowId']) - } - - const shouldShowWelcomeScreen = state.getIn(['about', 'welcome', 'showOnLoad']) - if (shouldShowWelcomeScreen) { - setImmediate(() => tabs.create(welcomeScreenProperties)) - // We only need to run welcome screen once - state = state.setIn(['about', 'welcome', 'showOnLoad'], false) + // Show promotion + const page = updateState.getUpdateProp(state, 'referralPage') || null + if (page) { + setImmediate(() => tabs.create({ + url: page, + windowId + })) + state = updateState.setUpdateProp(state, 'referralPage', null) + } } + state = state.set('windowReady', true) break } case appConstants.APP_ENABLE_PEPPER_MENU: { @@ -389,15 +537,21 @@ const tabsReducer = (state, action, immutableAction) => { if (dragData && dragData.get('type') === dragTypes.TAB) { const frame = dragData.get('data') let frameOpts = frameOptsFromFrame(frame) + const draggingTabId = frameOpts.get('tabId') const browserOpts = { positionByMouseCursor: true, checkMaximized: true } const tabIdForIndex = dragData.getIn(['dragOverData', 'draggingOverKey']) - const tabForIndex = tabState.getByTabId(state, tabIdForIndex) + const tabForIndex = tabIdForIndex !== draggingTabId && tabState.getByTabId(state, tabIdForIndex) const dropWindowId = dragData.get('dropWindowId') - if (dropWindowId != null && dropWindowId !== -1 && tabForIndex) { + let newIndex = -1 + // Set new index for new window if last dragged-over tab is in new window. + // Otherwise, could be over another tab's tab strip, but most recently dragged-over a tab in another window. + if (dropWindowId != null && dropWindowId !== -1 && tabForIndex && tabForIndex.get('windowId') === dropWindowId) { const prependIndexByTabId = dragData.getIn(['dragOverData', 'draggingOverLeftHalf']) - frameOpts = frameOpts.set('index', tabForIndex.get('index') + (prependIndexByTabId ? 0 : 1)) + newIndex = tabForIndex.get('index') + (prependIndexByTabId ? 0 : 1) } - tabs.moveTo(state, frame.get('tabId'), frameOpts, browserOpts, dragData.get('dropWindowId')) + // ensure the tab never moves window with its original index + frameOpts = frameOpts.set('index', newIndex) + tabs.moveTo(state, draggingTabId, frameOpts, browserOpts, dragData.get('dropWindowId')) } break } diff --git a/app/browser/reducers/windowsReducer.js b/app/browser/reducers/windowsReducer.js index 808a44345db..73abc4e31eb 100644 --- a/app/browser/reducers/windowsReducer.js +++ b/app/browser/reducers/windowsReducer.js @@ -158,7 +158,7 @@ function setDefaultWindowSize (state) { return state } -const handleCreateWindowAction = (state, action) => { +const handleCreateWindowAction = (state, action = Immutable.Map()) => { const frameOpts = (action.get('frameOpts') || Immutable.Map()).toJS() let browserOpts = (action.get('browserOpts') || Immutable.Map()).toJS() let immutableWindowState = action.get('restoredState') || Immutable.Map() @@ -305,6 +305,14 @@ const windowsReducer = (state, action, immutableAction) => { }) } break + case appConstants.APP_FOCUS_OR_CREATE_WINDOW: + const activeWindowId = windows.getActiveWindowId() + if (activeWindowId === windowState.WINDOW_ID_NONE) { + state = handleCreateWindowAction(state) + } else { + windows.focus(activeWindowId) + } + break case appConstants.APP_CLOSE_WINDOW: windows.closeWindow(action.get('windowId')) break @@ -316,10 +324,7 @@ const windowsReducer = (state, action, immutableAction) => { case appConstants.APP_WINDOW_CREATED: case appConstants.APP_WINDOW_RESIZED: { - const bookmarkToolbarState = require('../../common/state/bookmarkToolbarState') state = windowState.maybeCreateWindow(state, action) - const windowId = action.getIn(['windowValue', 'windowId'], windowState.WINDOW_ID_NONE) - state = bookmarkToolbarState.setToolbar(state, windowId) break } case appConstants.APP_TAB_STRIP_EMPTY: diff --git a/app/browser/share.js b/app/browser/share.js index 99e0a35164f..4bbc196ea26 100644 --- a/app/browser/share.js +++ b/app/browser/share.js @@ -15,7 +15,6 @@ const templateUrls = { googlePlus: 'https://plus.google.com/share?url={url}', linkedIn: 'https://www.linkedin.com/shareArticle?url={url}&title={title}', buffer: 'https://buffer.com/add?text={title}&url={url}', - digg: 'https://digg.com/submit?url={url}&title={title}', reddit: 'https://reddit.com/submit?url={url}&title={title}' } diff --git a/app/browser/tabMessageBox.js b/app/browser/tabMessageBox.js index 03c6286851d..fe95a5924a4 100644 --- a/app/browser/tabMessageBox.js +++ b/app/browser/tabMessageBox.js @@ -1,6 +1,7 @@ const appActions = require('../../js/actions/appActions') const tabMessageBoxState = require('../common/state/tabMessageBoxState') const {makeImmutable} = require('../common/state/immutableUtil') +const locale = require('../../app/locale') // callbacks for alert, confirm, etc. let messageBoxCallbacks = {} @@ -13,6 +14,23 @@ const cleanupCallback = (tabId) => { return false } +const onWindowPrompt = show => (webContents, extraData, title, message, defaultPromptText, + shouldDisplaySuppressCheckbox, isBeforeUnloadDialog, isReload, muonCb) => { + const tabId = webContents.getId() + const detail = { + message, + title, + buttons: [locale.translation('messageBoxOk'), locale.translation('messageBoxCancel')], + cancelId: 1, + suppress: false, + allowInput: true, + defaultPromptText, + showSuppress: shouldDisplaySuppressCheckbox + } + + show(tabId, detail, muonCb) +} + const tabMessageBox = { init: (state, action) => { process.on('window-alert', (webContents, extraData, title, message, defaultPromptText, @@ -21,7 +39,7 @@ const tabMessageBox = { const detail = { message, title, - buttons: ['ok'], + buttons: [locale.translation('messageBoxOk')], suppress: false, showSuppress: shouldDisplaySuppressCheckbox } @@ -35,7 +53,7 @@ const tabMessageBox = { const detail = { message, title, - buttons: ['ok', 'cancel'], + buttons: [locale.translation('messageBoxOk'), locale.translation('messageBoxCancel')], cancelId: 1, suppress: false, showSuppress: shouldDisplaySuppressCheckbox @@ -44,12 +62,7 @@ const tabMessageBox = { tabMessageBox.show(tabId, detail, muonCb) }) - process.on('window-prompt', (webContents, extraData, title, message, defaultPromptText, - shouldDisplaySuppressCheckbox, isBeforeUnloadDialog, isReload, muonCb) => { - console.warn('window.prompt is not supported yet') - let suppress = false - muonCb(null, '', suppress) - }) + process.on('window-prompt', onWindowPrompt(tabMessageBox.show)) return state }, @@ -70,6 +83,7 @@ const tabMessageBox = { const muonCb = messageBoxCallbacks[tabId] let suppress = false let result = true + let input = '' state = tabMessageBoxState.removeDetail(state, action) if (muonCb) { cleanupCallback(tabId) @@ -80,7 +94,10 @@ const tabMessageBox = { if (detail.has('result')) { result = detail.get('result') } - muonCb(result, '', suppress) + if (detail.has('input')) { + input = detail.get('input') + } + muonCb(result, input, suppress) } else { muonCb(false, '', false) } @@ -130,3 +147,4 @@ const tabMessageBox = { } module.exports = tabMessageBox +module.exports.onWindowPrompt = onWindowPrompt diff --git a/app/browser/tabs.js b/app/browser/tabs.js index b5e0da8b6c4..0533ccc862c 100644 --- a/app/browser/tabs.js +++ b/app/browser/tabs.js @@ -3,13 +3,11 @@ * You can obtain one at http://mozilla.org/MPL/2.0/. */ const appActions = require('../../js/actions/appActions') -const windowActions = require('../../js/actions/windowActions') const tabActions = require('../common/actions/tabActions') -const config = require('../../js/constants/config') const Immutable = require('immutable') const { shouldDebugTabEvents } = require('../cmdLine') const tabState = require('../common/state/tabState') -const {app, BrowserWindow, extensions, session, ipcMain} = require('electron') +const {app, extensions, session, ipcMain} = require('electron') const {makeImmutable, makeJS} = require('../common/state/immutableUtil') const {getTargetAboutUrl, getSourceAboutUrl, isSourceAboutUrl, newFrameUrl, isTargetAboutUrl, isIntermediateAboutPage, isTargetMagnetUrl, getSourceMagnetUrl} = require('../../js/lib/appUrlUtil') const {isURL, getUrlFromInput, toPDFJSLocation, getDefaultFaviconUrl, isHttpOrHttps, getLocationIfPDF} = require('../../js/lib/urlutil') @@ -25,6 +23,7 @@ const aboutHistoryState = require('../common/state/aboutHistoryState') const aboutNewTabState = require('../common/state/aboutNewTabState') const appStore = require('../../js/stores/appStore') const appConfig = require('../../js/constants/appConfig') +const config = require('../../js/constants/config') const {newTabMode} = require('../common/constants/settingsEnums') const {tabCloseAction} = require('../common/constants/settingsEnums') const webContentsCache = require('./webContentsCache') @@ -37,7 +36,7 @@ const historyState = require('../common/state/historyState') const siteSettingsState = require('../common/state/siteSettingsState') const bookmarkOrderCache = require('../common/cache/bookmarkOrderCache') const ledgerState = require('../common/state/ledgerState') -const {getWindow} = require('./windows') +const {getWindow, notifyWindowWebContentsAdded} = require('./windows') const activeTabHistory = require('./activeTabHistory') let adBlockRegions @@ -66,6 +65,8 @@ const getTabValue = function (tabId) { tabValue = tabValue.set('guestInstanceId', tab.guestInstanceId) tabValue = tabValue.set('partition', tab.session.partition) tabValue = tabValue.set('partitionNumber', getPartitionNumber(tab.session.partition)) + tabValue = tabValue.set('isPlaceholder', tab.isPlaceholder()) + tabValue = tabValue.set('zoomPercent', tab.getZoomPercent()) return tabValue.set('tabId', tabId) } } @@ -73,8 +74,15 @@ const getTabValue = function (tabId) { const updateTab = (tabId, changeInfo = {}) => { let tabValue = getTabValue(tabId) if (shouldDebugTabEvents) { - console.log('tab updated from muon', { tabId, changeIndex: changeInfo.index, changeActive: changeInfo.active, newIndex: tabValue && tabValue.get('index'), newActive: tabValue && tabValue.get('active') }) + console.log(`Tab [${tabId}] updated from muon. changeInfo:`, changeInfo, 'currentValues:', { + newIndex: tabValue && tabValue.get('index'), + newActive: tabValue && tabValue.get('active'), + windowId: tabValue && tabValue.get('windowId'), + isPlaceholder: tabValue && tabValue.get('isPlaceholder'), + guestInstanceId: tabValue && tabValue.get('guestInstanceId') + }) } + if (tabValue) { appActions.tabUpdated(tabValue, makeImmutable(changeInfo)) } @@ -148,6 +156,10 @@ ipcMain.on(messages.ABOUT_COMPONENT_INITIALIZED, (e) => { const tabId = tab.getId() aboutTabs[tabId] = {} + if (shouldDebugTabEvents) { + console.log(`Tab [${tabId}] ABOUT_COMPONENT_INITIALIZED`) + } + const url = getSourceAboutUrl(tab.getURL()) const location = getBaseUrl(url) if (location === 'about:preferences') { @@ -181,13 +193,17 @@ const getBookmarksData = (state) => { .set('bookmarkFolders', bookmarkFoldersState.getFolders(state)) .set('bookmarkOrder', bookmarkOrderCache.getOrderCache(state)) } - +const shouldDebugAboutData = false const sendAboutDetails = (tabId, type, value, shared = false) => { // use a weak map to avoid holding references to large objects that will never be equal to anything aboutTabs[tabId][type] = aboutTabs[tabId][type] || new WeakMap() if (aboutTabs[tabId] && !aboutTabs[tabId][type].get(value)) { const tab = webContentsCache.getWebContents(tabId) if (tab && !tab.isDestroyed()) { + if (shouldDebugAboutData) { + console.log(`Tab [${tabId}] sendAboutDetails(${type})`) + } + if (shared) { const handle = muon.shared_memory.create(makeJS(value)) tab.sendShared(type, handle) @@ -196,6 +212,28 @@ const sendAboutDetails = (tabId, type, value, shared = false) => { } aboutTabs[tabId][type] = new WeakMap() aboutTabs[tabId][type].set(value, true) + } else { + if (shouldDebugAboutData) { + const isNull = !tab + const isDestroyed = tab && tab.isDestroyed() + const reason = isNull + ? 'tab is null' + : isDestroyed + ? 'tab is destroyed' + : '' + console.log(`Tab [${tabId}] skipping sendAboutDetails(${type}); ${reason}`) + } + } + } else { + if (shouldDebugAboutData) { + const tabFalsey = !aboutTabs[tabId] + const tabHasValue = aboutTabs[tabId] && !!aboutTabs[tabId][type].get(value) + const reason = tabFalsey + ? 'tab is falsey' + : tabHasValue + ? 'tab has a value' + : '' + console.log(`Tab [${tabId}] skipping sendAboutDetails(${type}); ${reason}`) } } } @@ -204,6 +242,9 @@ const updateAboutDetails = (tabId) => { const appState = appStore.getState() const tabValue = tabState.getByTabId(appState, tabId) if (!tabValue) { + if (shouldDebugTabEvents) { + console.log(`Tab [${tabId}] updateAboutDetails - unable to get tabValue from tabState`) + } return } @@ -230,7 +271,7 @@ const updateAboutDetails = (tabId) => { if (location === 'about:contributions' || onPaymentsPage) { const ledgerInfo = ledgerState.getInfoProps(appState) const preferencesData = appState.getIn(['about', 'preferences'], Immutable.Map()) - const synopsis = appState.getIn(['ledger', 'about']) + const synopsis = ledgerState.getAboutData(appState) const migration = appState.get('migrations') const wizardData = ledgerState.geWizardData(appState) const ledgerData = ledgerInfo @@ -239,6 +280,7 @@ const updateAboutDetails = (tabId) => { .set('wizardData', wizardData) .set('migration', migration) .set('promotion', ledgerState.getAboutPromotion(appState)) + .set('tabId', tabId) sendAboutDetails(tabId, messages.LEDGER_UPDATED, ledgerData) } else if (url === 'about:preferences#sync' || location === 'about:contributions' || onPaymentsPage) { const sync = appState.get('sync', Immutable.Map()) @@ -274,6 +316,11 @@ const updateAboutDetails = (tabId) => { sendAboutDetails(tabId, messages.DOWNLOADS_UPDATED, { downloads: downloads.toJS() }) + } else if (location === 'about:printkeys') { + const phrase = ledgerState.getInfoProp(appState, 'passphrase') + sendAboutDetails(tabId, messages.PRINTKEYS_UPDATED, { + passphrase: phrase + }) } else if (location === 'about:passwords') { autofill.getAutofillableLogins((result) => { sendAboutDetails(tabId, messages.PASSWORD_DETAILS_UPDATED, result) @@ -400,29 +447,6 @@ const createNavigationState = (navigationHandle, controller) => { }) } -let backgroundProcess = null -let backgroundProcessTimer = null -/** - * Execute script in the browser tab - * @param win{object} - window in which we want to execute script - * @param debug{boolean} - would you like to close window or not - * @param script{string} - script that you want to execute - * @param cb{function} - function that we call after script is completed - */ -const runScript = (win, debug, script, cb) => { - win.webContents.executeScriptInTab(config.braveExtensionId, script, {}, (err, url, result) => { - cb(err, url, result) - if (!debug) { - backgroundProcessTimer = setTimeout(() => { - if (backgroundProcess) { - win.close() - backgroundProcess = null - } - }, 2 * 60 * 1000) // 2 min - } - }) -} - const api = { init: (state, action) => { process.on('open-url-from-tab', (e, source, targetUrl, disposition) => { @@ -434,6 +458,9 @@ const api = { }) process.on('add-new-contents', (e, source, newTab, disposition, size, userGesture) => { + if (shouldDebugTabEvents) { + console.log(`Contents [${newTab.getId()}] process:add-new-contents`, {userGesture, isBackground: newTab.isBackgroundPage(), disposition}) + } if (userGesture === false) { e.preventDefault() return @@ -448,6 +475,10 @@ const api = { return } + if (!newTab.isTab()) { + return + } + let displayURL = newTab.getURL() let location = displayURL || 'about:blank' const openerTabId = !source.isDestroyed() ? source.getId() : -1 @@ -458,7 +489,10 @@ const api = { } const tabId = newTab.getId() + + // update our webContents Map with the openerTabId webContentsCache.updateWebContents(tabId, newTab, openerTabId) + let newTabValue = getTabValue(newTab.getId()) let windowId @@ -495,8 +529,14 @@ const api = { const windowOpts = makeImmutable(size) appActions.newWindow(makeImmutable(frameOpts), windowOpts) } else { - // TODO(bridiver) - use tabCreated in place of newWebContentsAdded - appActions.newWebContentsAdded(windowId, frameOpts, newTabValue) + // Unfortunately we rely on tab events in the renderer window process + // so use an IPC call here to notify that process of the new tab to track. + // TODO: move all events to browser process (this module) and do not handle all + // tab events inside the window. + if (shouldDebugTabEvents) { + console.log('notifyWindowWebContentsAdded: on tab creation in existing window', windowId) + } + notifyWindowWebContentsAdded(windowId, frameOpts, newTabValue.toJS()) } }) @@ -509,7 +549,7 @@ const api = { process.on('chrome-tabs-updated', (tabId, changeInfo) => { if (shouldDebugTabEvents) { - console.log(`tab [${tabId} via process] chrome-tabs-updated`) + console.log(`tab [${tabId} via process] chrome-tabs-updated`, changeInfo) } updateTab(tabId, changeInfo) }) @@ -521,11 +561,15 @@ const api = { }) app.on('web-contents-created', function (event, tab) { - if (tab.isBackgroundPage() || !tab.isGuest()) { + if (!tab.isTab()) { return } const tabId = tab.getId() + // This is the first and most consistent event for WebContents so cache the item in the Map. + // Not all contents will get the 'add-new-contents' event (e.g. replaced contents during tab discard). + webContentsCache.updateWebContents(tabId, tab, null) + // command-line flag --debug-tab-events if (shouldDebugTabEvents) { console.log(`Tab [${tabId}] created in window ${tab.tabValue().windowId}`) @@ -586,15 +630,21 @@ const api = { updateTab(tabId) }) - tab.on('tab-moved', () => { - appActions.tabMoved(tabId) + tab.on('tab-moved', (e, fromIndex, toIndex) => { + const tabValue = getTabValue(tabId) + appActions.tabMoved(tabId, fromIndex, toIndex, tabValue && tabValue.get('windowId')) + }) + + tab.on('pinned', (e, isPinned) => { + updateTab(tabId) }) tab.on('will-attach', (e, windowWebContents) => { - appActions.tabWillAttach(tab.getId()) + // tab will attach to webview }) tab.on('set-active', (sender, isActive) => { + updateTab(tab.getId(), { active: isActive }) if (isActive) { const tabValue = getTabValue(tabId) if (tabValue) { @@ -609,31 +659,31 @@ const api = { } }) - tab.on('tab-strip-empty', () => { - // It's only safe to close a window when the last web-contents tab has been - // re-attached. A detach which already happens by this point is not enough. - // Otherwise the closing window will destroy the tab web-contents and it'll - // lead to a dead tab. The destroy will happen because the old main window - // webcontents is still the embedder. - const tabValue = getTabValue(tabId) - const windowId = tabValue.get('windowId') - tab.once('did-attach', () => { - appActions.tabStripEmpty(windowId) - }) - }) + tab.on('tab-replaced-at', (e, windowId, tabIndex, newContents) => { + // if not a placeholder, new contents is permanent replacement, e.g. tab has been discarded + // if is a placeholder, new contents is temporary, and should not be used for tab ID + const isPlaceholder = newContents.isPlaceholder() + const newTabId = newContents.getId() + const tabValue = getTabValue(newTabId) + if (shouldDebugTabEvents) { + if (isPlaceholder) { + console.log(`Tab [${tabId}] got a new placeholder (${newTabId}), not updating state.`) + } else { + console.log(`Tab [${tabId}] permanently changed to tabId ${newTabId}. Updating state references...`) + } + } - tab.on('did-detach', (e, oldTabId) => { - // forget last active trail in window tab - // is detaching from - const oldTab = getTabValue(oldTabId) - const detachedFromWindowId = oldTab.get('windowId') - if (detachedFromWindowId != null) { - activeTabHistory.clearTabFromWindow(detachedFromWindowId, oldTabId) + // update state + appActions.tabReplaced(tabId, tabValue, windowId, !isPlaceholder) + if (!isPlaceholder) { + // update in-memory caches + webContentsCache.tabIdChanged(tabId, newTabId) + activeTabHistory.tabIdChanged(tabId, newTabId) } }) tab.on('did-attach', (e, tabId) => { - appActions.tabAttached(tab.getId()) + // tab has been attached to a webview }) tab.on('save-password', (e, username, origin) => { @@ -644,10 +694,26 @@ const api = { appActions.updatePassword(username, origin, tabId) }) - tab.on('did-get-response-details', (evt, status, newURL, originalURL, httpResponseCode, requestMethod, referrer, headers, resourceType) => { - if (resourceType === 'mainFrame') { - windowActions.gotResponseDetails(tabId, {status, newURL, originalURL, httpResponseCode, requestMethod, referrer, resourceType}) + tab.on('enter-html-full-screen', () => { + let tabValue = getTabValue(tabId) + if (!tabValue) { + return + } + const windowId = tabValue.get('windowId') + appActions.tabSetFullScreen(tabId, true, true, windowId) + // disable the fullscreen warning after 5 seconds + setTimeout(() => { + appActions.tabSetFullScreen(tabId, undefined, false, windowId) + }, 5000) + }) + + tab.on('leave-html-full-screen', () => { + let tabValue = getTabValue(tabId) + if (!tabValue) { + return } + const windowId = tabValue.get('windowId') + appActions.tabSetFullScreen(tabId, false, false, windowId) }) tab.on('media-started-playing', (e) => { @@ -667,22 +733,10 @@ const api = { }) tab.once('will-destroy', (e) => { + api.willBeRemovedFromWindow(tabId) const tabValue = getTabValue(tabId) if (tabValue) { const windowId = tabValue.get('windowId') - // forget about this tab in the history of active tabs - activeTabHistory.clearTabFromWindow(windowId, tabId) - // handle closed tab being the current active tab for window - if (tabValue.get('active')) { - // set the next active tab, if different from what muon will have set to - // Muon sets it to the next index (immediately above or below) - // But this app can be configured to select the parent tab, - // or the last active tab - let nextTabId = api.getNextActiveTabId(windowId, tabId) - if (nextTabId != null) { - api.setActive(nextTabId) - } - } // let the state know appActions.tabClosed(tabId, windowId) } @@ -745,6 +799,9 @@ const api = { }, setActive: (tabId) => { + if (shouldDebugTabEvents) { + console.log(`tabs.setActive: ${tabId}`) + } let tab = webContentsCache.getWebContents(tabId) if (tab && !tab.isDestroyed()) { tab.setActive(true) @@ -760,14 +817,22 @@ const api = { reload: (tabId, ignoreCache = false) => { const tab = webContentsCache.getWebContents(tabId) + let isIntermediate = false if (tab && !tab.isDestroyed()) { // TODO(bridiver) - removeEntryAtIndex for intermediate about pages after loading - if (isIntermediateAboutPage(getSourceAboutUrl(tab.getURL()))) { + isIntermediate = isIntermediateAboutPage(getSourceAboutUrl(tab.getURL())) + if (isIntermediate) { tab.goToOffset(-1) } else { tab.reload(ignoreCache) } } + + if (shouldDebugTabEvents) { + const isNull = !tab + const isDestroyed = tab && tab.isDestroyed() + console.log(`Tab [${tabId}] reload - ignoreCache=${ignoreCache}, tab null: ${isNull}, tab.isDestroyed: ${isDestroyed}, isIntermediateAboutPage: ${isIntermediate}`) + } }, discard: (tabId) => { @@ -782,16 +847,44 @@ const api = { } }, + willBeRemovedFromWindow (tabId) { + const tabValue = getTabValue(tabId) + if (tabValue) { + const windowId = tabValue.get('windowId') + const wasActive = tabValue.get('active') + if (shouldDebugTabEvents) { + console.log(`tab ${tabId} will be removed from window ${windowId}, wasActive: ${wasActive}`) + } + // forget about this tab in the history of active tabs + activeTabHistory.clearTabFromWindow(windowId, tabId) + // handle closed tab being the current active tab for window + if (wasActive) { + // set the next active tab, if different from what muon will have set to + // Muon sets it to the next index (immediately above or below) + // But this app can be configured to select the parent tab, + // or the last active tab + let nextTabId = api.getNextActiveTabId(windowId, tabId) + if (nextTabId != null) { + if (shouldDebugTabEvents) { + console.log(`Got next active tab Id of ${nextTabId}`) + } + api.setActive(nextTabId) + } + } + } + }, + loadURL: (action) => { action = makeImmutable(action) const tabId = action.get('tabId') const tab = webContentsCache.getWebContents(tabId) if (tab && !tab.isDestroyed()) { - let url = normalizeUrl(action.get('url')) + const url = normalizeUrl(action.get('url')) + const currentUrl = tab.getURL() // We only allow loading URLs explicitly when the origin is // the same for pinned tabs. This is to help preserve a users // pins. - if (tab.pinned && getOrigin(tab.getURL()) !== getOrigin(url)) { + if (tab.pinned && getOrigin(currentUrl) !== getOrigin(url)) { api.create({ url, partition: tab.session.partition @@ -799,7 +892,19 @@ const api = { return } - tab.loadURL(url) + const parsed = muon.url.parse(url) + // Set reloadMatchingUrl to true for hash URLs as workaround for + // https://github.com/brave/browser-laptop/issues/14231. (muon emits + // security-style-changed to insecure when a hash URL is loaded using + // tab.loadURL in a tab with the same URL) + const reloadMatchingUrl = action.get('reloadMatchingUrl') || + (parsed && parsed.hash) || + false + if (reloadMatchingUrl && currentUrl === url) { + tab.reload(true) + } else { + tab.loadURL(url) + } } }, @@ -822,11 +927,17 @@ const api = { setAudioMuted: (action) => { action = makeImmutable(action) const muted = action.get('muted') + // We're crossing into type-safe muon code so make sure args + // are of correct type + if (typeof muted !== 'boolean') { + return + } const tabId = action.get('tabId') const tab = webContentsCache.getWebContents(tabId) - if (tab && !tab.isDestroyed()) { - tab.setAudioMuted(muted) + if (!tab || tab.isDestroyed()) { + return } + tab.setAudioMuted(muted) }, clone: (action) => { @@ -873,6 +984,9 @@ const api = { }, closeTab: (tabId, forceClosePinned = false) => { + if (shouldDebugTabEvents) { + console.log(`[${tabId}] tabs.closeTab(forceClosePinned: ${forceClosePinned})`) + } const tabValue = getTabValue(tabId) if (!tabValue) { return false @@ -934,19 +1048,15 @@ const api = { if (preventLazyLoad) { createProperties.discarded = false } - // Similarly, autoDiscardable will happen for regular tabs (not about: tabs) - const preventAutoDiscard = createProperties.pinned || !isRegularContent - if (preventAutoDiscard) { - createProperties.autoDiscardable = false - } else { - // (temporarily) forced autoDiscardable to ALWAYS false due to - // inability to switch to auto-discarded tabs. - // See https://github.com/brave/browser-laptop/issues/10673 - // remove this forced 'else' condition when #10673 is resolved - createProperties.autoDiscardable = false - } + // autoDiscardable can happen for all tabs + // TODO(petemill): if there are schemes / Urls that should not be autodiscarded + // then the flag should be exposed from muon and set on each URL change for a tab + createProperties.autoDiscardable = true const doCreate = () => { + if (shouldDebugTabEvents) { + console.log('Creating tab with properties: ', createProperties) + } extensions.createTab(createProperties, (tab) => { cb && cb(tab) }) @@ -969,94 +1079,81 @@ const api = { }) }, - /** - * Execute script in the background browser window - * @param script{string} - script that we want to run - * @param cb{function} - function that we want to call when script is done - * @param debug{boolean} - would you like to keep browser window when script is done - */ - executeScriptInBackground: (script, cb, debug = false) => { - if (backgroundProcessTimer) { - clearTimeout(backgroundProcessTimer) - } - - if (backgroundProcess === null) { - backgroundProcess = new BrowserWindow({ - show: debug, - webPreferences: { - partition: 'default' - } - }) - - backgroundProcess.webContents.on('did-finish-load', () => { - runScript(backgroundProcess, debug, script, cb) - }) - backgroundProcess.loadURL('about:blank') - } else { - runScript(backgroundProcess, debug, script, cb) - } - }, - moveTo: (state, tabId, frameOpts, browserOpts, toWindowId) => { frameOpts = makeImmutable(frameOpts) browserOpts = makeImmutable(browserOpts) + if (shouldDebugTabEvents) { + console.log(`Tab [${tabId}] tabs.moveTo(window: ${toWindowId}, index: ${frameOpts.get('index')})`) + } const tab = webContentsCache.getWebContents(tabId) - if (tab && !tab.isDestroyed()) { - const tabValue = getTabValue(tabId) - const guestInstanceId = tabValue && tabValue.get('guestInstanceId') - if (guestInstanceId != null) { - frameOpts.set('guestInstanceId', guestInstanceId) - } - - const currentWindowId = tabValue && tabValue.get('windowId') - if (toWindowId != null && currentWindowId === toWindowId) { + if (!tab || tab.isDestroyed()) { + return + } + const tabValue = getTabValue(tabId) + // don't move it to the same window + const currentWindowId = tabValue && tabValue.get('windowId') + if (toWindowId != null && currentWindowId === toWindowId) { + return + } + // If there's only one tab and we're dragging outside the window, then disallow + // a new window to be created. + if (toWindowId == null || toWindowId === -1) { + const windowTabCount = tabState.getTabsByWindowId(state, currentWindowId).size + if (windowTabCount === 1) { return } - - if (toWindowId == null || toWindowId === -1) { - // If there's only one tab and we're dragging outside the window, then disallow - // a new window to be created. - const windowTabCount = tabState.getTabsByWindowId(state, currentWindowId).size - if (windowTabCount === 1) { - return - } - } - - if (tabValue.get('pinned')) { - // If the current tab is pinned, then don't allow to drag out - return + } + // If the current tab is pinned, then don't allow to drag out + if (tabValue.get('pinned')) { + return + } + // + // perform the actual moving + // + + // It's only safe to close a window when the last web-contents tab has been + // re-attached. A tab-removed-at or tab-strip-empty is not enough. + // Otherwise the closing window will destroy the tab web-contents by the time it gets to the new window. + // The destroy will happen because the old main window + // webcontents is still the embedder. + tab.once('did-attach', () => { + // let the window know the tab has moved to another window, so + // it is free to close + const win = getWindow(currentWindowId) + if (win && !win.isDestroyed()) { + win.webContents.emit('detached-tab-new-window') } - - // detach from current window - tab.detach(() => { - // handle tab has detached from window - // handle tab was the active tab of the window - if (tabValue.get('active')) { - // decide and set next-active-tab muon override - const nextActiveTabIdForOldWindow = api.getNextActiveTabId(currentWindowId, tabId) - if (nextActiveTabIdForOldWindow !== null) { - api.setActive(nextActiveTabIdForOldWindow) - } - } - if (toWindowId == null || toWindowId === -1) { - // move tab to a new window - frameOpts = frameOpts.set('index', 0) - appActions.newWindow(frameOpts, browserOpts) - } else { - // ask for tab to be attached (via frame state and webview) to - // specified window - appActions.newWebContentsAdded(toWindowId, frameOpts, tabValue) - } - // handle tab has made it to the new window - tab.once('did-attach', () => { - // put the tab in the desired index position - const index = frameOpts.get('index') - if (index !== undefined) { - api.setTabIndex(tabId, frameOpts.get('index')) - } - }) - }) + }) + // make sure frame has latest guestinstanceid + const guestInstanceId = tabValue && tabValue.get('guestInstanceId') + if (guestInstanceId != null) { + frameOpts.set('guestInstanceId', guestInstanceId) + } + // create a new window if required + if (toWindowId == null || toWindowId === -1) { + // this will eventually call tab.moveTo when the window is known + api.willBeRemovedFromWindow(tabId, currentWindowId) + appActions.newWindow(frameOpts, browserOpts) + return + } + // use existing window + let toIndex = frameOpts.get('index') + // invalid index? add to end of tab strip by specifying -1 index + if (toIndex == null || toIndex === -1) { + toIndex = -1 + frameOpts = frameOpts.set('index', -1) } + const win = getWindow(toWindowId) + if (!win || win.isDestroyed()) { + console.error('Error: invalid window to move tab to') + return + } + api.willBeRemovedFromWindow(tabId, currentWindowId) + if (shouldDebugTabEvents) { + console.log('notifyWindowWebContentsAdded: on tab move to existing window', toWindowId) + } + notifyWindowWebContentsAdded(toWindowId, frameOpts.toJS(), tabValue.toJS()) + tab.moveTo(toIndex, toWindowId) }, maybeCreateTab: (state, createProperties) => { @@ -1082,6 +1179,47 @@ const api = { } }, + setFullScreen (tabId, isFullScreen) { + const tab = webContentsCache.getWebContents(tabId) + if (!tab || tab.isDestroyed()) { + return + } + const script = isFullScreen + ? 'document.documentElement.webkitRequestFullScreen()' + : 'document.webkitExitFullscreen()' + tab.executeScriptInTab(config.braveExtensionId, script, {}) + }, + + findInPage (tabId, searchString, caseSensitivity, forward, findNext) { + const tab = webContentsCache.getWebContents(tabId) + if (shouldDebugTabEvents) { + console.log(`tabs.findInPage: ${tabId}, ${searchString}, ${caseSensitivity}, ${forward}, ${findNext}`) + } + if (!tab || tab.isDestroyed()) { + return + } + if (searchString) { + tab.findInPage(searchString, { + matchCase: caseSensitivity, + forward, + findNext + }) + } else { + tab.stopFindInPage('clearSelection') + } + }, + + stopFindInPage (tabId) { + const tab = webContentsCache.getWebContents(tabId) + if (shouldDebugTabEvents) { + console.log(`tabs.stopFindInPage: ${tabId}`) + } + if (!tab || tab.isDestroyed()) { + return + } + tab.stopFindInPage('keepSelection') + }, + goBack: (tabId) => { const tab = webContentsCache.getWebContents(tabId) if (tab && !tab.isDestroyed()) { @@ -1194,7 +1332,6 @@ const api = { tab.forceClose() } }) - state = api.updateTabsStateForWindow(state, windowId) return state }, @@ -1216,7 +1353,6 @@ const api = { tab.forceClose() } }) - state = api.updateTabsStateForWindow(state, windowId) return state }, @@ -1238,7 +1374,6 @@ const api = { tab.forceClose() } }) - state = api.updateTabsStateForWindow(state, windowId) return state }, @@ -1255,7 +1390,6 @@ const api = { tab.forceClose() } }) - state = api.updateTabsStateForWindow(state, windowId) return state }, @@ -1286,39 +1420,62 @@ const api = { if (!tabValue) { return state } - return api.updateTabsStateForWindow(state, tabValue.get('windowId')) + return api.updateTabIndexesForWindow(state, tabValue.get('windowId')) }, - updateTabsStateForWindow: (state, windowId) => { - tabState.getTabsByWindowId(state, windowId).forEach((tabValue) => { - const tabId = tabValue.get('tabId') - const oldTabValue = tabState.getByTabId(state, tabId) + updateTabIndexesForWindow: (state, windowId) => { + const t0 = shouldDebugTabEvents ? process.hrtime() : null + let changesMade = 0 + // make sure all indexes are up to date + const stateTabs = tabState.getTabsByWindowId(state, windowId) + for (const stateTab of stateTabs.values()) { + const tabId = stateTab.get('tabId') const newTabValue = getTabValue(tabId) - - // For now the renderer needs to know about index and pinned changes - // communicate those out here. - if (newTabValue && oldTabValue) { - const changeInfo = {} - const rendererAwareProps = ['index', 'pinned', 'url', 'active'] - rendererAwareProps.forEach((prop) => { - const newPropVal = newTabValue.get(prop) - if (oldTabValue.get(prop) !== newPropVal) { - changeInfo[prop] = newPropVal - } - }) - if (Object.keys(changeInfo).length > 0) { - updateTab(tabId, changeInfo) - } + if (!newTabValue) { + // This is probably the deleted tab + continue } - - if (newTabValue) { - state = tabState.updateTabValue(state, newTabValue, false) + const oldIndex = stateTab.get('index') + const newIndex = newTabValue.get('index') + if (oldIndex !== newIndex) { + if (shouldDebugTabEvents) { + console.log(`Tab [${tabId} Updating state index from ${oldIndex} to ${newIndex}`) + } + state = tabState.updateTabValue(state, Immutable.Map({ tabId, index: newIndex })) + changesMade++ } - }) + } + if (shouldDebugTabEvents) { + const t1 = process.hrtime(t0) + console.info(`updateTabIndexesForWindow took: %ds %dms with ${changesMade} changes`, t1[0], t1[1] / 1000000) + } return state }, + forgetTab: (tabId) => { - webContentsCache.cleanupWebContents(tabId) + const tab = webContentsCache.getWebContents(tabId) + if (!tab) { + // perhaps tab was set to be null, but other cache data still exists + webContentsCache.cleanupWebContents(tabId) + return + } + // Do not remove tab until it is destroyed, as we still need to refer to it, + // even though state has let us know it does not care about the tab anymore + // But, we do this here in case state still needs to refer to tab + // after it is destroyed, for a brief time. + if (tab.isDestroyed()) { + if (shouldDebugTabEvents) { + console.log(`Tab [${tabId}] forgetTab: is already destroyed, cleaning up webContents from cache immediately`) + } + webContentsCache.cleanupWebContents(tabId) + } else { + tab.once('destroyed', function () { + if (shouldDebugTabEvents) { + console.log(`Tab [${tabId}] forgetTab: 'destroyed' emitted, cleaning up webContents from cache`) + } + webContentsCache.cleanupWebContents(tabId) + }) + } } } diff --git a/app/browser/webContentsCache.js b/app/browser/webContentsCache.js index e483ffb3bf6..7e27bd00952 100644 --- a/app/browser/webContentsCache.js +++ b/app/browser/webContentsCache.js @@ -31,11 +31,29 @@ const forgetOpenerForTabId = (tabId) => { } } +const tabIdChanged = (oldTabId, newTabId) => { + // any tabs referencing the old contents Id as the opener, + // should now reference the new contents Id + for (const tabId in currentWebContents) { + const tabData = currentWebContents[tabId] + if (tabData && tabData.openerTabId != null && tabData.openerTabId === oldTabId) { + tabData.openerTabId = newTabId + } + } + // we should also give the replacement tab the opener for the old tab + const newTabData = currentWebContents[newTabId] + const oldTabData = currentWebContents[oldTabId] + if (newTabData && oldTabData && oldTabData.openerTabId != null) { + newTabData.openerTabId = oldTabData.openerTabId + } +} + module.exports = { cleanupWebContents, getWebContents, getOpenerTabId, forgetOpenerForTabId, updateWebContents, + tabIdChanged, currentWebContents } diff --git a/app/browser/windows.js b/app/browser/windows.js index 9fdb4636b59..6009dbd4604 100644 --- a/app/browser/windows.js +++ b/app/browser/windows.js @@ -25,8 +25,9 @@ const browserWindowUtil = require('../common/lib/browserWindowUtil') const windowState = require('../common/state/windowState') const pinnedSitesState = require('../common/state/pinnedSitesState') const {zoomLevel} = require('../common/constants/toolbarUserInterfaceScale') -const { shouldDebugWindowEvents } = require('../cmdLine') +const { shouldDebugWindowEvents, shouldDebugTabEvents, shouldDebugStoreActions, disableBufferWindow, disableDeferredWindowLoad } = require('../cmdLine') const activeTabHistory = require('./activeTabHistory') +const webContentsCache = require('./webContentsCache') const isDarwin = platformUtil.isDarwin() const isWindows = platformUtil.isWindows() @@ -125,7 +126,10 @@ const updatePinnedTabs = (win, appState) => { // tabs are sites our window already has pinned // for each site which should be pinned, find if it's already pinned const statePinnedSitesOrdered = statePinnedSites.sort((a, b) => a.get('order') - b.get('order')) + // pinned sites should always be at the front of the window tab indexes, starting with 0 + let pinnedSiteIndex = -1 for (const site of statePinnedSitesOrdered.values()) { + pinnedSiteIndex++ const existingPinnedTabIdx = pinnedWindowTabs.findIndex(tab => siteMatchesTab(site, tab)) if (existingPinnedTabIdx !== -1) { // if it's already pinned we don't need to consider the tab in further searches @@ -135,6 +139,7 @@ const updatePinnedTabs = (win, appState) => { appActions.createTabRequested({ url: site.get('location'), partitionNumber: site.get('partitionNumber'), + index: pinnedSiteIndex, pinned: true, active: false, windowId @@ -147,11 +152,39 @@ const updatePinnedTabs = (win, appState) => { } } +function refocusFocusedWindow (win) { + if (win && !win.isDestroyed()) { + if (shouldDebugWindowEvents) { + console.log('focusing on window', win.id) + } + win.focus() + } +} + function showDeferredShowWindow (win) { - if (shouldDebugWindowEvents) { - console.log(`Window [${win.id}] showDeferredShowWindow`) + // were we asked to make the window active / foreground? + // note: do not call win.showInactive if there is no other active window, otherwise this window will + // never get an entry in taskbar on Windows + const currentlyFocused = BrowserWindow.getFocusedWindow() + const shouldShowInactive = win.webContents.browserWindowOptions.inactive && currentlyFocused + if (shouldShowInactive) { + // we were asked NOT to show the window active. + // we should maintain focus on the window which already has it + if (shouldDebugWindowEvents) { + console.log('showing deferred window inactive', win.id) + } + win.show() + // Whilst the window will not have focus, it will potentially be + // on top of the window which already had focus, + // so re-focus the focused window. + setImmediate(refocusFocusedWindow.bind(null, currentlyFocused)) + } else { + // we were asked to show the window active + if (shouldDebugWindowEvents) { + console.log('showing deferred window active', win.id) + } + win.show() } - win.show() if (win.__shouldFullscreen) { // this timeout helps with an issue that // when a user is loading from state, and @@ -160,6 +193,9 @@ function showDeferredShowWindow (win) { // spaces because macOS has switched away from the desktop space setTimeout(() => { win.setFullScreen(true) + if (shouldShowInactive) { + setImmediate(refocusFocusedWindow.bind(null, currentlyFocused)) + } }, 100) } else if (win.__shouldMaximize) { win.maximize() @@ -175,8 +211,15 @@ function openFramesInWindow (win, frames, activeFrameKey) { let frameIndex = -1 for (const frame of frames) { frameIndex++ - if (frame.guestInstanceId) { - appActions.newWebContentsAdded(win.id, frame) + if (frame.tabId != null && frame.guestInstanceId != null) { + if (shouldDebugTabEvents) { + console.log('notifyWindowWebContentsAdded: on window create with existing tab', win.id) + } + api.notifyWindowWebContentsAdded(win.id, frame) + const tab = webContentsCache.getWebContents(frame.tabId) + if (tab && !tab.isDestroyed()) { + tab.moveTo(frameIndex, win.id) + } } else { appActions.createTabRequested({ windowId: win.id, @@ -242,6 +285,30 @@ const api = { win.once('close', () => { LocalShortcuts.unregister(win) }) + win.webContents.on('tab-inserted-at', (e, contents, index, active) => { + const tabId = contents.getId() + appActions.tabInsertedToTabStrip(win.id, tabId, index) + if (shouldDebugWindowEvents) { + console.log(`window ${win.id} had ${!active ? 'in' : ''}active tab ${tabId} inserted at index ${index}`) + } + }) + win.webContents.on('tab-detached-at', (e, index, windowId) => { + appActions.tabDetachedFromTabStrip(windowId, index) + if (shouldDebugWindowEvents) { + console.log(`window ${win.id} had tab at removed at index ${index}`) + } + }) + win.webContents.on('tab-strip-empty', () => { + // must wait for pending tabs to be attached to new window before closing + // TODO(petemill): race condition if multiple different tabs are moved at the same time + // ...tab-strip-empty may fire before all of those tabs are inserted to new window + win.webContents.once('detached-tab-new-window', () => { + if (shouldDebugWindowEvents) { + console.log('departing tab made it to new window') + } + api.closeWindow(win.id) + }) + }) win.on('scroll-touch-begin', function (e) { win.webContents.send('scroll-touch-begin') }) @@ -305,6 +372,19 @@ const api = { win.once('closed', () => { appActions.windowClosed(windowId) cleanupWindow(windowId) + // if we have a bufferWindow, the 'window-all-closed' + // event will not fire once the last window is closed, + // so close the buffer window if this is the last closed window + // apart from the buffer window. + // This would mean that the last window to close is the buffer window, but + // that will not get saved to state as the last-closed window which should be restored + // since we won't save state if there are no frames. + if (!platformUtil.isDarwin() && api.getBufferWindow()) { + const remainingWindows = api.getAllRendererWindows() + if (!remainingWindows.length) { + api.closeBufferWindow() + } + } }) win.on('blur', () => { appActions.windowBlurred(windowId) @@ -401,6 +481,18 @@ const api = { }) }, + focus: (windowId) => { + setImmediate(() => { + const win = currentWindows[windowId] + if (win && !win.isDestroyed()) { + if (win.isMinimized()) { + win.restore() + } + win.focus() + } + }) + }, + setFullScreen: (windowId, fullScreen) => { setImmediate(() => { const win = currentWindows[windowId] @@ -444,6 +536,9 @@ const api = { renderedWindows.add(win) setImmediate(() => { if (win && win.__showWhenRendered && !win.isDestroyed() && !win.isVisible()) { + if (shouldDebugWindowEvents) { + console.log('rendered window so showing window') + } // window is hidden by default until we receive 'ready' message, // so show it now showDeferredShowWindow(win) @@ -521,6 +616,12 @@ const api = { }, getOrCreateBufferWindow: function (options = { }) { + if (disableBufferWindow) { + if (shouldDebugWindowEvents) { + console.log(`getOrCreateBufferWindow: buffer window disabled, not creating one.`) + } + return + } // only if we don't have one already let win = api.getBufferWindow() if (!win) { @@ -539,6 +640,9 @@ const api = { }, createWindow: function (windowOptionsIn, parentWindow, maximized, frames, immutableState = Immutable.Map(), hideUntilRendered = true, cb = null) { + if (disableDeferredWindowLoad) { + hideUntilRendered = false + } const defaultOptions = { // hide the window until the window reports that it is rendered show: true, @@ -562,6 +666,16 @@ const api = { defaultOptions, windowOptionsIn ) + // validate activeFrameKey if provided + let activeFrameKey = immutableState.get('activeFrameKey') + if (frames && frames.length && activeFrameKey) { + const keyIsValid = frames.some(frame => frame.key === activeFrameKey) + if (!keyIsValid) { + // make first frame active if invalid key provided + activeFrameKey = frames[0].key + } + immutableState = immutableState.set('activeFrameKey', activeFrameKey) + } // will only hide until rendered if the options specify to show window // so that a caller can control showing the window themselves with the option { show: false } const showWhenRendered = hideUntilRendered && windowOptions.show @@ -650,6 +764,7 @@ const api = { if (shouldDebugWindowEvents) { markWindowCreationTime(win.id) + console.log(`createWindow: new BrowserWindow with ID ${win.id} created with options`, windowOptions) } // TODO: pass UUID publicEvents.emit('new-window-state', win.id, immutableState) @@ -665,6 +780,9 @@ const api = { // in those cases, we want to still show it, so that the user can find the error message setTimeout(() => { if (win && !win.isDestroyed() && !win.isVisible()) { + if (shouldDebugWindowEvents) { + console.log('deferred-show window passed timeout, so showing deferred') + } showDeferredShowWindow(win) } }, config.windows.timeoutToShowWindowMs) @@ -689,7 +807,11 @@ const api = { const position = win.getPosition() const size = win.getSize() - const windowState = (immutableState && immutableState.toJS()) || undefined + + const windowState = (immutableState && immutableState.toJS()) || { } + windowState.debugTabEvents = shouldDebugTabEvents + windowState.debugStoreActions = shouldDebugStoreActions + const mem = muon.shared_memory.create({ windowValue: { disposition: windowOptions.disposition, @@ -701,13 +823,10 @@ const api = { width: size[0] }, appState: appStore.getLastEmittedState().toJS(), - windowState, - // TODO: dispatch frame create action on appStore, as this is what the window does anyway - // ...and do it after the window has rendered - frames + windowState }) - e.sender.sendShared(messages.INITIALIZE_WINDOW, mem) + openFramesInWindow(win, frames, windowState && windowState.activeFrameKey) // TODO: remove callback, use store action, returning a new window UUID from this function if (cb) { cb() @@ -720,21 +839,51 @@ const api = { return currentWindows[windowId] }, - getActiveWindowId: () => { - if (BrowserWindow.getFocusedWindow()) { - return BrowserWindow.getFocusedWindow().id + getActiveWindow: () => { + const focusedWindow = BrowserWindow.getFocusedWindow() + const allOpenWindows = api.getAllRendererWindows() + if (allOpenWindows.includes(focusedWindow)) { + return focusedWindow + } + // handle no active window, but do have open windows + if (allOpenWindows && allOpenWindows.length) { + // use first window + return allOpenWindows[0] } - return windowState.WINDOW_ID_NONE + // no open windows + return null + }, + + getActiveWindowId: () => { + const activeWindow = api.getActiveWindow() + return activeWindow ? activeWindow.id : windowState.WINDOW_ID_NONE }, /** * Provides an array of all Browser Windows which are actual * main windows (not background workers), and are not destroyed */ - getAllRendererWindows: () => { + getAllRendererWindows: (includingBufferWindow = false) => { return Object.keys(currentWindows) .map(key => currentWindows[key]) - .filter(win => win && !win.isDestroyed()) + .filter(win => + win && + !win.isDestroyed() && + (includingBufferWindow || win !== api.getBufferWindow()) + ) + }, + + notifyWindowWebContentsAdded (windowId, frame, tabValue) { + const win = api.getWindow(windowId) + if (!win || win.isDestroyed()) { + console.error(`notifyWindowWebContentsAdded, no window for id ${windowId}`) + return + } + if (!win.webContents || win.webContents.isDestroyed()) { + console.error(`notifyWindowWebContentsAdded, no window webContents for id ${windowId}`) + return + } + win.webContents.send('new-web-contents-added', frame, tabValue) }, on: (...args) => publicEvents.on(...args), diff --git a/app/cmdLine.js b/app/cmdLine.js index 3566a70b78c..cbd4dbf667a 100644 --- a/app/cmdLine.js +++ b/app/cmdLine.js @@ -4,11 +4,9 @@ 'use strict' -const Immutable = require('immutable') const electron = require('electron') const app = electron.app const messages = require('../js/constants/messages') -const BrowserWindow = electron.BrowserWindow const appActions = require('../js/actions/appActions') const urlParse = require('./common/urlParse') const {fileUrl} = require('../js/lib/appUrlUtil') @@ -17,41 +15,31 @@ const fs = require('fs') const path = require('path') const isDarwin = process.platform === 'darwin' -const promoCodeFilenameRegex = /-([a-zA-Z\d]{3}\d{3})\s?(?:\(\d+\))?$/g const debugTabEventsFlagName = '--debug-tab-events' let appInitialized = false let newWindowURL const debugWindowEventsFlagName = '--debug-window-events' +const disableBufferWindowFlagName = '--disable-buffer-window' +const disableDeferredWindowLoadFlagName = '--show-windows-immediately' +const debugStoreActionsFlagName = '--debug-store-actions' const focusOrOpenWindow = function (url) { // don't try to do anything if the app hasn't been initialized if (!appInitialized) { return false } - - let win = BrowserWindow.getFocusedWindow() - if (!win) { - win = BrowserWindow.getActiveWindow() || BrowserWindow.getAllWindows()[0] - if (win) { - if (win.isMinimized()) { - win.restore() - } - win.focus() + // create a tab and focus the tab's window + if (url) { + const tabCreateProperties = { + url } + // request to create tab in a new or existing window, and focus the window + appActions.createTabRequested(tabCreateProperties, false, false, true) + return true } - - if (!win) { - appActions.newWindow(Immutable.fromJS({ - location: url - })) - } else if (url) { - appActions.createTabRequested({ - url, - windowId: win.id - }) - } - + // focus the active window, or create a new one with default tabs + appActions.focusOrCreateWindow() return true } @@ -164,13 +152,17 @@ const api = module.exports = { // parse promo code from installer path // first, get filename const fileName = path.win32.parse(installerPath).name + const promoCodeFilenameRegex = /-(([a-zA-Z\d]{3}\d{3})|([a-zA-Z]{1,}-[a-zA-Z]{1,}))\s?(?:\(\d+\))?$/g const matches = promoCodeFilenameRegex.exec(fileName) - if (matches && matches.length === 2) { + if (matches && matches.length > 1) { return matches[1] } return null }, shouldDebugTabEvents: process.argv.includes(debugTabEventsFlagName), - shouldDebugWindowEvents: process.argv.includes(debugWindowEventsFlagName) + shouldDebugWindowEvents: process.argv.includes(debugWindowEventsFlagName), + disableBufferWindow: process.env.NODE_ENV === 'test' || process.argv.includes(disableBufferWindowFlagName), + disableDeferredWindowLoad: process.argv.includes(disableDeferredWindowLoadFlagName), + shouldDebugStoreActions: process.argv.includes(debugStoreActionsFlagName) } diff --git a/app/common/actions/tabActions.js b/app/common/actions/tabActions.js index 8f336450cad..2bddff5098e 100644 --- a/app/common/actions/tabActions.js +++ b/app/common/actions/tabActions.js @@ -35,6 +35,36 @@ const tabActions = { windowId } }) + }, + + findInPageRequest (tabId, searchString, caseSensitivity, forward, findNext) { + dispatchAction(tabActionConstants.FIND_IN_PAGE_REQUEST, { + tabId, + searchString, + caseSensitivity, + forward, + findNext + }) + }, + + stopFindInPageRequest (tabId) { + dispatchAction(tabActionConstants.STOP_FIND_IN_PAGE_REQUEST, { + tabId + }) + }, + + zoomChanged (tabId, zoomPercent) { + dispatchAction(tabActionConstants.ZOOM_CHANGED, { + tabId, + zoomPercent + }) + }, + + setContentsError (tabId, errorDetails) { + dispatchAction(tabActionConstants.SET_CONTENTS_ERROR, { + tabId, + errorDetails + }) } } diff --git a/app/common/cache/ledgerVideoCache.js b/app/common/cache/ledgerVideoCache.js index c7f0ae3fcb1..adab8a5f561 100644 --- a/app/common/cache/ledgerVideoCache.js +++ b/app/common/cache/ledgerVideoCache.js @@ -33,7 +33,24 @@ const setCacheByVideoId = (state, key, data) => { return state.setIn(['cache', 'ledgerVideos', key], data) } +const mergeCacheByVideoId = (state, key, data) => { + state = validateState(state) + + if (key == null || data == null) { + return state + } + + data = makeImmutable(data) + + if (data.isEmpty()) { + return state + } + + return state.mergeIn(['cache', 'ledgerVideos', key], data) +} + module.exports = { getDataByVideoId, - setCacheByVideoId + setCacheByVideoId, + mergeCacheByVideoId } diff --git a/app/common/commonMenu.js b/app/common/commonMenu.js index bf28b204621..ede60939542 100644 --- a/app/common/commonMenu.js +++ b/app/common/commonMenu.js @@ -13,27 +13,22 @@ const getSetting = require('../../js/settings').getSetting const communityURL = 'https://community.brave.com/' const isDarwin = process.platform === 'darwin' const electron = require('electron') - -let BrowserWindow -if (process.type === 'browser') { - BrowserWindow = electron.BrowserWindow -} else { - BrowserWindow = electron.remote.BrowserWindow -} +const menuUtil = require('./lib/menuUtil') const ensureAtLeastOneWindow = (frameOpts) => { - if (process.type === 'browser') { - if (BrowserWindow.getAllWindows().length === 0) { - appActions.newWindow(frameOpts || {}) - return - } - } - - if (!frameOpts) { + // Handle no new tab requested, but need a window + // and possibly there is no window. + if (!frameOpts && process.type === 'browser') { + // focus active window, or create a new one if there are none + appActions.focusOrCreateWindow() return } - - appActions.createTabRequested(frameOpts) + // If this action is dispatched from a renderer window (Windows OS), + // it will create the tab in the current window since the action originates from it. + // If it was dispatched by the browser (macOS / Linux), + // then it will create the tab in the active window + // or a new window if there is no active window. + appActions.createTabRequested(frameOpts, false, false, true) } /** @@ -142,7 +137,7 @@ module.exports.printMenuItem = () => { } module.exports.simpleShareActiveTabMenuItem = (l10nId, type, accelerator) => { - const siteName = type.charAt(0).toUpperCase() + type.slice(1) + const siteName = menuUtil.extractSiteName(type) return { label: locale.translation(l10nId, {siteName: siteName}), diff --git a/app/common/constants/ledgerMediaProviders.js b/app/common/constants/ledgerMediaProviders.js index 84ef2d1af80..b8d241eb3d2 100644 --- a/app/common/constants/ledgerMediaProviders.js +++ b/app/common/constants/ledgerMediaProviders.js @@ -3,7 +3,8 @@ * You can obtain one at http://mozilla.org/MPL/2.0/. */ const providers = { - YOUTUBE: 'youtube' + YOUTUBE: 'youtube', + TWITCH: 'twitch' } module.exports = providers diff --git a/app/common/constants/ledgerStatuses.js b/app/common/constants/ledgerStatuses.js new file mode 100644 index 00000000000..216ca1f966d --- /dev/null +++ b/app/common/constants/ledgerStatuses.js @@ -0,0 +1,12 @@ +/* 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 statuses = { + CORRUPTED_SEED: 'corruptedSeed', + IN_PROGRESS: 'contributionInProgress', + SERVER_PROBLEM: 'serverProblem', + FUZZING: 'fuzzing' +} + +module.exports = statuses diff --git a/app/common/constants/promotionStatuses.js b/app/common/constants/promotionStatuses.js new file mode 100644 index 00000000000..767f42e4834 --- /dev/null +++ b/app/common/constants/promotionStatuses.js @@ -0,0 +1,12 @@ +/* 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 statuses = { + GENERAL_ERROR: 'generalError', + PROMO_EXPIRED: 'expiredError', + CAPTCHA_CHECK: 'captchaCheck', + CAPTCHA_ERROR: 'captchaError' +} + +module.exports = statuses diff --git a/app/common/constants/tabAction.js b/app/common/constants/tabAction.js index 739bb1680e2..23898d601eb 100644 --- a/app/common/constants/tabAction.js +++ b/app/common/constants/tabAction.js @@ -5,5 +5,9 @@ module.exports = { RELOAD: 'tabActionsReload', FINISH_NAVIGATION: 'tabActionsDidFinishNavigation', - START_NAVIGATION: 'tabActionsDidStartNavigation' + START_NAVIGATION: 'tabActionsDidStartNavigation', + FIND_IN_PAGE_REQUEST: 'tabActionsFindInPageRequest', + STOP_FIND_IN_PAGE_REQUEST: 'tabActionsStopFindInPageRequest', + ZOOM_CHANGED: 'tabActionsZoomChanged', + SET_CONTENTS_ERROR: 'tabActionsSetContentsError' } diff --git a/app/common/constants/twitchEvents.js b/app/common/constants/twitchEvents.js new file mode 100644 index 00000000000..cb069eef190 --- /dev/null +++ b/app/common/constants/twitchEvents.js @@ -0,0 +1,16 @@ +/* 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 events = { + BUFFER_EMPTY: 'buffer-empty', + BUFFER_REFILL: 'buffer-refill', + MINUTE_WATCHED: 'minute-watched', + PLAY_PAUSE: 'video_pause', + SEEK: 'player_click_vod_seek', + START: 'video-play', + END: 'video_end', + VIDEO_ERROR: 'video_error' +} + +module.exports = events diff --git a/app/common/lib/bookmarkToolbarUtil.js b/app/common/lib/bookmarkToolbarUtil.js deleted file mode 100644 index 872338cd6e2..00000000000 --- a/app/common/lib/bookmarkToolbarUtil.js +++ /dev/null @@ -1,93 +0,0 @@ -/* 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 Immutable = require('immutable') - -// Utils -const bookmarkUtil = require('../lib/bookmarkUtil') -const bookmarkFoldersUtil = require('../lib/bookmarkFoldersUtil') - -// Styles -const globalStyles = require('../../renderer/components/styles/global') -const {iconSize} = require('../../../js/constants/config') -const maxWidth = parseInt(globalStyles.spacing.bookmarksItemMaxWidth, 10) -const padding = parseInt(globalStyles.spacing.bookmarksItemPadding, 10) * 2 -const itemMargin = parseInt(globalStyles.spacing.bookmarksItemMargin, 10) -const toolbarPadding = parseInt(globalStyles.spacing.bookmarksToolbarPadding) -const overflowButtonWidth = parseInt(globalStyles.spacing.bookmarksToolbarOverflowButtonWidth, 10) -const chevronMargin = parseInt(globalStyles.spacing.bookmarksItemChevronMargin) -const chevronFontSize = parseInt(globalStyles.spacing.bookmarksItemChevronFontSize) -const chevronWidth = chevronMargin + chevronFontSize - -const getBookmarkKeys = (width, bookmarks) => { - if (bookmarks == null) { - return { - toolbar: Immutable.List(), - other: Immutable.List() - } - } - - let widthAccountedFor = 0 - - const onlyText = bookmarkUtil.showOnlyText() - const textAndFavicon = bookmarkUtil.showTextAndFavicon() - const onlyFavicon = bookmarkUtil.showOnlyFavicon() - - // No margin for show only fav icons - const margin = onlyFavicon ? 0 : (itemMargin * 2) - const maximumBookmarksToolbarWidth = width - overflowButtonWidth - - widthAccountedFor += toolbarPadding - - // Loop through until we fill up the entire bookmark toolbar width - let i = 0 - for (let item of bookmarks) { - let iconWidth - const isFolder = bookmarkFoldersUtil.isFolder(item) - - if (onlyText) { - iconWidth = 0 - } else if (textAndFavicon || isFolder) { - iconWidth = iconSize + itemMargin - } else if (onlyFavicon) { - iconWidth = iconSize - } - - let extraWidth = 0 - - if (onlyText) { - extraWidth = padding + item.get('width') - } else if (textAndFavicon) { - extraWidth = padding + iconWidth + item.get('width') - } else if (onlyFavicon) { - extraWidth = padding + iconWidth - - if (isFolder) { - extraWidth += item.get('width') - } - } - - if (isFolder) { - extraWidth += chevronWidth - } - - extraWidth = Math.min(extraWidth, maxWidth) - widthAccountedFor += extraWidth + margin - - if (widthAccountedFor >= maximumBookmarksToolbarWidth) { - break - } - - i++ - } - - return { - toolbar: bookmarks.take(i).map((item) => item.get('key')).toList(), - other: bookmarks.skip(i).take(100).map((item) => item.get('key')).toList() - } -} - -module.exports = { - getBookmarkKeys -} diff --git a/app/common/lib/bookmarkUtil.js b/app/common/lib/bookmarkUtil.js index afd20aa9f1d..3bec9133119 100644 --- a/app/common/lib/bookmarkUtil.js +++ b/app/common/lib/bookmarkUtil.js @@ -22,6 +22,9 @@ const {getSetting} = require('../../../js/settings') const UrlUtil = require('../../../js/lib/urlutil') const {makeImmutable} = require('../state/immutableUtil') +// Actions +const appActions = require('../../../js/actions/appActions') + const bookmarkHangerHeading = (editMode, isAdded) => { if (isAdded) { return 'bookmarkAdded' @@ -36,23 +39,10 @@ const isBookmarkNameValid = (location) => { return location != null && location.trim().length > 0 } -const showOnlyText = () => { - const btbMode = getSetting(settings.BOOKMARKS_TOOLBAR_MODE) - return btbMode === bookmarksToolbarMode.TEXT_ONLY -} +const getBookmarksToolbarMode = (appState) => + getSetting(settings.BOOKMARKS_TOOLBAR_MODE, appState && appState.get('settings')) -const showTextAndFavicon = () => { - const btbMode = getSetting(settings.BOOKMARKS_TOOLBAR_MODE) - return btbMode === bookmarksToolbarMode.TEXT_AND_FAVICONS -} - -const showOnlyFavicon = () => { - const btbMode = getSetting(settings.BOOKMARKS_TOOLBAR_MODE) - return btbMode === bookmarksToolbarMode.FAVICONS_ONLY -} - -const showFavicon = () => { - const btbMode = getSetting(settings.BOOKMARKS_TOOLBAR_MODE) +const showFavicon = (appState, btbMode = getBookmarksToolbarMode(appState)) => { return btbMode === bookmarksToolbarMode.TEXT_AND_FAVICONS || btbMode === bookmarksToolbarMode.FAVICONS_ONLY } @@ -198,8 +188,7 @@ const buildBookmark = (state, bookmarkDetail) => { themeColor: dataItem.get('themeColor'), type: siteTags.BOOKMARK, key: key, - skipSync: bookmarkDetail.get('skipSync', null), - width: 0 + skipSync: bookmarkDetail.get('skipSync', null) }) } @@ -220,10 +209,16 @@ const buildEditBookmark = (oldBookmark, bookmarkDetail) => { return newBookmark.set('key', newKey) } +const closeToolbarIfEmpty = (state) => { + const bookmarkBarItemCount = bookmarksState.getBookmarksWithFolders(state, 0).size + if (bookmarkBarItemCount === 0 && getSetting(settings.SHOW_BOOKMARKS_TOOLBAR, state.get('settings'))) { + appActions.changeSetting(settings.SHOW_BOOKMARKS_TOOLBAR, false) + } +} + module.exports = { bookmarkHangerHeading, isBookmarkNameValid, - showOnlyFavicon, showFavicon, getDNDBookmarkData, getDetailFromFrame, @@ -233,8 +228,8 @@ module.exports = { updateTabBookmarked, updateActiveTabBookmarked, getKey, - showOnlyText, - showTextAndFavicon, + getBookmarksToolbarMode, buildBookmark, - buildEditBookmark + buildEditBookmark, + closeToolbarIfEmpty } diff --git a/app/common/lib/historyUtil.js b/app/common/lib/historyUtil.js index 995cf35250d..dfcf5597a7b 100644 --- a/app/common/lib/historyUtil.js +++ b/app/common/lib/historyUtil.js @@ -160,8 +160,8 @@ const mergeSiteDetails = (oldDetail, newDetail) => { } const getDetailFromFrame = (frame) => { - if (frame == null) { - return Immutable.Map() + if (frame == null || !frame.has('location')) { + return null } return makeImmutable({ diff --git a/app/common/lib/ledgerUtil.js b/app/common/lib/ledgerUtil.js index 6b02863b09a..34cf47782e0 100644 --- a/app/common/lib/ledgerUtil.js +++ b/app/common/lib/ledgerUtil.js @@ -9,20 +9,26 @@ const format = require('date-fns/format') const distanceInWordsToNow = require('date-fns/distance_in_words_to_now') const BigNumber = require('bignumber.js') const queryString = require('querystring') +const tldjs = require('tldjs') // State const siteSettingsState = require('../state/siteSettingsState') const ledgerState = require('../state/ledgerState') +const ledgerVideoCache = require('../cache/ledgerVideoCache') // Constants const settings = require('../../../js/constants/settings') const ledgerMediaProviders = require('../constants/ledgerMediaProviders') +const twitchEvents = require('../constants/twitchEvents') +const ledgerStatuses = require('../constants/ledgerStatuses') // Utils const {responseHasContent} = require('./httpUtil') const urlUtil = require('../../../js/lib/urlutil') const getSetting = require('../../../js/settings').getSetting const urlParse = require('../urlParse') +const {makeImmutable} = require('../state/immutableUtil') + /** * Is page an actual page being viewed by the user? (not an error page, etc) * If the page is invalid, we don't want to collect usage info. @@ -59,22 +65,22 @@ const batToCurrencyString = (bat, ledgerData) => { return `${converted} ${currency}` } -const formatCurrentBalance = (ledgerData) => { +const formatCurrentBalance = (ledgerData, amount, showAlt = true) => { let currency = 'USD' let balance = 0 let converted = 0 let hasRate = false if (ledgerData != null) { - balance = Number(ledgerData.get('balance') || 0) + balance = Number(amount || 0) converted = Number.parseFloat(ledgerData.get('converted')) || 0 - hasRate = ledgerData.has('currentRate') && ledgerData.hasIn(['rates', 'BTC']) + hasRate = showAlt ? ledgerData.has('currentRate') && ledgerData.hasIn(['rates', 'BTC']) : false } balance = balance.toFixed(2) if (converted > 0 && converted < 0.01) { - converted = '<.01' + converted = '< 0.01' } else { converted = converted.toFixed(2) } @@ -90,9 +96,18 @@ const formattedDateFromTimestamp = (timestamp, dateFormat) => { return format(new Date(timestamp), dateFormat, {locale: navigator.language}) } -const walletStatus = (ledgerData) => { +const walletStatus = (ledgerData, settings) => { let status = {} + switch (ledgerData.get('status')) { + case ledgerStatuses.FUZZING: + { + return { + id: 'ledgerFuzzed' + } + } + } + if (ledgerData == null) { return { id: 'createWalletStatus' @@ -105,7 +120,7 @@ const walletStatus = (ledgerData) => { const transactions = ledgerData.get('transactions') const pendingFunds = Number(ledgerData.get('unconfirmed') || 0) const balance = Number(ledgerData.get('balance') || 0) - const minBalance = ledgerState.getContributionAmount(null, ledgerData.get('contributionAmount')) + const minBalance = ledgerState.getContributionAmount(null, ledgerData.get('contributionAmount'), settings) if (pendingFunds + balance < minBalance) { status.id = 'insufficientFundsStatus' @@ -129,6 +144,24 @@ const walletStatus = (ledgerData) => { return status } +const shouldShowMenuOption = (state, location) => { + if (location == null) { + return false + } + + const publisherKey = tldjs.tldExists(location) && tldjs.getDomain(location) + const validUrl = urlUtil.isURL(location) && urlParse(location).protocol !== undefined + + if (!publisherKey || !validUrl) { + return false + } + + const isVisible = visibleP(state, publisherKey) + const isBlocked = blockedP(state, publisherKey) + + return (!isVisible && !isBlocked) +} + // TODO rename function const blockedP = (state, publisherKey) => { const pattern = urlUtil.getHostPattern(publisherKey) @@ -221,6 +254,21 @@ const getMediaId = (data, type) => { id = data.docid break } + case ledgerMediaProviders.TWITCH: + { + if ( + Object.values(twitchEvents).includes(data.event) && + data.properties + ) { + id = data.properties.channel + let vod = data.properties.vod + + if (vod) { + vod = vod.replace('v', '') + id += `_vod_${vod}` + } + } + } } return id @@ -234,23 +282,76 @@ const getMediaKey = (id, type) => { return `${type.toLowerCase()}_${id}` } -const getMediaData = (xhr, type) => { +const getMediaData = (xhr, type, details) => { let result = null if (xhr == null || type == null) { return result } + const parsedUrl = urlParse(xhr) + const query = parsedUrl && parsedUrl.query + + if (!parsedUrl) { + return null + } + switch (type) { case ledgerMediaProviders.YOUTUBE: { - const parsedUrl = urlParse(xhr) - let query = null + if (!query) { + return null + } + + result = queryString.parse(query) + break + } + case ledgerMediaProviders.TWITCH: + { + const uploadData = details.get('uploadData') || Immutable.List() + + if (uploadData.size === 0) { + result = null + break + } + + let params = uploadData.reduce((old, item) => { + const bytes = item.get('bytes') + let data = '' + if (bytes) { + data = Buffer.from(bytes).toString('utf8') || '' + } + return old + data + }, '') + + if (!params || params.length === 0) { + result = null + break + } + + const paramQuery = queryString.parse(params) + + if (!paramQuery || !paramQuery.data) { + result = null + break + } - if (parsedUrl && parsedUrl.query) { - query = queryString.parse(parsedUrl.query) + let obj = Buffer.from(paramQuery.data, 'base64').toString('utf8') + if (obj == null) { + result = null + break } - result = query + + let parsed + try { + parsed = JSON.parse(obj) + } catch (error) { + result = null + console.error(error.toString(), obj) + break + } + + result = parsed break } } @@ -258,18 +359,198 @@ const getMediaData = (xhr, type) => { return result } -const getMediaDuration = (data, type) => { +const getMediaDuration = (state, data, mediaKey, type) => { let duration = 0 + + if (data == null) { + return duration + } + switch (type) { - case ledgerMediaProviders.YOUTUBE: { - duration = getYouTubeDuration(data) - break - } + case ledgerMediaProviders.YOUTUBE: + { + duration = getYouTubeDuration(data) + break + } + case ledgerMediaProviders.TWITCH: + { + duration = getTwitchDuration(state, data, mediaKey) + break + } } return duration } +const generateMediaCacheData = (state, parsed, type, mediaKey) => { + let data = Immutable.Map() + + if (parsed == null) { + return data + } + + switch (type) { + case ledgerMediaProviders.TWITCH: + { + data = generateTwitchCacheData(state, parsed, mediaKey) + break + } + } + + return data +} + +const generateTwitchCacheData = (state, parsed, mediaKey) => { + if (parsed == null) { + return Immutable.Map() + } + + const statusConst = { + playing: 'playing', + paused: 'paused' + } + + const previousData = ledgerVideoCache.getDataByVideoId(state, mediaKey) + let status = statusConst.playing + + if ( + ( + parsed.event === twitchEvents.PLAY_PAUSE && + previousData.get('event') !== twitchEvents.PLAY_PAUSE + ) || // user clicked pause (we need to exclude seeking while paused) + ( + parsed.event === twitchEvents.PLAY_PAUSE && + previousData.get('event') === twitchEvents.PLAY_PAUSE && + previousData.get('status') === statusConst.playing + ) || // user clicked pause as soon as he clicked played + ( + parsed.event === twitchEvents.SEEK && + previousData.get('status') === statusConst.paused + ) // seeking video while it is paused + ) { + status = statusConst.paused + } + + // User pauses a video, then seek it and play it again + if ( + parsed.event === twitchEvents.PLAY_PAUSE && + previousData.get('event') === twitchEvents.SEEK && + previousData.get('status') === statusConst.paused + ) { + status = statusConst.playing + } + + if (parsed.properties) { + return Immutable.fromJS({ + event: parsed.event, + time: parsed.properties.time, + status + }) + } + + return Immutable.fromJS({ + event: parsed.event, + status + }) +} + +const getDefaultMediaFavicon = (providerName) => { + let image = null + + if (!providerName) { + return image + } + + providerName = providerName.toLowerCase() + + switch (providerName) { + case ledgerMediaProviders.YOUTUBE: + { + image = require('../../../img/mediaProviders/youtube.png') + break + } + case ledgerMediaProviders.TWITCH: + { + image = require('../../../img/mediaProviders/twitch.svg') + break + } + } + + return image +} + +const getTwitchDuration = (state, data, mediaKey) => { + if (data == null || mediaKey == null || !data.properties) { + return 0 + } + + const previousData = ledgerVideoCache.getDataByVideoId(state, mediaKey) + + // remove duplicate events + if ( + previousData.get('event') === data.event && + previousData.get('time') === data.properties.time + ) { + return null + } + + const oldEvent = previousData.get('event') + const twitchMinimumSeconds = 10 + + if (data.event === twitchEvents.START && oldEvent === twitchEvents.START) { + return 0 + } + + if (data.event === twitchEvents.START) { + return twitchMinimumSeconds * milliseconds.second + } + + let time = 0 + const currentTime = parseFloat(data.properties.time) + const oldTime = parseFloat(previousData.get('time')) + const currentEvent = data.event + + if (oldEvent === twitchEvents.START) { + // From video play event to x event + time = currentTime - oldTime - twitchMinimumSeconds + } else if ( + currentEvent === twitchEvents.MINUTE_WATCHED || // Minute watched + currentEvent === twitchEvents.BUFFER_EMPTY || // Run out of buffer + currentEvent === twitchEvents.VIDEO_ERROR || // Video has some problems + currentEvent === twitchEvents.END || // Video ended + (currentEvent === twitchEvents.SEEK && previousData.get('status') !== 'paused') || // Vod seek + ( + currentEvent === twitchEvents.PLAY_PAUSE && + ( + ( + oldEvent !== twitchEvents.PLAY_PAUSE && + oldEvent !== twitchEvents.SEEK + ) || + previousData.get('status') === 'playing' + ) + ) // User paused a video + ) { + time = currentTime - oldTime + } + + if (isNaN(time)) { + return 0 + } + + if (time < 0) { + return 0 + } + + if (time > 120) { + time = 120 // 2 minutes + } + + // we get seconds back, so we need to convert it into ms + time = time * milliseconds.second + + return time +} + const getYouTubeDuration = (data) => { let time = 0 @@ -294,7 +575,7 @@ const getYouTubeDuration = (data) => { return parseInt(time) } -const getMediaProvider = (url) => { +const getMediaProvider = (url, firstPartyUrl, referrer) => { let provider = null if (url == null) { @@ -303,16 +584,77 @@ const getMediaProvider = (url) => { // Youtube if (url.startsWith('https://www.youtube.com/api/stats/watchtime?')) { - provider = ledgerMediaProviders.YOUTUBE + return ledgerMediaProviders.YOUTUBE + } + + // Twitch + if ( + ( + (firstPartyUrl && firstPartyUrl.startsWith('https://www.twitch.tv/')) || + (referrer && referrer.startsWith('https://player.twitch.tv/')) + ) && + ( + url.includes('.ttvnw.net/v1/segment/') || + url.includes('https://ttvnw.net/v1/segment/') + ) + ) { + return ledgerMediaProviders.TWITCH } return provider } +const hasRequiredVisits = (state, publisherKey) => { + if (!publisherKey) { + return false + } + + state = makeImmutable(state) || Immutable.Map() + + const minimumVisits = parseInt(getSetting(settings.PAYMENTS_MINIMUM_VISITS)) + + if (minimumVisits === 1) { + return true + } + + const publisher = ledgerState.getPublisher(state, publisherKey) + const publisherVisits = publisher.get('visits') + + if (typeof publisherVisits !== 'number') { + return minimumVisits === 1 + } + + const visitDifference = minimumVisits - publisherVisits + + return (visitDifference === 1) +} + +const getRemainingRequiredTime = (state, publisherKey) => { + state = makeImmutable(state) || Immutable.Map() + const minimumVisitTime = parseInt(getSetting(settings.PAYMENTS_MINIMUM_VISIT_TIME)) + + if (!publisherKey) { + return minimumVisitTime + } + + const publisher = ledgerState.getPublisher(state, publisherKey) + const publisherDuration = publisher.get('duration') + + if ( + typeof publisherDuration !== 'number' || + publisherDuration >= minimumVisitTime + ) { + return minimumVisitTime + } + + return (minimumVisitTime - publisherDuration) +} + const defaultMonthlyAmounts = Immutable.List([5.0, 7.5, 10.0, 17.5, 25.0, 50.0, 75.0, 100.0]) const milliseconds = { year: 365 * 24 * 60 * 60 * 1000, + month: (365 * 24 * 60 * 60 * 1000) / 12, week: 7 * 24 * 60 * 60 * 1000, day: 24 * 60 * 60 * 1000, hour: 60 * 60 * 1000, @@ -339,14 +681,21 @@ const getMethods = () => { getMediaData, getMediaKey, milliseconds, - defaultMonthlyAmounts + defaultMonthlyAmounts, + getDefaultMediaFavicon, + generateMediaCacheData, + shouldShowMenuOption, + hasRequiredVisits, + getRemainingRequiredTime } let privateMethods = {} if (process.env.NODE_ENV === 'test') { privateMethods = { - getYouTubeDuration + getYouTubeDuration, + getTwitchDuration, + generateTwitchCacheData } } diff --git a/app/common/lib/menuUtil.js b/app/common/lib/menuUtil.js index 508b28256e0..894fd460bae 100644 --- a/app/common/lib/menuUtil.js +++ b/app/common/lib/menuUtil.js @@ -67,16 +67,20 @@ const getTemplateItem = (template, label) => { return null } +module.exports.extractSiteName = (type) => { + return type.charAt(0).toUpperCase() + type.slice(1) +} + /** - * Search a menu template and update the checked status + * Searches a menu template and updates a passed item key * * @return the new template OR null if no change was made (no update needed) */ -module.exports.setTemplateItemChecked = (template, label, checked) => { +module.exports.setTemplateItemAttribute = (template, label, key, value) => { const menu = template.toJS() const menuItem = getTemplateItem(menu, label) - if (menuItem.checked !== checked) { - menuItem.checked = checked + if (menuItem[key] !== value) { + menuItem[key] = value return makeImmutable(menu) } return null @@ -129,6 +133,15 @@ module.exports.createBookmarkTemplateItems = (state) => { return createBookmarkTemplateItems(state) } +/** + * Used to create bookmarks and bookmark folder entries for "Other Bookamrks" in the "Bookmarks" menu + * + * @param state The application state + */ +module.exports.createOtherBookmarkTemplateItems = (state) => { + return createBookmarkTemplateItems(state, -1) +} + /** * @param {string} key within closedFrames, i.e. a URL * @return {string} diff --git a/app/common/lib/publisherUtil.js b/app/common/lib/publisherUtil.js index 89d1c503175..45b5a70c171 100644 --- a/app/common/lib/publisherUtil.js +++ b/app/common/lib/publisherUtil.js @@ -8,17 +8,17 @@ const settings = require('../../../js/constants/settings') // Utils const ledgerUtil = require('./ledgerUtil') const {getSetting} = require('../../../js/settings') -const {isHttpOrHttps} = require('../../../js/lib/urlutil') +const {isHttpOrHttps, getUrlFromPDFUrl} = require('../../../js/lib/urlutil') const {isSourceAboutUrl} = require('../../../js/lib/appUrlUtil') -const publisherState = { +const publisherUtil = { shouldShowAddPublisherButton: (state, location, publisherKey) => { return location && !isSourceAboutUrl(location) && getSetting(settings.PAYMENTS_ENABLED) && - isHttpOrHttps(location) && + isHttpOrHttps(getUrlFromPDFUrl(location)) && !ledgerUtil.blockedP(state, publisherKey) } } -module.exports = publisherState +module.exports = publisherUtil diff --git a/app/common/lib/siteSuggestions.js b/app/common/lib/siteSuggestions.js index 9cfc0def82c..567ce4a70c0 100644 --- a/app/common/lib/siteSuggestions.js +++ b/app/common/lib/siteSuggestions.js @@ -102,8 +102,9 @@ const tokenizeInput = (data) => { } } - if (url && isUrl(url)) { - const parsedUrl = urlParse(url.toLowerCase()) + const parsedUrl = typeof url === 'string' && isUrl(url) && urlParse(url.toLowerCase()) + + if (parsedUrl && (parsedUrl.hash || parsedUrl.host || parsedUrl.pathname || parsedUrl.query || parsedUrl.protocol)) { if (parsedUrl.hash) { parts.push(parsedUrl.hash.slice(1)) } diff --git a/app/common/lib/suggestion.js b/app/common/lib/suggestion.js index e89a0e6cc9c..cd4c616e93b 100644 --- a/app/common/lib/suggestion.js +++ b/app/common/lib/suggestion.js @@ -124,9 +124,8 @@ const isParsedUrlSimpleDomainNameValue = (parsed) => { */ const normalizeLocation = (location) => { if (typeof location === 'string') { - location = location.replace(/www\./, '') - location = location.replace(/^http:\/\//, '') - location = location.replace(/^https:\/\//, '') + // remove http://, https:// and www. from beginning of location string + location = location.replace(/^(https?:\/\/)?(www\.)?/, '') } return location } @@ -250,8 +249,8 @@ const getSortByDomainForSites = (userInputLower, userInputHost) => { // what the user is entering as the host and the host is null. let host1 = s1.get('parsedUrl').host || s1.get('parsedUrl').pathname || s1.get('location') || '' let host2 = s2.get('parsedUrl').host || s2.get('parsedUrl').pathname || s2.get('location') || '' - host1 = host1.replace('www.', '') - host2 = host2.replace('www.', '') + host1 = normalizeLocation(host1) + host2 = normalizeLocation(host2) let pos1 = host1.indexOf(userInputHost) let pos2 = host2.indexOf(userInputHost) @@ -287,8 +286,8 @@ const getSortByDomainForSites = (userInputLower, userInputHost) => { */ const getSortByDomainForHosts = (userInputHost) => { return (host1, host2) => { - host1 = host1.replace('www.', '') - host2 = host2.replace('www.', '') + host1 = normalizeLocation(host1) + host2 = normalizeLocation(host2) let pos1 = host1.indexOf(userInputHost) let pos2 = host2.indexOf(userInputHost) if (pos1 !== -1 && pos2 === -1) { diff --git a/app/common/state/aboutPreferencesState.js b/app/common/state/aboutPreferencesState.js new file mode 100644 index 00000000000..c23231b3fe2 --- /dev/null +++ b/app/common/state/aboutPreferencesState.js @@ -0,0 +1,71 @@ +/* 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 assert = require('assert') + +// utils +const {makeImmutable, isMap} = require('../../common/state/immutableUtil') + +const validateState = (state) => { + state = makeImmutable(state) + assert.ok(isMap(state), 'state must be an Immutable.Map') + assert.ok(isMap(state.getIn(['about', 'preferences'])), 'state must contain an Immutable.Map of \'about\' \'preferences\'') + return state +} +const aboutPreferencesState = { + setBackupStatus: (state, status) => { + state = validateState(state) + if (status == null) { + return state + } + const date = new Date().getTime() + state = aboutPreferencesState.setPreferencesProp(state, 'backupSucceeded', status) + return aboutPreferencesState.setPreferencesProp(state, 'updatedStamp', date) + }, + + hasBeenBackedUp: (state) => { + state = validateState(state) + return (aboutPreferencesState.getPreferencesProp(state, 'backupSucceeded') || aboutPreferencesState.getPreferencesProp(state, 'updatedStamp') != null) || false + }, + + getPreferencesProp: (state, key) => { + state = validateState(state) + if (key == null) { + return null + } + return state.getIn(['about', 'preferences', key]) + }, + + setPreferencesProp: (state, key, value) => { + state = validateState(state) + if (key == null) { + return state + } + return state.setIn(['about', 'preferences', key], value) + }, + + setRecoveryInProgress: (state, inProgress) => { + state = validateState(state) + return aboutPreferencesState.setPreferencesProp(state, 'recoveryInProgress', inProgress) + }, + + setRecoveryBalanceRecalculated: (state, hasRecalculated) => { + state = validateState(state) + return aboutPreferencesState.setPreferencesProp(state, 'recoveryBalanceRecalculated', hasRecalculated) + }, + + getRecoveryBalanceRecalulated: (state) => { + state = validateState(state) + return aboutPreferencesState.getPreferencesProp(state, 'recoveryBalanceRecalculated') || false + }, + + setRecoveryStatus: (state, status) => { + state = validateState(state) + const date = new Date().getTime() + state = aboutPreferencesState.setRecoveryInProgress(state, false) + state = aboutPreferencesState.setPreferencesProp(state, 'recoverySucceeded', status) + return aboutPreferencesState.setPreferencesProp(state, 'updatedStamp', date) + } +} +module.exports = aboutPreferencesState diff --git a/app/common/state/bookmarkFoldersState.js b/app/common/state/bookmarkFoldersState.js index 4db477cacd4..7cbfb29ccc6 100644 --- a/app/common/state/bookmarkFoldersState.js +++ b/app/common/state/bookmarkFoldersState.js @@ -198,17 +198,6 @@ const bookmarkFoldersState = { append ) return state - }, - - setWidth: (state, key, width) => { - state = validateState(state) - width = parseFloat(width) - - if (key == null || isNaN(width)) { - return state - } - - return state.setIn([STATE_SITES.BOOKMARK_FOLDERS, key, 'width'], width) } } diff --git a/app/common/state/bookmarkToolbarState.js b/app/common/state/bookmarkToolbarState.js deleted file mode 100644 index 9b10bcc577e..00000000000 --- a/app/common/state/bookmarkToolbarState.js +++ /dev/null @@ -1,72 +0,0 @@ -/* 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 assert = require('assert') -const Immutable = require('immutable') - -// State -const bookmarksState = require('./bookmarksState') -const windowState = require('./windowState') - -// Utils -const {makeImmutable, isList, isMap} = require('./immutableUtil') -const bookmarkToolbarUtil = require('../lib/bookmarkToolbarUtil') - -const validateState = function (state) { - state = makeImmutable(state) - assert.ok(isMap(state), 'state must be an Immutable.Map') - assert.ok(isList(state.get('windows'), 'state must contain an Immutable.List of windows')) - return state -} - -const bookmarkToolbarState = { - setToolbars: (state) => { - validateState(state) - const bookmarks = bookmarksState.getBookmarksWithFolders(state, 0) - - state.get('windows').forEach((item, index) => { - const width = state.getIn(['windows', index, 'width']) - const data = bookmarkToolbarUtil.getBookmarkKeys(width, bookmarks) - - if (!state.hasIn(['windows', index])) { - return state - } - - state = state - .setIn(['windows', index, 'bookmarksToolbar', 'toolbar'], data.toolbar) - .setIn(['windows', index, 'bookmarksToolbar', 'other'], data.other) - }) - - return state - }, - - setToolbar: (state, windowId) => { - validateState(state) - const bookmarks = bookmarksState.getBookmarksWithFolders(state, 0) - const windowIndex = windowState.getWindowIndexByWindowId(state, windowId) - - if (!state.hasIn(['windows', windowIndex])) { - return state - } - - const width = state.getIn(['windows', windowIndex, 'width']) - const data = bookmarkToolbarUtil.getBookmarkKeys(width, bookmarks) - - return state - .setIn(['windows', windowIndex, 'bookmarksToolbar', 'toolbar'], data.toolbar) - .setIn(['windows', windowIndex, 'bookmarksToolbar', 'other'], data.other) - }, - - getToolbar: (state, windowId) => { - const index = windowState.getWindowIndexByWindowId(state, windowId) - return state.getIn(['windows', index, 'bookmarksToolbar', 'toolbar'], Immutable.List()) - }, - - getOther: (state, windowId) => { - const index = windowState.getWindowIndexByWindowId(state, windowId) - return state.getIn(['windows', index, 'bookmarksToolbar', 'other'], Immutable.List()) - } -} - -module.exports = bookmarkToolbarState diff --git a/app/common/state/bookmarksState.js b/app/common/state/bookmarksState.js index 1053e7a0378..320c71fef9f 100644 --- a/app/common/state/bookmarksState.js +++ b/app/common/state/bookmarksState.js @@ -290,16 +290,6 @@ const bookmarksState = { const cache = bookmarkOrderCache.getBookmarksByParentId(state, folderKey) return cache.map((item) => bookmarksState.getBookmark(state, item.get('key'))) - }, - - setWidth: (state, key, width) => { - width = parseFloat(width) - - if (key == null || isNaN(width)) { - return state - } - - return state.setIn([STATE_SITES.BOOKMARKS, key, 'width'], width) } } diff --git a/app/common/state/contextMenuState.js b/app/common/state/contextMenuState.js index 31840aa9830..aa4b5cf356e 100644 --- a/app/common/state/contextMenuState.js +++ b/app/common/state/contextMenuState.js @@ -5,6 +5,7 @@ const Immutable = require('immutable') const assert = require('assert') const { makeImmutable, isMap } = require('./immutableUtil') +const uuid = require('uuid') const validateState = function (state) { state = makeImmutable(state) @@ -12,31 +13,34 @@ const validateState = function (state) { return state } +let contextMenuDetail = Immutable.Map() + const api = { setContextMenu: (windowState, detail) => { detail = makeImmutable(detail) windowState = validateState(windowState) if (!detail) { - if (windowState.getIn(['contextMenuDetail', 'type']) === 'hamburgerMenu') { + if (contextMenuDetail.get('type') === 'hamburgerMenu') { windowState = windowState.set('hamburgerMenuWasOpen', true) } else { windowState = windowState.set('hamburgerMenuWasOpen', false) } + contextMenuDetail = Immutable.Map() windowState = windowState.delete('contextMenuDetail') } else { if (!(detail.get('type') === 'hamburgerMenu' && windowState.get('hamburgerMenuWasOpen'))) { - windowState = windowState.set('contextMenuDetail', detail) + contextMenuDetail = detail + windowState = windowState.set('contextMenuDetail', uuid()) } windowState = windowState.set('hamburgerMenuWasOpen', false) } - return windowState }, getContextMenu: (windowState) => { windowState = validateState(windowState) - return windowState.get('contextMenuDetail', Immutable.Map()) + return contextMenuDetail }, selectedIndex: (windowState) => { diff --git a/app/common/state/extensionState.js b/app/common/state/extensionState.js index 68cfa50022b..84d22cd21ba 100644 --- a/app/common/state/extensionState.js +++ b/app/common/state/extensionState.js @@ -2,9 +2,15 @@ * 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 { makeImmutable } = require('./immutableUtil') const Immutable = require('immutable') + +// Constants +const settings = require('../../../js/constants/settings') + +// Utils +const { makeImmutable } = require('./immutableUtil') const platformUtil = require('../lib/platformUtil') +const getSetting = require('../../../js/settings').getSetting const {chromeUrl} = require('../../../js/lib/appUrlUtil') const browserActionDefaults = Immutable.fromJS({ @@ -205,6 +211,15 @@ const extensionState = { }) }) return allProperties + }, + + isWebTorrentEnabled: (state) => { + if (state == null) { + return false + } + + const settingsState = state.get('settings') + return getSetting(settings.TORRENT_VIEWER_ENABLED, settingsState) } } diff --git a/app/common/state/frameState.js b/app/common/state/frameState.js index dbbe8b3275e..befb3f8ed50 100644 --- a/app/common/state/frameState.js +++ b/app/common/state/frameState.js @@ -37,7 +37,7 @@ const api = { return null } return path.concat(['frames', index]) - } else { + } else if (state.get('tabs')) { // in AppState const index = state.get('tabs').findIndex((tab) => tab.getIn(['frame', 'key']) === frameKey) if (index === -1) { diff --git a/app/common/state/historyState.js b/app/common/state/historyState.js index 4dd8cf41401..130d378f3fc 100644 --- a/app/common/state/historyState.js +++ b/app/common/state/historyState.js @@ -8,6 +8,7 @@ const {STATE_SITES} = require('../../../js/constants/stateConstants') const historyUtil = require('../lib/historyUtil') const urlUtil = require('../../../js/lib/urlutil') const {makeImmutable, isMap} = require('./immutableUtil') +const shouldLogWarnings = process.env.NODE_ENV !== 'production' const validateState = function (state) { state = makeImmutable(state) @@ -33,8 +34,20 @@ const historyState = { }, addSite: (state, siteDetail) => { + if (!siteDetail) { + if (shouldLogWarnings) { + console.error('historyState:addSite siteDetail was null') + } + return state + } let sites = historyState.getSites(state) let siteKey = historyUtil.getKey(siteDetail) + if (!siteKey) { + if (shouldLogWarnings) { + console.log('historyState:addSite siteKey was null for siteDetail:', (siteDetail && siteDetail.toJS) ? siteDetail.toJS() : siteDetail) + } + return state + } siteDetail = makeImmutable(siteDetail) const oldSite = sites.get(siteKey) diff --git a/app/common/state/immutableUtil.js b/app/common/state/immutableUtil.js index 2d758d1225b..920d92eb28b 100644 --- a/app/common/state/immutableUtil.js +++ b/app/common/state/immutableUtil.js @@ -45,6 +45,22 @@ const api = { return defaultValue } return api.isImmutable(obj) ? obj.toJS() : obj + }, + + findNullKeyPaths (state, pathToState = []) { + let nullKeys = [ ] + if (!Immutable.Map.isMap(state) && !Immutable.List.isList(state)) { + return nullKeys + } + for (const key of state.keySeq()) { + const keyPath = [...pathToState, key] + if (key === null) { + nullKeys.push(keyPath) + } + // recursive, to find deep keys + nullKeys.push(...api.findNullKeyPaths(state.get(key), keyPath)) + } + return nullKeys } } diff --git a/app/common/state/ledgerState.js b/app/common/state/ledgerState.js index 2392ff24b37..1bc60144060 100644 --- a/app/common/state/ledgerState.js +++ b/app/common/state/ledgerState.js @@ -16,9 +16,9 @@ const settings = require('../../../js/constants/settings') // Utils const getSetting = require('../../../js/settings').getSetting -const siteSettings = require('../../../js/state/siteSettings') -const urlUtil = require('../../../js/lib/urlutil') const {makeImmutable, isMap} = require('../../common/state/immutableUtil') +const urlParse = require('../../common/urlParse') +const getBaseDomain = require('../../../js/lib/baseDomain').getBaseDomain const validateState = function (state) { state = makeImmutable(state) @@ -67,12 +67,28 @@ const ledgerState = { return state.setIn(['ledger', 'locations', url, prop], value) }, + getVerifiedPublisherLocation: (state, url) => { + state = validateState(state) + if (url == null) { + return null + } + + let publisherKey = state.getIn(['ledger', 'locations', url, 'publisher']) + + if (!publisherKey) { + const parsedUrl = urlParse(url) || {} + if (parsedUrl.hostname != null) { + publisherKey = getBaseDomain(parsedUrl.hostname) + } + } + return publisherKey + }, + getLocationProp: (state, url, prop) => { state = validateState(state) if (url == null || prop == null) { return null } - return state.getIn(['ledger', 'locations', url, prop]) }, @@ -106,21 +122,25 @@ const ledgerState = { return state }, - resetSynopsis: (state, options = false) => { + deleteSynopsis: (state) => { state = validateState(state) - - if (options) { - state = state - .setIn(['ledger', 'synopsis', 'options'], Immutable.Map()) - .setIn(['ledger', 'about', 'synopsisOptions'], Immutable.Map()) - } - state = pageDataState.resetPageData(state) return state - .setIn(['ledger', 'synopsis', 'publishers'], Immutable.Map()) - .setIn(['ledger', 'locations'], Immutable.Map()) - .setIn(['ledger', 'about', 'synopsis'], Immutable.List()) + .setIn(['cache', 'ledgerVideos'], Immutable.Map()) + .set('ledger', Immutable.fromJS({ + about: { + synopsis: [], + synopsisOptions: {} + }, + info: {}, + locations: {}, + synopsis: { + options: {}, + publishers: {} + }, + promotion: {} + })) }, /** @@ -179,6 +199,18 @@ const ledgerState = { return state.setIn(['ledger', 'synopsis', 'publishers', key, prop], value) }, + resetPublishers: (state) => { + state = validateState(state) + state = pageDataState.resetPageData(state) + + return state + .setIn(['ledger', 'synopsis', 'publishers'], Immutable.Map()) + .setIn(['ledger', 'locations'], Immutable.Map()) + .setIn(['ledger', 'about', 'synopsis'], Immutable.List()) + .setIn(['ledger', 'publisherTimestamp'], 0) + .setIn(['cache', 'ledgerVideos'], Immutable.Map()) + }, + /** * SYNOPSIS / PUBLISHER / OPTIONS */ @@ -288,6 +320,11 @@ const ledgerState = { if (paymentId) { newData = newData.set('paymentId', paymentId) } + + const transactions = ledgerState.getInfoProp(state, 'transactions') + if (transactions) { + newData = newData.set('transactions', transactions) + } } return state.setIn(['ledger', 'info'], newData) @@ -332,13 +369,6 @@ const ledgerState = { /** * OTHERS */ - setRecoveryStatus: (state, status) => { - state = validateState(state) - const date = new Date().getTime() - state = state.setIn(['about', 'preferences', 'recoverySucceeded'], status) - return state.setIn(['about', 'preferences', 'updatedStamp'], date) - }, - setLedgerError: (state, error, caller) => { state = validateState(state) if (error == null && caller == null) { @@ -351,24 +381,6 @@ const ledgerState = { })) }, - changePinnedValues: (state, publishers) => { - state = validateState(state) - if (publishers == null) { - return state - } - - publishers = makeImmutable(publishers) - publishers.forEach((item) => { - const publisherKey = item.get('publisherKey') - const pattern = urlUtil.getHostPattern(publisherKey) - const percentage = item.get('pinPercentage') - let newSiteSettings = siteSettings.mergeSiteSetting(state.get('siteSettings'), pattern, 'ledgerPinPercentage', percentage) - state = state.set('siteSettings', newSiteSettings) - }) - - return state - }, - /** * PROMOTIONS */ @@ -513,15 +525,15 @@ const ledgerState = { // TODO (optimization) don't have two almost identical object in state (synopsi->publishers and about->synopsis) saveAboutSynopsis: (state, publishers) => { state = validateState(state) + state = ledgerState.setAboutProp(state, 'synopsis', publishers) + state = ledgerState.setAboutProp(state, 'synopsisOptions', ledgerState.getSynopsisOptions(state)) + return state - .setIn(['ledger', 'about', 'synopsis'], publishers) - .setIn(['ledger', 'about', 'synopsisOptions'], ledgerState.getSynopsisOptions(state)) }, setAboutSynopsisOptions: (state) => { state = validateState(state) - return state - .setIn(['ledger', 'about', 'synopsisOptions'], ledgerState.getSynopsisOptions(state)) + return ledgerState.setAboutProp(state, 'synopsisOptions', ledgerState.getSynopsisOptions(state)) }, getAboutData: (state) => { @@ -545,6 +557,7 @@ const ledgerState = { let promotion = ledgerState.getActivePromotion(state) const claim = state.getIn(['ledger', 'promotion', 'claimedTimestamp']) || null const status = state.getIn(['ledger', 'promotion', 'promotionStatus']) || null + const captcha = state.getIn(['ledger', 'promotion', 'captcha']) || null if (claim) { promotion = promotion.set('claimedTimestamp', claim) @@ -554,7 +567,31 @@ const ledgerState = { promotion = promotion.set('promotionStatus', status) } + if (captcha) { + promotion = promotion.set('captcha', captcha) + } + return promotion + }, + + setAboutProp: (state, prop, value) => { + state = validateState(state) + + if (prop == null) { + return state + } + + return state.setIn(['ledger', 'about', prop], value) + }, + + getAboutProp: (state, prop) => { + state = validateState(state) + + if (prop == null) { + return null + } + + return state.getIn(['ledger', 'about', prop]) } } diff --git a/app/common/state/migrationState.js b/app/common/state/migrationState.js deleted file mode 100644 index 06f8985527d..00000000000 --- a/app/common/state/migrationState.js +++ /dev/null @@ -1,69 +0,0 @@ -/* 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 assert = require('assert') - -const {makeImmutable, isMap} = require('../../common/state/immutableUtil') - -const validateState = function (state) { - state = makeImmutable(state) - assert.ok(isMap(state), 'state must be an Immutable.Map') - assert.ok(isMap(state.get('migrations')), 'state must contain an Immutable.Map of migrations') - return state -} - -const migrationState = { - setTransitionStatus: (state, value) => { - state = validateState(state) - if (value == null) { - return state - } - - return state.setIn(['migrations', 'btc2BatTransitionPending'], value) - }, - - inTransition: (state) => { - state = validateState(state) - return state.getIn(['migrations', 'btc2BatTransitionPending']) === true - }, - - setConversionTimestamp: (state, value) => { - state = validateState(state) - if (value == null) { - return state - } - - return state.setIn(['migrations', 'btc2BatTimestamp'], value) - }, - - setNotifiedTimestamp: (state, value) => { - state = validateState(state) - if (value == null) { - return state - } - - return state.setIn(['migrations', 'btc2BatNotifiedTimestamp'], value) - }, - - isNewInstall: (state) => { - state = validateState(state) - return state.get('firstRunTimestamp') === state.getIn(['migrations', 'batMercuryTimestamp']) - }, - - // we set this values when we initialize 0.19 state and this will be only true when transition is done - // or when you create wallet on 0.19+ version - hasUpgradedWallet: (state) => { - state = validateState(state) - return state.getIn(['migrations', 'batMercuryTimestamp']) !== state.getIn(['migrations', 'btc2BatTimestamp']) - }, - - // we set this values when we initialize 0.19 state and this will be only true when transition is done - // or when you create wallet on 0.19+ version - hasBeenNotified: (state) => { - state = validateState(state) - return state.getIn(['migrations', 'batMercuryTimestamp']) !== state.getIn(['migrations', 'btc2BatNotifiedTimestamp']) - } -} - -module.exports = migrationState diff --git a/app/common/state/pageDataState.js b/app/common/state/pageDataState.js index da6cc816489..47e9fd923e4 100644 --- a/app/common/state/pageDataState.js +++ b/app/common/state/pageDataState.js @@ -83,6 +83,7 @@ const pageDataState = { .setIn(['pageData', 'info'], Immutable.Map()) .setIn(['pageData', 'last', 'info'], null) .setIn(['pageData', 'last', 'tabId'], null) + .setIn(['pageData', 'last', 'closedTabValue'], null) } } diff --git a/app/common/state/pinnedSitesState.js b/app/common/state/pinnedSitesState.js index 59fd051a50a..ad4a5ddf186 100644 --- a/app/common/state/pinnedSitesState.js +++ b/app/common/state/pinnedSitesState.js @@ -98,48 +98,42 @@ const pinnedSiteState = { }, /** - * Moves the specified pinned site from one location to another + * Moves the specified pinned site from one position to another * - * @param state The application state Immutable map + * @param state The application state Immutable map * @param sourceKey The site key to move - * @param destinationKey The site key to move to - * @param prepend Whether the destination detail should be prepended or not + * @param newOrder The new position to move to * @return The new state Immutable object */ - reOrderSite: (state, sourceKey, destinationKey, prepend) => { - state = validateState(state) - let sites = state.get(STATE_SITES.PINNED_SITES) + moveSiteToNewOrder: (state, sourceKey, newOrder, shouldDebug = false) => { + const sites = state.get(STATE_SITES.PINNED_SITES) + if (shouldDebug) { + console.log('moveSiteToNewOrder pinnedSites before', sites.toJS()) + } let sourceSite = sites.get(sourceKey, Immutable.Map()) - const destinationSite = sites.get(destinationKey, Immutable.Map()) - - if (sourceSite.isEmpty()) { + if (sourceSite.isEmpty() || sourceSite.get('order') === newOrder) { + if (shouldDebug) { + console.log('NO CHANGE') + } return state } - - const sourceSiteIndex = sourceSite.get('order') - const destinationSiteIndex = destinationSite.get('order') - let newIndex = destinationSiteIndex + (prepend ? 0 : 1) - if (destinationSiteIndex > sourceSiteIndex) { - --newIndex - } - - state = state.set(STATE_SITES.PINNED_SITES, state.get(STATE_SITES.PINNED_SITES).map((site, index) => { + const sourceSiteOrder = sourceSite.get('order') + state = state.set(STATE_SITES.PINNED_SITES, sites.map((site, index) => { const siteOrder = site.get('order') - if (index === sourceKey) { - return site + if (index === sourceKey && siteOrder !== newOrder) { + return site.set('order', newOrder) } - - if (siteOrder >= newIndex && siteOrder < sourceSiteIndex) { + if (siteOrder >= newOrder && siteOrder < sourceSiteOrder) { return site.set('order', siteOrder + 1) - } else if (siteOrder <= newIndex && siteOrder > sourceSiteIndex) { + } else if (siteOrder <= newOrder && siteOrder > sourceSiteOrder) { return site.set('order', siteOrder - 1) } - return site })) - - sourceSite = sourceSite.set('order', newIndex) - return state.setIn([STATE_SITES.PINNED_SITES, sourceKey], sourceSite) + if (shouldDebug) { + console.log('moveSiteToNewOrder pinnedSites after', state.get(STATE_SITES.PINNED_SITES).toJS()) + } + return state } } diff --git a/app/common/state/tabContentState/audioState.js b/app/common/state/tabContentState/audioState.js index 49b8b5835d8..4f88eb9089d 100644 --- a/app/common/state/tabContentState/audioState.js +++ b/app/common/state/tabContentState/audioState.js @@ -49,6 +49,10 @@ module.exports.showAudioTopBorder = (state, frameKey, isPinned) => { return false } + if (module.exports.isAudioMuted(state, frameKey)) { + return false + } + return ( module.exports.canPlayAudio(state, frameKey) && (isEntryIntersected(state, 'tabs') || isPinned) diff --git a/app/common/state/tabContentState/titleState.js b/app/common/state/tabContentState/titleState.js index 6bdc5b75f47..1c21e294c35 100644 --- a/app/common/state/tabContentState/titleState.js +++ b/app/common/state/tabContentState/titleState.js @@ -8,6 +8,7 @@ const partitionState = require('../tabContentState/partitionState') const privateState = require('../tabContentState/privateState') const frameStateUtil = require('../../../../js/state/frameStateUtil') + const tabUIState = require('../tabUIState') // Utils const {isEntryIntersected} = require('../../../../app/renderer/lib/observerUtil') @@ -26,11 +27,13 @@ const isActive = frameStateUtil.isFrameKeyActive(state, frameKey) const isPartition = partitionState.isPartitionTab(state, frameKey) const isPrivate = privateState.isPrivateTab(state, frameKey) - const secondaryIconVisible = !isNewTabPage && (isPartition || isPrivate || isActive) + const secondaryIconVisible = !isNewTabPage && + (isPartition || isPrivate || isActive) && + tabUIState.showTabEndIcon(state, frameKey) // If title is being intersected by ~half with other icons visible // such as closeTab (activeTab) or session icons, do not show it - if (isEntryIntersected(state, 'tabs', intersection.at45) && secondaryIconVisible) { + if (isEntryIntersected(state, 'tabs', intersection.at46) && secondaryIconVisible) { return false } diff --git a/app/common/state/tabState.js b/app/common/state/tabState.js index 74cb2cf1222..346d2a53514 100644 --- a/app/common/state/tabState.js +++ b/app/common/state/tabState.js @@ -61,6 +61,10 @@ const validateAction = function (action) { return action } +const selectTabs = function (state) { + return state.get('tabs', Immutable.List()).filter(tab => !tab.isEmpty()) +} + const matchTab = function (queryInfo, tab) { queryInfo = queryInfo.toJS ? queryInfo.toJS() : queryInfo return !Object.keys(queryInfo).map((queryKey) => (tab.get(queryKey) === queryInfo[queryKey])).includes(false) @@ -130,6 +134,9 @@ const updateTabsInternalIndex = (state, fromIndex) => { fromIndex = validateIndex(fromIndex) let tabsInternal = state.get('tabsInternal') || Immutable.Map() state.get('tabs').slice(fromIndex).forEach((tab, idx) => { + if (tab.isEmpty()) { + return + } const tabId = validateId('tabId', tab.get('tabId')).toString() if (tabId !== tabState.TAB_ID_NONE) { tabsInternal = tabsInternal.setIn(['index', tabId], (idx + fromIndex).toString()) @@ -171,7 +178,7 @@ const tabState = { return state } state = deleteTabsInternalIndex(state, tabValue) - state = state.set('tabs', state.get('tabs').delete(index)) + state = state.setIn(['tabs', index], Immutable.Map()) return updateTabsInternalIndex(state, index) }, @@ -208,12 +215,12 @@ const tabState = { getTabsByWindowId: (state, windowId) => { state = validateState(state) windowId = validateId('windowId', windowId) - return state.get('tabs').filter((tab) => tab.get('windowId') === windowId) + return selectTabs(state).filter((tab) => tab.get('windowId') === windowId) }, getPinnedTabs: (state) => { state = validateState(state) - return state.get('tabs').filter((tab) => !!tab.get('pinned')) + return selectTabs(state).filter((tab) => !!tab.get('pinned')) }, isTabPinned: (state, tabId) => { @@ -225,7 +232,7 @@ const tabState = { getNonPinnedTabs: (state) => { state = validateState(state) - return state.get('tabs').filter((tab) => !tab.get('pinned')) + return selectTabs(state).filter((tab) => !tab.get('pinned')) }, getPinnedTabsByWindowId: (state, windowId) => { @@ -404,6 +411,31 @@ const tabState = { return state.set('tabs', tabs.delete(index).insert(index, tabValue)) }, + replaceTabValue: (state, tabId, newTabValue) => { + state = validateState(state) + newTabValue = makeImmutable(newTabValue) + // update tab + const index = getTabInternalIndexByTabId(state, tabId) + const oldTabValue = state.getIn(['tabs', index]) + if (index == null || index === -1) { + console.error(`tabState: cannot replace tab ${tabId} as tab's index did not exist in state`, { index }) + return state + } + let mergedTabValue = oldTabValue.mergeDeep(newTabValue) + if (mergedTabValue.has('frame')) { + mergedTabValue = mergedTabValue.mergeIn(['frame'], { + tabId: newTabValue.get('tabId'), + guestInstanceId: newTabValue.get('guestInstanceId') + }) + } + mergedTabValue = mergedTabValue.set('windowId', oldTabValue.get('windowId')) + state = state.set('tabs', state.get('tabs').delete(index).insert(index, mergedTabValue)) + // update tabId at tabsInternal index + state = deleteTabsInternalIndex(state, oldTabValue) + state = updateTabsInternalIndex(state, 0) + return state + }, + removeTabField: (state, field) => { state = makeImmutable(state) @@ -417,23 +449,30 @@ const tabState = { return state.set('tabs', tabs) }, - updateFrame: (state, action) => { + updateFrame: (state, action, shouldDebugTabEvents = false) => { state = validateState(state) action = validateAction(action) const tabId = action.getIn(['frame', 'tabId']) + if (!tabId) { + if (shouldDebugTabEvents) { + console.log(`Tab [${tabId}] frame changed for tab - no tabId provided!`) + } return state } let tabValue = tabState.getByTabId(state, tabId) if (!tabValue) { + if (shouldDebugTabEvents) { + console.log(`Tab [${tabId}] frame changed for tab - tab not found in state, probably a temporary frame`) + } return state } - const bookmarkUtil = require('../lib/bookmarkUtil') - const frameLocation = action.getIn(['frame', 'location']) - const frameBookmarked = bookmarkUtil.isLocationBookmarked(state, frameLocation) - const frameValue = action.get('frame').set('bookmarked', frameBookmarked) + if (shouldDebugTabEvents) { + console.log(`Tab [${tabId}] frame changed for tab`) + } + const frameValue = action.get('frame') tabValue = tabValue.set('frame', makeImmutable(frameValue)) return tabState.updateTabValue(state, tabValue) }, @@ -620,7 +659,8 @@ const tabState = { getVisibleOrigin: (state, tabId) => { const entry = tabState.getVisibleEntry(state, tabId) - const origin = entry ? entry.get('origin') : '' + // plain js in browser, immutable in renderer + const origin = entry ? entry.get ? entry.get('origin') : entry.origin : '' // TODO(bridiver) - all origins in browser-laptop should be changed to have a trailing slash to match chromium return (origin || '').replace(/\/$/, '') }, @@ -628,6 +668,35 @@ const tabState = { getVisibleVirtualURL: (state, tabId) => { const entry = tabState.getVisibleEntry(state, tabId) return entry ? entry.get('virtualURL') : '' + }, + + setTabStripWindowId: (state, tabId, windowId) => { + let path = tabState.getPathByTabId(state, tabId) + if (!path) { + console.error(`setTabStripWindowId: tab with ID ${tabId} not found!`) + return state + } + path = [...path, 'tabStripWindowId'] + // handle clear window + if (windowId == null || windowId === -1) { + return state.deleteIn(path) + } + // handle set window + return state.setIn(path, windowId) + }, + + setZoomPercent: (state, tabId, zoomPercent) => { + let path = tabState.getPathByTabId(state, tabId) + if (!path) { + console.error(`setZoomPercent: tab with ID ${tabId} not found!`) + return state + } + if (typeof zoomPercent !== 'number') { + console.error(`setZoomPercent: bad value for zoomPercent: ${zoomPercent}`) + return state + } + path = [...path, 'zoomPercent'] + return state.setIn(path, zoomPercent) } } diff --git a/app/common/state/tabUIState.js b/app/common/state/tabUIState.js index 072ff4f0d04..55159538bc9 100644 --- a/app/common/state/tabUIState.js +++ b/app/common/state/tabUIState.js @@ -6,8 +6,6 @@ const settings = require('../../../js/constants/settings') // State helpers -const partitionState = require('../../common/state/tabContentState/partitionState') -const privateState = require('../../common/state/tabContentState/privateState') const closeState = require('../../common/state/tabContentState/closeState') const frameStateUtil = require('../../../js/state/frameStateUtil') @@ -20,7 +18,6 @@ const {getSetting} = require('../../../js/settings') // Styles const {intersection} = require('../../renderer/components/styles/global') -const {theme} = require('../../renderer/components/styles/theme') module.exports.getThemeColor = (state, frameKey) => { const frame = frameStateUtil.getFrameByKey(state, frameKey) @@ -76,7 +73,7 @@ module.exports.showTabEndIcon = (state, frameKey) => { return ( !closeState.hasFixedCloseIcon(state, frameKey) && !closeState.hasRelativeCloseIcon(state, frameKey) && - !isEntryIntersected(state, 'tabs', intersection.at40) + !isEntryIntersected(state, 'tabs', intersection.at46) ) } @@ -99,41 +96,3 @@ module.exports.centralizeTabIcons = (state, frameKey, isPinned) => { return isPinned || isEntryIntersected(state, 'tabs', intersection.at40) } - -module.exports.getTabEndIconBackgroundColor = (state, frameKey) => { - const frame = frameStateUtil.getFrameByKey(state, frameKey) - - if (frame == null) { - return false - } - - const themeColor = module.exports.getThemeColor(state, frameKey) - const isPrivate = privateState.isPrivateTab(state, frameKey) - const isPartition = partitionState.isPartitionTab(state, frameKey) - const isHover = frameStateUtil.getTabHoverState(state, frameKey) - const isActive = frameStateUtil.isFrameKeyActive(state, frameKey) - const hasCloseIcon = closeState.showCloseTabIcon(state, frameKey) - const isIntersecting = isEntryIntersected(state, 'tabs', intersection.at40) - - let backgroundColor = theme.tab.background - - if (isActive && themeColor) { - backgroundColor = themeColor - } - if (isActive && !themeColor) { - backgroundColor = theme.tab.active.background - } - if (isIntersecting) { - backgroundColor = 'transparent' - } - if (!isActive && isPrivate) { - backgroundColor = theme.tab.private.background - } - if ((isActive || isHover) && isPrivate) { - backgroundColor = theme.tab.active.private.background - } - - return isPartition || isPrivate || hasCloseIcon - ? `linear-gradient(to left, ${backgroundColor} 10px, transparent 40px)` - : `linear-gradient(to left, ${backgroundColor} 0, transparent 12px)` -} diff --git a/app/common/state/urlBarState.js b/app/common/state/urlBarState.js index b07420d75bc..e45e37d753d 100644 --- a/app/common/state/urlBarState.js +++ b/app/common/state/urlBarState.js @@ -19,27 +19,33 @@ const api = { */ getSearchData: function (state, activeFrame) { // TODO: don't have activeFrame param when reselect is used for state retrieval memoization - const urlbar = api.getActiveFrameUrlBarState(activeFrame) - const activeFrameIsPrivate = activeFrame.get('isPrivate') - const urlbarSearchDetail = urlbar.get('searchDetail') - const appSearchDetail = state.get('searchDetail') - const activateSearchEngine = urlbarSearchDetail && urlbarSearchDetail.get('activateSearchEngine') + let searchURL + let searchShortcut // get default search provider from app state - let searchURL = + const appSearchDetail = state.get('searchDetail') + const activeFrameIsPrivate = activeFrame.get('isPrivate') + if (appSearchDetail) { + searchURL = (activeFrameIsPrivate && appSearchDetail.has('privateSearchURL')) ? appSearchDetail.get('privateSearchURL') : appSearchDetail.get('searchURL') - let searchShortcut = '' + searchShortcut = '' + } + // change search url if overrided by active frame state or shortcut + const urlbar = api.getActiveFrameUrlBarState(activeFrame) + const urlbarSearchDetail = urlbar && urlbar.get('searchDetail') + const activateSearchEngine = urlbarSearchDetail && urlbarSearchDetail.get('activateSearchEngine') if (activateSearchEngine) { const provider = urlbarSearchDetail searchShortcut = new RegExp('^' + provider.get('shortcut') + ' ', 'g') searchURL = - (activeFrame.get('isPrivate') && provider.has('privateSearch')) + (activeFrameIsPrivate && provider.has('privateSearch')) ? provider.get('privateSearch') : provider.get('search') } + return { searchURL, searchShortcut diff --git a/app/common/tracing.js b/app/common/tracing.js index 747a1ac3d11..7a07012ddf7 100644 --- a/app/common/tracing.js +++ b/app/common/tracing.js @@ -52,7 +52,7 @@ exports.trace = (obj, ...args) => { } return function (...fnArgs) { metadata.name = propKey - muon.crashReporter.setCrashKeyValue('javascript-info', JSON.stringify(metadata)) + muon.crashReporter.setJavascriptInfoCrashValue(JSON.stringify(metadata)) let result, end, exception const start = timer.now() try { @@ -64,7 +64,7 @@ exports.trace = (obj, ...args) => { metadata.stack = e.stack != null ? e.stack : e.name + ': ' + e.message exception = e } - muon.crashReporter.setCrashKeyValue('javascript-info', JSON.stringify(metadata)) + muon.crashReporter.setJavascriptInfoCrashValue(JSON.stringify(metadata)) if (exception) { muon.crashReporter.dumpWithoutCrashing() throw exception diff --git a/app/crash-herald.js b/app/crash-herald.js index 682bc53b14b..de23e0e6458 100644 --- a/app/crash-herald.js +++ b/app/crash-herald.js @@ -8,19 +8,13 @@ const {app} = require('electron') const version = app.getVersion() const channel = Channel.channel() -const crashKeys = { - '_version': version, - 'channel': channel -} - const initCrashKeys = () => { // set muon-app-version switch to pass version to renderer processes app.commandLine.appendSwitch('muon-app-version', version) app.commandLine.appendSwitch('muon-app-channel', channel) - for (let key in crashKeys) { - muon.crashReporter.setCrashKeyValue(key, crashKeys[key]) - } + muon.crashReporter.setVersionCrashValue(version) + muon.crashReporter.setChannelCrashValue(channel) } exports.init = (enabled) => { diff --git a/app/dataFile.js b/app/dataFile.js index 6abe76f8c9f..8f02c0438dc 100644 --- a/app/dataFile.js +++ b/app/dataFile.js @@ -87,7 +87,7 @@ module.exports.init = (resourceName, version, startExtension, onInitDone, forceD let versionFolder = version const hasStagedDatFile = [appConfig.resourceNames.ADBLOCK, appConfig.resourceNames.SAFE_BROWSING].includes(resourceName) - if (process.env.NODE_ENV === 'development' && hasStagedDatFile) { + if (hasStagedDatFile && (process.env.NODE_ENV === 'development' || process.env.BRAVE_USE_STAGING_DATA_FILES !== undefined)) { versionFolder = `test/${versionFolder}` } const url = appConfig[resourceName].url.replace('{version}', versionFolder) diff --git a/app/extensions.js b/app/extensions.js index 9cae84b5d07..82ca7772b47 100644 --- a/app/extensions.js +++ b/app/extensions.js @@ -4,7 +4,7 @@ const extensionActions = require('./common/actions/extensionActions') const config = require('../js/constants/config') const appConfig = require('../js/constants/appConfig') const {fileUrl} = require('../js/lib/appUrlUtil') -const {getExtensionsPath, getBraveExtUrl, getBraveExtIndexHTML} = require('../js/lib/appUrlUtil') +const {getComponentExtensionsPath, getExtensionsPath, getBraveExtUrl, getBraveExtIndexHTML} = require('../js/lib/appUrlUtil') const {getSetting} = require('../js/settings') const settings = require('../js/constants/settings') const extensionStates = require('../js/constants/extensionStates') @@ -38,10 +38,18 @@ let generateBraveManifest = () => { manifest_version: 2, version: '1.0', background: { - scripts: [ 'content/scripts/idleHandler.js' ], + scripts: [ 'content/scripts/metaScraper.js', 'content/scripts/requestHandler.js', 'content/scripts/idleHandler.js' ], persistent: true }, content_scripts: [ + { + run_at: 'document_start', + all_frames: true, + matches: ['https://www.marketwatch.com/*'], + css: [ + 'content/styles/siteHack-marketwatch.com.css' + ] + }, { run_at: 'document_start', all_frames: true, @@ -175,7 +183,7 @@ let generateBraveManifest = () => { 'img/favicon.ico', 'img/newtab/defaultTopSitesIcon/appstore.png', 'img/newtab/defaultTopSitesIcon/brave.ico', - 'img/newtab/defaultTopSitesIcon/facebook.png', + 'img/newtab/defaultTopSitesIcon/github.png', 'img/newtab/defaultTopSitesIcon/playstore.png', 'img/newtab/defaultTopSitesIcon/twitter.png', 'img/newtab/defaultTopSitesIcon/youtube.png' @@ -198,6 +206,7 @@ let generateBraveManifest = () => { 'style-src': '\'self\' \'unsafe-inline\'', 'font-src': '\'self\' data:', 'img-src': '* data: file://*', + 'connect-src': '\'self\' https://www.youtube.com', 'frame-src': '\'self\' https://brave.com' } @@ -205,9 +214,11 @@ let generateBraveManifest = () => { // allow access to webpack dev server resources let devServer = 'localhost:' + process.env.npm_package_config_port cspDirectives['default-src'] = '\'self\' http://' + devServer - cspDirectives['connect-src'] = ['\'self\'', + cspDirectives['connect-src'] = [ + cspDirectives['connect-src'], 'http://' + devServer, - 'ws://' + devServer].join(' ') + 'ws://' + devServer + ].join(' ') cspDirectives['style-src'] = '\'self\' \'unsafe-inline\' http://' + devServer cspDirectives['font-src'] += ` http://${devServer}` } @@ -465,7 +476,7 @@ module.exports.init = () => { manifest = { name: 'PDF Viewer', manifest_version: 2, - version: '1.9.457', + version: '1.9.459', description: 'Uses HTML5 to display PDF files directly in the browser.', icons: { '128': 'icon128.png', @@ -524,7 +535,7 @@ module.exports.init = () => { loadExtension(config.braveExtensionId, getExtensionsPath('brave'), generateBraveManifest(), 'component') // Cryptotoken extension is loaded from electron_resources.pak extensionInfo.setState(config.cryptoTokenExtensionId, extensionStates.REGISTERED) - loadExtension(config.cryptoTokenExtensionId, path.join(process.resourcesPath, 'cryptotoken'), {}, 'component') + loadExtension(config.cryptoTokenExtensionId, getComponentExtensionsPath('cryptotoken'), {}, 'component') extensionInfo.setState(config.syncExtensionId, extensionStates.REGISTERED) loadExtension(config.syncExtensionId, getExtensionsPath('brave'), generateSyncManifest(), 'unpacked') diff --git a/app/extensions/brave/about-printkeys.html b/app/extensions/brave/about-printkeys.html new file mode 100644 index 00000000000..33a14a1c879 --- /dev/null +++ b/app/extensions/brave/about-printkeys.html @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + +
+ + diff --git a/app/extensions/brave/content/scripts/adInsertion.js b/app/extensions/brave/content/scripts/adInsertion.js index 5430444eed8..0fc314f2eea 100644 --- a/app/extensions/brave/content/scripts/adInsertion.js +++ b/app/extensions/brave/content/scripts/adInsertion.js @@ -157,7 +157,7 @@ if (chrome.contentSettings.adInsertion == 'allow') { var host = document.location.hostname if (host) { - host = host.replace('www.', '') + host = host.replace(/^www\./, '') chrome.ipcRenderer.on('set-ad-div-candidates', (e, divHost, adDivCandidates, placeholderUrl) => { // don't accidentally intercept messages not intended for this host if (host === divHost) { diff --git a/app/extensions/brave/content/scripts/blockCanvasFingerprinting.js b/app/extensions/brave/content/scripts/blockCanvasFingerprinting.js index 06dafc3f721..d58606b5cc6 100644 --- a/app/extensions/brave/content/scripts/blockCanvasFingerprinting.js +++ b/app/extensions/brave/content/scripts/blockCanvasFingerprinting.js @@ -144,7 +144,7 @@ if (chrome.contentSettings.canvasFingerprinting == 'block') { }) function reportBlock (type) { - var script_url = getOriginatingScriptUrl() + var script_url = getOriginatingScriptUrl() || window.location.href var msg = { type, scriptUrl: stripLineAndColumnNumbers(script_url) diff --git a/app/extensions/brave/content/scripts/metaScraper.js b/app/extensions/brave/content/scripts/metaScraper.js new file mode 100644 index 00000000000..e8855b2ff04 --- /dev/null +++ b/app/extensions/brave/content/scripts/metaScraper.js @@ -0,0 +1,102 @@ +/* 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/. */ + + // Main repository + // Version 3.9.2 +// https://github.com/microlinkhq/metascraper + +// Image +// https://github.com/microlinkhq/metascraper/tree/master/packages/metascraper-image + +// Author +// https://github.com/microlinkhq/metascraper/tree/master/packages/metascraper-author + +// Title +// https://github.com/microlinkhq/metascraper/tree/master/packages/metascraper-title + +// YouTube +// https://github.com/microlinkhq/metascraper/tree/master/packages/metascraper-youtube + +const metaScraperRules = { + // Rules + getImageRules: () => { + const wrap = rule => ({htmlDom,url}) => { + const value = rule(htmlDom) + return requestHandlerApi.isUrl(value) && requestHandlerApi.getUrl(url, value) + } + + return [ + // Youtube + ({htmlDom,url}) => { + const {id,service} = requestHandlerApi.getVideoId(url) + return service === 'youtube' && id && requestHandlerApi.getThumbnailUrl(id) + }, + // Regular + wrap(html => requestHandlerApi.getContent(html.querySelector('meta[property="og:image:secure_url"]'))), + wrap(html => requestHandlerApi.getContent(html.querySelector('meta[property="og:image:url"]'))), + wrap(html => requestHandlerApi.getContent(html.querySelector('meta[property="og:image"]'))), + wrap(html => requestHandlerApi.getContent(html.querySelector('meta[name="twitter:image:src"]'))), + wrap(html => requestHandlerApi.getContent(html.querySelector('meta[name="twitter:image"]'))), + wrap(html => requestHandlerApi.getContent(html.querySelector('meta[name="sailthru.image.thumb"]'))), + wrap(html => requestHandlerApi.getContent(html.querySelector('meta[name="sailthru.image.full"]'))), + wrap(html => requestHandlerApi.getContent(html.querySelector('meta[name="sailthru.image"]'))), + wrap(html => requestHandlerApi.getValue(html.querySelectorAll('article img[src]'), requestHandlerApi.getSrc)), + wrap(html => requestHandlerApi.getValue(html.querySelectorAll('#content img[src]'), requestHandlerApi.getSrc)), + wrap(html => requestHandlerApi.getSrc(html.querySelector('img[alt*="author"]'))), + wrap(html => requestHandlerApi.getSrc(html.querySelector('img[src]'))) + ] + }, + + getTitleRules: () => { + const wrap = rule => ({htmlDom}) => { + const value = rule(htmlDom) + return requestHandlerApi.isString(value) && requestHandlerApi.titleize(value) + } + + return [ + // Regular + wrap(html => requestHandlerApi.getContent(html.querySelector('meta[property="og:title"]'))), + wrap(html => requestHandlerApi.getContent(html.querySelector('meta[name="twitter:title"]'))), + wrap(html => requestHandlerApi.getContent(html.querySelector('meta[name="sailthru.title"]'))), + wrap(html => requestHandlerApi.getText(html.querySelector('.post-title'))), + wrap(html => requestHandlerApi.getText(html.querySelector('.entry-title'))), + wrap(html => requestHandlerApi.getText(html.querySelector('[itemtype="http://schema.org/BlogPosting"] [itemprop="name"]'))), + wrap(html => requestHandlerApi.getText(html.querySelector('h1[class*="title"] a'))), + wrap(html => requestHandlerApi.getText(html.querySelector('h1[class*="title"]'))), + wrap(html => requestHandlerApi.getText(html.querySelector('title'))) + ] + }, + + getAuthorRules: () => { + const wrap = rule => ({htmlDom}) => { + const value = rule(htmlDom) + + return requestHandlerApi.isString(value) && + !requestHandlerApi.isUrl(value, {relative: false}) && + requestHandlerApi.titleize(value, {removeBy: true}) + } + + return [ + // Youtube + wrap(html => requestHandlerApi.getText(html.querySelector('#owner-name'))), + wrap(html => requestHandlerApi.getText(html.querySelector('#channel-title'))), + wrap(html => requestHandlerApi.getValue(html.querySelectorAll('[class*="user-info"]'))), + // Regular + wrap(html => requestHandlerApi.getContent(html.querySelector('meta[property="author"]'))), + wrap(html => requestHandlerApi.getContent(html.querySelector('meta[property="article:author"]'))), + wrap(html => requestHandlerApi.getContent(html.querySelector('meta[name="author"]'))), + wrap(html => requestHandlerApi.getContent(html.querySelector('meta[name="sailthru.author"]'))), + wrap(html => requestHandlerApi.getValue(html.querySelectorAll('[rel="author"]'))), + wrap(html => requestHandlerApi.getValue(html.querySelectorAll('[itemprop*="author"] [itemprop="name"]'))), + wrap(html => requestHandlerApi.getValue(html.querySelectorAll('[itemprop*="author"]'))), + wrap(html => requestHandlerApi.getContent(html.querySelector('meta[property="book:author"]'))), + requestHandlerApi.strict(wrap(html => requestHandlerApi.getValue(html.querySelectorAll('a[class*="author"]')))), + requestHandlerApi.strict(wrap(html => requestHandlerApi.getValue(html.querySelectorAll('[class*="author"] a')))), + requestHandlerApi.strict(wrap(html => requestHandlerApi.getValue(html.querySelectorAll('a[href*="/author/"]')))), + wrap(html => requestHandlerApi.getValue(html.querySelectorAll('a[class*="screenname"]'))), + requestHandlerApi.strict(wrap(html => requestHandlerApi.getValue(html.querySelectorAll('[class*="author"]')))), + requestHandlerApi.strict(wrap(html => requestHandlerApi.getValue(html.querySelectorAll('[class*="byline"]')))) + ] + } +} diff --git a/app/extensions/brave/content/scripts/navigator.js b/app/extensions/brave/content/scripts/navigator.js index 261485ca4a2..02bf81fe8af 100644 --- a/app/extensions/brave/content/scripts/navigator.js +++ b/app/extensions/brave/content/scripts/navigator.js @@ -12,6 +12,11 @@ chrome.webFrame.setGlobal("navigator.getBattery", function () { return new Promise((resolve, reject) => { reject(new Error('navigator.getBattery not supported.')) }) }) +// bluetooth is not currently supported +executeScript("window.Navigator.prototype.__defineGetter__('bluetooth', () => { return undefined })") +// webusb also not supported yet +executeScript("window.Navigator.prototype.__defineGetter__('usb', () => { return undefined })") + if (chrome.contentSettings.doNotTrack == 'allow') { executeScript("window.Navigator.prototype.__defineGetter__('doNotTrack', () => { return 1 })") } diff --git a/app/extensions/brave/content/scripts/requestHandler.js b/app/extensions/brave/content/scripts/requestHandler.js new file mode 100644 index 00000000000..7cf87589305 --- /dev/null +++ b/app/extensions/brave/content/scripts/requestHandler.js @@ -0,0 +1,389 @@ +/* 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 ipc = chrome.ipcRenderer + +ipc.send('got-background-page-webcontents') +const domParser = new DOMParser() + +/** + * Takes a string and sanitizes it for HTML. + * This doesn't defend against other forms of code injection (for instance + * interpreting the input as js), so the input should still be considered + * untrusted. + * @param {string} input + * @returns {string} + */ +const sanitizeHtml = (input) => { + if (typeof input !== 'string') { + return '' + } + return input.replace(/([\s\n]*<[^>]*>[\s\n]*)+/g, ' ') +} + +ipc.on('fetch-publisher-info', (e, url, options) => { + let finalUrl = url + window.fetch(url, options).then((response) => { + finalUrl = response.url + return response.text() + }).then((text) => { + const html = domParser.parseFromString(text, 'text/html') + requestHandlerApi.getMetaData(html, url, finalUrl) + }).catch((err) => { + requestHandlerApi.onError(err, url, finalUrl) + }) +}) + +const requestHandlerApi = { + onError: (err, url, finalUrl) => { + console.error('fetch error', err) + ipc.send(`got-publisher-info-${url}`, { + error: err.message, + body: { + url: finalUrl + } + }) + }, + + getMetaData: async (htmlDom, url, finalUrl) => { + try { + const result = { + image: await requestHandlerApi.getData({ htmlDom, finalUrl, conditions: metaScraperRules.getImageRules() }), + title: await requestHandlerApi.getData({ htmlDom, finalUrl, conditions: metaScraperRules.getTitleRules() }), + author: await requestHandlerApi.getData({ htmlDom, finalUrl, conditions: metaScraperRules.getAuthorRules() }) + } + + ipc.send(`got-publisher-info-${url}`, { + error: null, + body: { + url: finalUrl, + title: sanitizeHtml(result.title) || '', + image: sanitizeHtml(result.image) || '', + author: sanitizeHtml(result.author) || '' + } + }) + } catch (err) { + requestHandlerApi.onError(err, url, finalUrl) + } + }, + + // Basic logic + getData: async ({htmlDom,url,conditions}) => { + const size = conditions.length + let index = -1 + let value + + while (!value && index++ < size - 1) { + value = await conditions[index]({htmlDom,url}) + } + + return value + }, + + // Helpers + getText: (node) => { + if (!node) { + return '' + } + + const html = (node.outerHTML || new XMLSerializer().serializeToString(node)) || '' + return sanitizeHtml(html) + }, + + urlCheck: (url) => { + try { + new URL(url) + return true + } catch (e) { + return false + } + }, + + getContent: (selector) => { + if (!selector) { + return null + } + + return selector.content + }, + + getSrc: (selector) => { + if (!selector) { + return null + } + + return selector.src + }, + + urlTest: (url, opts) => { + let relative + if (opts == null) { + relative = true + } else { + relative = opts.relative == null ? true : opts.relative + } + + return relative + ? requestHandlerApi.isAbsoluteUrl(url) === false || requestHandlerApi.urlCheck(url) + : requestHandlerApi.urlCheck(url) + }, + + isEmpty: (value) => { + return value == null || value.length === 0 + }, + + isUrl: (url, opts = {}) => { + return !requestHandlerApi.isEmpty(url) && requestHandlerApi.urlTest(url, opts) + }, + + getUrl: (baseUrl, relativePath = '') => { + return requestHandlerApi.isAbsoluteUrl(relativePath) === false + ? requestHandlerApi.resolveUrl(baseUrl, relativePath) + : relativePath + }, + + strict: rule => htmlDom => { + const value = rule(htmlDom) + return requestHandlerApi.isStrictString(value) + }, + + isStrictString: value => { + return /^\S+\s+\S+/.test(value) && value + }, + + titleize: (src, {removeBy = false} = {}) => { + if (!src) { + return '' + } + + let title = requestHandlerApi.createTitle(src) + if (removeBy) title = requestHandlerApi.removeByPrefix(title).trim() + return title + }, + + defaultFn: (el) => { + if (!el) { + return '' + } + + const text = requestHandlerApi.getText(el) || '' + return text.trim() + }, + + getValue: (collection, fn = requestHandlerApi.defaultFn) => { + if (!collection || !fn) { + return null + } + + if (!NodeList.prototype.isPrototypeOf(collection)) { + return fn(collection) + } + + for (const ele of collection) { + const value = fn(ele) + if (value) { + return value + } + } + + return null + }, + + getThumbnailUrl: (id) => { + if (id == null) { + return null + } + + return `https://img.youtube.com/vi/${id}/sddefault.jpg` + }, + + getVideoId: (str) => { + let metadata = {} + + if (typeof str !== 'string') { + return metadata + } + + // remove surrounding white spaces or line feeds + str = str.trim() + + // remove the '-nocookie' flag from youtube urls + str = str.replace('-nocookie', '') + + // remove any leading `www.` + str = str.replace('/www.', '/') + + if (/youtube|youtu\.be|i.ytimg\./.test(str)) { + metadata = { + id: requestHandlerApi.getYouTubeId(str), + service: 'youtube' + } + } + + return metadata + }, + + // https://github.com/radiovisual/get-video-id + getYouTubeId: (str) => { + if (str == null) { + return '' + } + + // short code + const shortCode = /youtube:\/\/|https?:\/\/youtu\.be\//g + if (shortCode.test(str)) { + const shortCodeId = str.split(shortCode)[1] + return requestHandlerApi.stripParameters(shortCodeId) + } + + // /v/ or /vi/ + const inlineV = /\/v\/|\/vi\//g + if (inlineV.test(str)) { + const inlineId = str.split(inlineV)[1] + return requestHandlerApi.stripParameters(inlineId) + } + + // v= or vi= + const parameterV = /v=|vi=/g + if (parameterV.test(str)) { + const arr = str.split(parameterV) + return arr[1].split('&')[0] + } + + // v= or vi= + const parameterWebP = /\/an_webp\//g + if (parameterWebP.test(str)) { + const webP = str.split(parameterWebP)[1] + return requestHandlerApi.stripParameters(webP) + } + + // embed + const embedReg = /\/embed\//g + if (embedReg.test(str)) { + const embedId = str.split(embedReg)[1] + return requestHandlerApi.stripParameters(embedId) + } + + // user + const userReg = /\/user\//g + if (userReg.test(str)) { + const elements = str.split('/') + return requestHandlerApi.stripParameters(elements.pop()) + } + + // attribution_link + const attrReg = /\/attribution_link\?.*v%3D([^%&]*)(%26|&|$)/ + if (attrReg.test(str)) { + return str.match(attrReg)[1] + } + }, + + stripParameters: (str) => { + if (str == null) { + return '' + } + + // Split parameters + if (str.includes('?')) { + return str.split('?')[0] + } + + // Split folder separator + if (str.includes('/')) { + return str.split('/')[0] + } + + return str + }, + + // https://github.com/kellym/smartquotesjs + getReplacements: () => { + return [ + // triple prime + [/'''/g, retainLength => '\u2034' + (retainLength ? '\u2063\u2063' : '')], + // beginning " + [/(\W|^)"(\w)/g, '$1\u201c$2'], + // ending " + [/(\u201c[^"]*)"([^"]*$|[^\u201c"]*\u201c)/g, '$1\u201d$2'], + // remaining " at end of word + [/([^0-9])"/g, '$1\u201d'], + // double prime as two single quotes + [/''/g, retainLength => '\u2033' + (retainLength ? '\u2063' : '')], + // beginning ' + [/(\W|^)'(\S)/g, '$1\u2018$2'], + // conjunction's possession + [/([a-z])'([a-z])/ig, '$1\u2019$2'], + // abbrev. years like '93 + [/(\u2018)([0-9]{2}[^\u2019]*)(\u2018([^0-9]|$)|$|\u2019[a-z])/ig, '\u2019$2$3'], + // ending ' + [/((\u2018[^']*)|[a-z])'([^0-9]|$)/ig, '$1\u2019$3'], + // backwards apostrophe + [/(\B|^)\u2018(?=([^\u2018\u2019]*\u2019\b)*([^\u2018\u2019]*\B\W[\u2018\u2019]\b|[^\u2018\u2019]*$))/ig, '$1\u2019'], + // double prime + [/"/g, '\u2033'], + // prime + [/'/g, '\u2032'] + ] + }, + + smartQuotes: (str) => { + const replacements = requestHandlerApi.getReplacements() + if (!replacements || !str) { + return '' + } + + replacements.forEach(replace => { + const replacement = typeof replace[1] === 'function' ? replace[1]({}) : replace[1] + str = str.replace(replace[0], replacement) + }) + + return str + }, + + removeByPrefix: (str = '') => { + if (str == null) { + return '' + } + + return str.replace(/^[\s\n]*by|@[\s\n]*/i, '').trim() + }, + + createTitle: (str = '') => { + if (str == null) { + return '' + } + + str = str.trim().replace(/\s{2,}/g, ' ') + return requestHandlerApi.smartQuotes(str) + }, + + // https://github.com/sindresorhus/is-absolute-url + isAbsoluteUrl: (url) => { + if (!requestHandlerApi.isString(url)) { + return + } + + return /^[a-z][a-z0-9+.-]*:/.test(url) + }, + + resolveUrl: (baseUrl, relativePath) => { + let url = baseUrl + + if (!relativePath) { + return url + } + + try { + url = new URL(relativePath, [baseUrl]) + } catch (e) {} + + return url + }, + + isString: (str) => { + return typeof str === 'string' + } +} + +if (module) module.exports = requestHandlerApi diff --git a/app/extensions/brave/content/scripts/themeColor.js b/app/extensions/brave/content/scripts/themeColor.js index 345b77b1bbe..71c5d034976 100644 --- a/app/extensions/brave/content/scripts/themeColor.js +++ b/app/extensions/brave/content/scripts/themeColor.js @@ -57,6 +57,16 @@ } if(window.top == window.self) { - chrome.ipcRenderer.sendToHost('theme-color-computed', computeThemeColor()) + if (document.visibilityState !== 'visible' && window.innerWidth === 0 && window.innerHeight === 0) { + const handleVisibilityChange = function() { + if (window.innerWidth !== 0 && window.innerHeight !== 0) { + window.removeEventListener('resize', handleVisibilityChange) + chrome.ipcRenderer.sendToHost('theme-color-computed', computeThemeColor()) + } + } + window.addEventListener('resize', handleVisibilityChange) + } else { + chrome.ipcRenderer.sendToHost('theme-color-computed', computeThemeColor()) + } } })() diff --git a/app/extensions/brave/content/styles/siteHack-marketwatch.com.css b/app/extensions/brave/content/styles/siteHack-marketwatch.com.css new file mode 100644 index 00000000000..03de69bfbe0 --- /dev/null +++ b/app/extensions/brave/content/styles/siteHack-marketwatch.com.css @@ -0,0 +1,7 @@ +.element--ad, +.container--bannerAd, +#brass-rail, +.ad.module, +iframe[src*="smartads.epl"] { + display: none !important; +} \ No newline at end of file diff --git a/app/extensions/brave/img/favicons/fireball.ico b/app/extensions/brave/img/favicons/fireball.ico new file mode 100644 index 00000000000..08cbac9b16b Binary files /dev/null and b/app/extensions/brave/img/favicons/fireball.ico differ diff --git a/app/extensions/brave/img/ledger/BAT_captcha_BG_arrow.png b/app/extensions/brave/img/ledger/BAT_captcha_BG_arrow.png new file mode 100644 index 00000000000..9f7eafd5d8f Binary files /dev/null and b/app/extensions/brave/img/ledger/BAT_captcha_BG_arrow.png differ diff --git a/app/extensions/brave/img/ledger/BAT_captcha_dragicon.png b/app/extensions/brave/img/ledger/BAT_captcha_dragicon.png new file mode 100644 index 00000000000..fc8c7199eb2 Binary files /dev/null and b/app/extensions/brave/img/ledger/BAT_captcha_dragicon.png differ diff --git a/app/extensions/brave/img/newtab/defaultTopSitesIcon/brave.ico b/app/extensions/brave/img/newtab/defaultTopSitesIcon/brave.ico index 374fb80899c..389d8d8f6f8 100644 Binary files a/app/extensions/brave/img/newtab/defaultTopSitesIcon/brave.ico and b/app/extensions/brave/img/newtab/defaultTopSitesIcon/brave.ico differ diff --git a/app/extensions/brave/img/newtab/defaultTopSitesIcon/facebook.png b/app/extensions/brave/img/newtab/defaultTopSitesIcon/facebook.png deleted file mode 100644 index 231076c247e..00000000000 Binary files a/app/extensions/brave/img/newtab/defaultTopSitesIcon/facebook.png and /dev/null differ diff --git a/app/extensions/brave/img/newtab/defaultTopSitesIcon/github.png b/app/extensions/brave/img/newtab/defaultTopSitesIcon/github.png new file mode 100644 index 00000000000..ea6ff545a24 Binary files /dev/null and b/app/extensions/brave/img/newtab/defaultTopSitesIcon/github.png differ diff --git a/app/extensions/brave/img/newtab/defaultTopSitesIcon/playstore.png b/app/extensions/brave/img/newtab/defaultTopSitesIcon/playstore.png index 5d6419829d0..29780b1ad07 100644 Binary files a/app/extensions/brave/img/newtab/defaultTopSitesIcon/playstore.png and b/app/extensions/brave/img/newtab/defaultTopSitesIcon/playstore.png differ diff --git a/app/extensions/brave/img/tabs/close_btn.svg b/app/extensions/brave/img/tabs/close_btn.svg deleted file mode 100644 index 926636ea455..00000000000 --- a/app/extensions/brave/img/tabs/close_btn.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/app/extensions/brave/locales/bn-BD/app.properties b/app/extensions/brave/locales/bn-BD/app.properties index ad64e12593c..85731c4ff9e 100644 --- a/app/extensions/brave/locales/bn-BD/app.properties +++ b/app/extensions/brave/locales/bn-BD/app.properties @@ -132,8 +132,7 @@ issuedBy=প্রদান করেছেন issuedTo=যার জন্য জারি করা হয়েছে ledgerBackupText1=Brave Wallet Recovery Keys ledgerBackupText2=Date created: -ledgerBackupText3=Recovery Key 1: -ledgerBackupText4=Recovery Key 2: +ledgerBackupText4=Recovery Key: ledgerBackupText5=Note: These keys are not stored on Brave servers. These keys are your only method of recovering your Brave wallet. Save these keys in a safe place, separate from your Brave browser. licenseTextOk=ঠিক আছে listOfAboutPages=About পাতা সমূহের তালিকা diff --git a/app/extensions/brave/locales/bn-IN/app.properties b/app/extensions/brave/locales/bn-IN/app.properties index f7847d4d1c5..23ab74db212 100644 --- a/app/extensions/brave/locales/bn-IN/app.properties +++ b/app/extensions/brave/locales/bn-IN/app.properties @@ -132,8 +132,7 @@ issuedBy=Issued By issuedTo=Issued To ledgerBackupText1=Brave Wallet Recovery Keys ledgerBackupText2=Date created: -ledgerBackupText3=Recovery Key 1: -ledgerBackupText4=Recovery Key 2: +ledgerBackupText4=Recovery Key: ledgerBackupText5=Note: These keys are not stored on Brave servers. These keys are your only method of recovering your Brave wallet. Save these keys in a safe place, separate from your Brave browser. licenseTextOk=Ok listOfAboutPages=পৃষ্ঠাগুলি সম্পর্কে এর তালিকা diff --git a/app/extensions/brave/locales/cs/app.properties b/app/extensions/brave/locales/cs/app.properties index e29a99cce48..4e9ab9d4be1 100644 --- a/app/extensions/brave/locales/cs/app.properties +++ b/app/extensions/brave/locales/cs/app.properties @@ -132,8 +132,7 @@ issuedBy=Vystaveno issuedTo=Vystaveno pro ledgerBackupText1=Klíče pro obnovení peněženky Brave ledgerBackupText2=Datum vytvoření: -ledgerBackupText3=Klíč pro obnovení 1: -ledgerBackupText4=Klíč pro obnovení 2: +ledgerBackupText4=Klíč pro obnovení: ledgerBackupText5=Poznámka: Tyto klíče nejsou uloženy na serverech Brave. Tyto klíče jsou vaším jediným způsobem, kterým můžete obnovit svoji peněženku Brave. Tyto klíče uložte na bezpečném místě, odděleně od prohlížeče Brave. licenseTextOk=OK listOfAboutPages=Seznam stránek s informacemi diff --git a/app/extensions/brave/locales/de-DE/app.properties b/app/extensions/brave/locales/de-DE/app.properties index 7f9f7599791..89bb0924db2 100644 --- a/app/extensions/brave/locales/de-DE/app.properties +++ b/app/extensions/brave/locales/de-DE/app.properties @@ -132,8 +132,7 @@ issuedBy=Ausgestellt von issuedTo=Ausgestellt an ledgerBackupText1=Brave Konto Wiederherstellungs-Schlüssel ledgerBackupText2=Erstellt am: -ledgerBackupText3=Wiederherstellungs-Schlüssel 1: -ledgerBackupText4=Wiederherstellungs-Schlüssel 2: +ledgerBackupText4=Wiederherstellungs-Schlüssel: ledgerBackupText5=Hinweis: Diese Schlüssel werden nicht auf den Servern von Brave gespeichert, sie dienen allein der Wiederherstellung Ihres Brave Kontos. Bewahren Sie die Schlüssel an einem Sicheren Ort, getrennt vom Brave Browser. licenseTextOk=Ok listOfAboutPages=Übersicht der 'about' Seiten diff --git a/app/extensions/brave/locales/en-GB/app.properties b/app/extensions/brave/locales/en-GB/app.properties index c55cb6537d7..15f5d2cb1b9 100644 --- a/app/extensions/brave/locales/en-GB/app.properties +++ b/app/extensions/brave/locales/en-GB/app.properties @@ -145,8 +145,7 @@ goToAdobe=Reinstall Flash allowFlashPlayer=Allow {{origin}} to run Flash Player? ledgerBackupText1=Brave Wallet Recovery Keys ledgerBackupText2=Date created: -ledgerBackupText3=Recovery Key 1: -ledgerBackupText4=Recovery Key 2: +ledgerBackupText4=Recovery Key: ledgerBackupText5=Note: These keys are not stored on Brave servers. These keys are your only method of recovering your Brave wallet. Save these keys in a safe place, separate from your Brave browser. allowWidevine=Allow {{origin}} to run Google Widevine? diff --git a/app/extensions/brave/locales/en-US/app.properties b/app/extensions/brave/locales/en-US/app.properties index 283a1de9e02..ef89a4df0b6 100644 --- a/app/extensions/brave/locales/en-US/app.properties +++ b/app/extensions/brave/locales/en-US/app.properties @@ -26,6 +26,8 @@ autocompleteData=Autocomplete data autofillData=Autofill data back=Back backButton.title=Go back +backupKeys=Let's backup your Brave wallet. It only takes a second... +backupKeysNow=Backup Now basicAuthMessage={{host}} requires a username and password. basicAuthPasswordLabel=Password basicAuthRequired=Authentication Required @@ -160,7 +162,7 @@ notificationTryPaymentsYes=Sure, I'll try notificationUpdatePassword=Would you like Brave to update your password on {{origin}}? notificationUpdatePasswordWithUserName=Would you like Brave to update the password for {{username}} on {{origin}}? notNow=Not Now -notVisiblePublisher.title=Publisher is not yet added to the ledger, because it doesn't meet criteria yet. +notVisiblePublisher.title=Publisher is not yet added to the ledger, click to include in Brave Payments. ok=OK openTypeInNewPrivateTab=Open {{type}} in a new private tab openTypeInNewTab=Open {{type}} in a new tab @@ -186,6 +188,7 @@ phone=Phone postalCode=Postal Code prefsRestart=Do you want to restart now? preventMoreAlerts=Prevent this page from creating additional dialogs +printKeysTitle=Backup keys reconciliationNotification=Good news! Brave will pay your favorite publisher sites in less than 24 hours. redirectedResources={{redirectedResourcesSize}} Resources upgraded to HTTPS releaseNotes=Release Notes @@ -260,10 +263,6 @@ versionInformation=Version Information videoCapturePermission=Video Capture viewCertificate=View Certificate viewPageSource=View Page Source -walletConvertedBackup=Back up your new wallet -walletConvertedDismiss=Later -walletConvertedLearnMore=Learn More -walletConvertedToBat=Your BTC will be converted to BAT and will appear in your Brave wallet within approximately 30 minutes. widevinePanelTitle=Brave needs to install Google Widevine to proceed windowCaptionButtonClose=Close windowCaptionButtonMaximize=Maximize diff --git a/app/extensions/brave/locales/en-US/menu.properties b/app/extensions/brave/locales/en-US/menu.properties index cdd0084f64b..dac4977850c 100644 --- a/app/extensions/brave/locales/en-US/menu.properties +++ b/app/extensions/brave/locales/en-US/menu.properties @@ -4,6 +4,7 @@ actualSize=Actual Size addBookmark=Add Bookmark… addFolder=Add Folder… addToFavoritesBar=Add to Favorites Bar +addToPublisherList=Include site in Brave Payments addToReadingList=Add to reading list allowFlashAlways=Allow for 1 week allowFlashOnce=Allow once @@ -45,6 +46,7 @@ cut=Cut delete=Delete deleteBookmark=Delete Bookmark deleteBookmarks=Delete Selected Bookmarks +deleteDomainFromHistory=Delete Domain from History deleteFolder=Delete Folder deleteHistoryEntries=Delete Selected History Entries deleteHistoryEntry=Delete History Entry @@ -91,6 +93,8 @@ learnSpelling=Learn Spelling licenseText=This software uses libraries from the FFmpeg project under the LGPLv2.1 lookupSelection=Look Up “{{selectedVariable}}” mergeAllWindows=Merge All Windows +messageBoxOk=ok +messageBoxCancel=cancel minimize=Minimize moveTabToNewWindow=Move to New Window muteOtherTabs=Mute other Tabs @@ -114,6 +118,7 @@ openInNewTabs=Open Links in New Tabs openInNewWindow=Open Link in New Window openLocation=Open Location… openSearch=Search for "{{selectedVariable}}" +otherBookmarks=Other Bookmarks passwordsManager=Passwords… paste=Paste pasteAndGo=Paste and Go diff --git a/app/extensions/brave/locales/en-US/passwords.properties b/app/extensions/brave/locales/en-US/passwords.properties index 4636ff8e3dc..81aa68660d3 100644 --- a/app/extensions/brave/locales/en-US/passwords.properties +++ b/app/extensions/brave/locales/en-US/passwords.properties @@ -1,5 +1,5 @@ clearPasswords=Delete all saved passwords -confirmClearPasswords=Are you sure you want to delete all passwords? This cannot be undone. +confirmClearPasswords=Are you sure you want to delete all saved passwords? This cannot be undone. deletePassword=Delete hidePassword=Hide noPasswordsSaved=No passwords have been saved. diff --git a/app/extensions/brave/locales/en-US/preferences.properties b/app/extensions/brave/locales/en-US/preferences.properties index 02b6dd5c8bc..a77a6f451ef 100644 --- a/app/extensions/brave/locales/en-US/preferences.properties +++ b/app/extensions/brave/locales/en-US/preferences.properties @@ -49,6 +49,7 @@ autoplay=Autoplay Media autoSuggestSites=auto-include backupLedger=Backup your wallet balanceRecovered={{balance}} was recovered and transferred to your Brave wallet. +banSiteConfirmation=Are you sure you want to delete this site? beta=beta bitcoin=Bitcoin bitcoinBalance=Please transfer:  @@ -75,6 +76,7 @@ clearAll=Clear all clearBrowsingDataNow=Clear Browsing Data Now… comingSoon=Coming soon! compactBraveryPanel=Use compact panel +confirmPaymentsClear=A Brave Payment contribution is in progress. Brave Payment data cannot be cleared during this time. contentSettings=Content Settings contributionAmount=Contribution Amount contributionDate=Contribution Date @@ -89,6 +91,11 @@ contributionTime=Contribution Time copied=Copied! copy=Copy copyToClipboard.title=Copy to clipboard +corruptedOverlayTitle=Hello! +corruptedOverlayMessage=Unfortunately your active wallet has been corrupted. +corruptedOverlayText=You must recover your backup wallet with your recovery keys before any transactions can be processed. We apologize for the inconvenience. +corruptedOverlayFAQ=View the Brave Payments FAQ… +corruptedOverlayButton=Recover your Brave Wallet createdWalletStatus=Your wallet is ready! createWallet=create wallet createWalletStatus=Click the Create Wallet button to get started. @@ -100,10 +107,11 @@ dashboardShowImages=Show images dashlane=Dashlane® (requires application) date=Date default=Default -default=Default defaultBrowser=Brave is your default browser. defaultWalletStatus=Thanks for helping support your favorite websites! defaultZoomLevel=Default zoom level +deletedSitesHeader=Deleted Sites +disableNonProxiedUdp=Disable non-proxied UDP disableTitleMode=Always show the URL bar disconnect=Disconnect dollarsPaid=Amount @@ -132,8 +140,7 @@ engineGoKey=Engine Go Key (Type First) engineGoKey=Engine Go Key (type first) enpass=Enpass® (requires application) extensions=Extensions -firstKey=Key 1 -firstRecoveryKey=Recovery Key 1 +expires=expires flash=Run Adobe Flash Player flashAllowAlways=Allow until {{time}} flashTroubleshooting=Flash not working? Try the troubleshooting tips on our @@ -158,17 +165,21 @@ lastPass=LastPass® ledgerBackupText1=Below, you will find the anonymized recovery key that is required if you ever lose access to this computer. ledgerBackupText2=Make sure you keep this key private, or else your wallet will be compromised. ledgerBackupTitle=Backup your Brave wallet -leaderLoaderText1=Please wait while we convert your Bitcoin to BAT. -leaderLoaderText2=This can take anywhere from a few minutes to several hours, due to the Bitcoin transaction confirmation process, and will continue normally even if you quit Brave. +ledgerFuzzed=You haven't accrued 30 minutes of browser activity yet so we pushed back the settlement date to give you more time. +ledgerNetworkErrorMessage=The Brave Payments server is not responding. We will fix this as soon as possible. +ledgerNetworkErrorTitle=Uh oh. +ledgerNetworkErrorText=Note: This error could also be caused by a network connection problem. ledgerPaymentsShown=Brave Payments -ledgerRecoveryContent=Your previous wallet will now be used. Your new wallet will be discarded. +ledgerRecoveryContent=Note: The recovered BAT wallet will replace the current BAT wallet, which will be discarded. ledgerRecoveryFailedMessage=Please re-enter keys or try different keys. ledgerRecoveryFailedTitle=Recovery Failed +ledgerRecoveryInProgress=Recovery in progress... +ledgerRecoveryInProgressTitle=Recovering ledgerRecoveryNetworkFailedMessage=Please check your internet connection and try again. ledgerRecoveryNetworkFailedTitle=Network Error -ledgerRecoverySubtitle=Enter your recovery key below +ledgerRecoverySubtitle=Enter your BAT wallet recovery key below: ledgerRecoverySucceeded=Success! -ledgerRecoveryTitle=Recover your Brave wallet +ledgerRecoveryTitle=Recover your Brave BAT wallet listOfContributionStatements=List of contribution statements locationBarSettings=Search Bar Options long=Long @@ -217,6 +228,9 @@ paintTabs=Show tabs in page theme color passwordManager=Password Manager passwordsAndForms=Passwords and Forms paymentsAllowPromotions=Notify me about token promotions +paymentsDeleteWallet=Delete wallet +paymentsDeleteWalletConfirmation=Are you sure that you want to delete your wallet? If you don't have your backup keys, your wallet will be lost forever. +paymentHistory=Brave Payments statements paymentHistoryDueFooterText=Your next contribution is due. paymentHistoryFooterText=Your next contribution is {{reconcileDate}}. paymentHistoryIcon.title=Your Payment History @@ -224,6 +238,7 @@ paymentHistoryOKText=OK paymentHistoryOverdueFooterText=Your contribution is overdue. paymentHistoryTitle=Your Payment History paymentHistoryTitle=Your Payment History +paymentInProgress=Currently processing payments=Payments paymentsFAQLink.title=View the FAQ paymentsSidebarText1=Our Partners @@ -249,6 +264,11 @@ printKeys=Print key privacy=Privacy privateData=Private Data privateDataMessage=Clear the following data types when I close Brave +promotionCaptchaTitle=Almost there! +promotionCaptchaErrorTitle=Hmmm…not quite. +promotionCaptchaErrorText=Please try again. +promotionCaptchaText=First, prove you are human: +promotionCaptchaMessage=Drag and drop the BAT logo onto the target promotionGeneralErrorMessage=The Brave Payments server is not responding. Please try again later to claim your token grant. promotionGeneralErrorText=Note: This error could also be caused by a network connection problem. promotionGeneralErrorTitle=Uh oh. @@ -256,10 +276,14 @@ promotionClaimedErrorMessage=The promotion has ended. promotionClaimedErrorText=There may be additional Basic Attention Token promotions in the future so stay tuned promotionClaimedErrorTitle=Sorry! protocolRegistrationPermission=Protocol registration +publicOnly=Default public interface only +publicPrivate=Default public and private interfaces publisher=Site +publishersClear=Brave Payments attention data publisherMediaName={{publisherName}} on {{provider}} publishers=Publishers rank=Rank +recalculatingBalance=Calculating balance... receiptLink=Receipt Link recover=Recover recoverFromFile=Import recovery key @@ -292,6 +316,7 @@ shieldsUp=All Brave Shields short=Short short=Short showAll=Show All +showDeletedSitesDialog=Show Deleted Sites showBookmarkMatches=Show bookmark matches showHistoryMatches=Show history matches showHomeButton=Show home button on URL bar @@ -337,7 +362,7 @@ syncHideQR=Hide QR code syncHistory=Browsing history syncNewDevice1=Open Brave on your new device and go to Preferences > Sync > 'I have an existing synced device'. syncNewDevice2=If it asks you to scan a QR code, click the button below and point your camera at the QR code. -syncNewDevice3=If asks you to enter code words, type in the words below. +syncNewDevice3=If it asks you to enter code words, type in the words below. syncNewDevice=Sync a new device… syncQRImg.title=Brave sync QR code syncReset=Reset Sync @@ -366,6 +391,7 @@ tabsSettings=Tabs Settings termsOfService=Terms of Service timeSpent=Time Spent toolbarUserInterfaceScale=Toolbar and UI elements scale +total=total totalAmount=Total Amount update=Update updateToPreviewReleases=Update to preview releases * @@ -375,12 +401,17 @@ useHardwareAcceleration=Use hardware acceleration when available * useSmoothScroll=Enable smooth scrolling * siteIsolation=Strict Site Isolation useSiteIsolation=Enhance security by loading each site in its own process (experimental) * +firewall=Application Firewall +useFirewall=Block external sites from connecting to private IP addresses, and only allow connections to private IP addresses with whitelisted hostnames (localhost, *.local, *.localhost). verifiedExplainerText= = publisher has verified their wallet viewLog=View Log viewPaymentHistory= {{date}} views=Views visit=visit visits=Visits +webrtcPolicy=WebRTC IP Handling Policy +webrtcPolicyExplanation=What do these policies mean? wideURLbar=Use wide URL bar widevine=Run Google Widevine widevineSection=Google Widevine Support +wordCount=Word Count: diff --git a/app/extensions/brave/locales/en-US/styles.properties b/app/extensions/brave/locales/en-US/styles.properties index b1ef50bb59f..d55675ca4ca 100644 --- a/app/extensions/brave/locales/en-US/styles.properties +++ b/app/extensions/brave/locales/en-US/styles.properties @@ -27,3 +27,5 @@ titles=Titles typography=Typography InfoBrowserButtonGrouped=To cancel the margin-left of the grouped buttons, they have to have a parent. Beginning with Selectors Level 4, it is no longer required. InfoCommonFormCustom=It is possible to customize the style of the components with this.props.custom. However, please consider to create a new component or enhance the existing one with props to maintain style consistency. +wordCount=Word Count: +copyToClipboard.title=Copy to clipboard diff --git a/app/extensions/brave/locales/es/app.properties b/app/extensions/brave/locales/es/app.properties index c876abca893..b76096f98ea 100644 --- a/app/extensions/brave/locales/es/app.properties +++ b/app/extensions/brave/locales/es/app.properties @@ -132,8 +132,7 @@ issuedBy=Emitido por issuedTo=Emitido para ledgerBackupText1=Claves de recuperación de cartera Brave ledgerBackupText2=Fecha de creación: -ledgerBackupText3=Clave de recuperación 1: -ledgerBackupText4=Clave de recuperación 2: +ledgerBackupText4=Clave de recuperación: ledgerBackupText5=Nota: Estas claves no están almacenadas en los servidores de Brave. Estas claves son la única manera de recuperar tu cartera Brave. Guarda estas claves en un lugar seguro, separado de tu navegador Brave. licenseTextOk=Ok listOfAboutPages=Lista de páginas de información diff --git a/app/extensions/brave/locales/eu/app.properties b/app/extensions/brave/locales/eu/app.properties index dae52961511..8a948239fd4 100644 --- a/app/extensions/brave/locales/eu/app.properties +++ b/app/extensions/brave/locales/eu/app.properties @@ -132,8 +132,7 @@ issuedBy=Honengatik bidalita issuedTo=Bidali hona ledgerBackupText1=Brave Wallet Recovery Keys ledgerBackupText2=Sortutako data: -ledgerBackupText3=Berreskuratzeko pasahitza 1: -ledgerBackupText4=Berreskuratzeko pasahitza 2: +ledgerBackupText4=Berreskuratzeko pasahitza: ledgerBackupText5=Note: These keys are not stored on Brave servers. These keys are your only method of recovering your Brave wallet. Save these keys in a safe place, separate from your Brave browser. licenseTextOk=Ados listOfAboutPages=Orrialdei buruzko zerrenda diff --git a/app/extensions/brave/locales/fr-FR/app.properties b/app/extensions/brave/locales/fr-FR/app.properties index 0badfd58965..76519eda91a 100644 --- a/app/extensions/brave/locales/fr-FR/app.properties +++ b/app/extensions/brave/locales/fr-FR/app.properties @@ -132,8 +132,7 @@ issuedBy=Émis par issuedTo=Émis à ledgerBackupText1=Clés de récupération du porte-monnaie Brave ledgerBackupText2=Date de création : -ledgerBackupText3=Clé de récupération N°1 : -ledgerBackupText4=Clé de récupération N°2 : +ledgerBackupText4=Clé de récupération: ledgerBackupText5=Note : Ces clés ne sont pas stockées sur les serveurs Brave. Ces clés constituent pour vous la seule méthode pour récupérer votre porte-monnaie Brave. Conservez ces clés dans un endroit sûr, en dehors de votre navigateur Brave. licenseTextOk=Ok listOfAboutPages=Liste des pages « À propos » diff --git a/app/extensions/brave/locales/hi-IN/app.properties b/app/extensions/brave/locales/hi-IN/app.properties index 949121c7c2b..1167dc1eb53 100644 --- a/app/extensions/brave/locales/hi-IN/app.properties +++ b/app/extensions/brave/locales/hi-IN/app.properties @@ -132,8 +132,7 @@ issuedBy=Issued By issuedTo=के लिए जारी किए ledgerBackupText1=Brave Wallet Recovery Keys ledgerBackupText2=Date created: -ledgerBackupText3=Recovery Key 1: -ledgerBackupText4=Recovery Key 2: +ledgerBackupText4=Recovery Key: ledgerBackupText5=Note: These keys are not stored on Brave servers. These keys are your only method of recovering your Brave wallet. Save these keys in a safe place, separate from your Brave browser. licenseTextOk=ठीक है listOfAboutPages=पृष्ठों के बारे में सूची diff --git a/app/extensions/brave/locales/id-ID/app.properties b/app/extensions/brave/locales/id-ID/app.properties index 130b080ef65..13fbffba53a 100644 --- a/app/extensions/brave/locales/id-ID/app.properties +++ b/app/extensions/brave/locales/id-ID/app.properties @@ -132,8 +132,7 @@ issuedBy=Diterbitkan oleh issuedTo=Diterbitkan untuk ledgerBackupText1=Kunci Pemulihan Dompet Brave ledgerBackupText2=Tanggal dibuat: -ledgerBackupText3=Kunci Pemulihan 1: -ledgerBackupText4=Kunci Pemulihan 2: +ledgerBackupText4=Kunci Pemulihan: ledgerBackupText5=Catatan: Kunci ini tidak disimpan di server Brave. Kunci ini adalah satu-satunya metode untuk memulihkan dompet Brave Anda. Simpan kunci tersebut di tempat yang aman, terpisah dari peramban Brave Anda. licenseTextOk=Oke listOfAboutPages=Daftar laman tentang diff --git a/app/extensions/brave/locales/it-IT/app.properties b/app/extensions/brave/locales/it-IT/app.properties index e29ffd4a06e..d5f586bff53 100644 --- a/app/extensions/brave/locales/it-IT/app.properties +++ b/app/extensions/brave/locales/it-IT/app.properties @@ -132,8 +132,7 @@ issuedBy=Rilasciato da issuedTo=Rilasciato a ledgerBackupText1=Chiavi di ripristino Brave Wallet ledgerBackupText2=Data di creazione: -ledgerBackupText3=Chiave di ripristino 1: -ledgerBackupText4=Chiave di ripristino 2: +ledgerBackupText4=Chiave di ripristino: ledgerBackupText5=Nota: queste chiavi non sono memorizzate sui server Brave. Queste chiavi sono il tuo unico modo per ripristinare il tuo Brave Wallet. Salva queste chiavi in un luogo sicuro e diverso dal tuo browser Brave. licenseTextOk=Ok listOfAboutPages=Lista delle pagine di informazioni diff --git a/app/extensions/brave/locales/ja-JP/app.properties b/app/extensions/brave/locales/ja-JP/app.properties index a4f0ba962ee..94779d4b1be 100644 --- a/app/extensions/brave/locales/ja-JP/app.properties +++ b/app/extensions/brave/locales/ja-JP/app.properties @@ -132,8 +132,7 @@ issuedBy=発行元 issuedTo=発行先 ledgerBackupText1=Braveウォレットのリカバリーキー ledgerBackupText2=作成日 -ledgerBackupText3=リカバリーキー 1 -ledgerBackupText4=リカバリーキー 2 +ledgerBackupText4=リカバリーキー ledgerBackupText5=これらのキーはBraveのサーバーには保存されていません。これらのキーがBraveウォレットを回復する唯一の方法なので、ブラウザとは別に、安全な場所に保存してください。 licenseTextOk=了解 listOfAboutPages=List of about pages diff --git a/app/extensions/brave/locales/ko-KR/app.properties b/app/extensions/brave/locales/ko-KR/app.properties index fec92b657b4..4f8063cb722 100644 --- a/app/extensions/brave/locales/ko-KR/app.properties +++ b/app/extensions/brave/locales/ko-KR/app.properties @@ -132,8 +132,7 @@ issuedBy=발급한 곳 issuedTo=발급받은 대상 ledgerBackupText1=Brave 지갑 복구 키 ledgerBackupText2=만든 날짜: -ledgerBackupText3=복구 키 1: -ledgerBackupText4=복구 키 2: +ledgerBackupText4=복구 키: ledgerBackupText5=참고: 이 키는 Brave 서버에 저장되지 않습니다. 이 키는 Brave 지갑을 복구하는 유일한 방법입니다. 안전한 곳에 이 키를 저장하고, Brave 브라우저와 분리하세요. licenseTextOk=확인 listOfAboutPages=페이지에 대한 목록 diff --git a/app/extensions/brave/locales/ms-MY/app.properties b/app/extensions/brave/locales/ms-MY/app.properties index 15f9dc3308c..075212b0802 100644 --- a/app/extensions/brave/locales/ms-MY/app.properties +++ b/app/extensions/brave/locales/ms-MY/app.properties @@ -132,8 +132,7 @@ issuedBy=Dikeluarkan Oleh issuedTo=Dikeluarkan Kepada ledgerBackupText1=Kunci Pengembalian Semula Dompet Brave ledgerBackupText2=Tarikh dicipta: -ledgerBackupText3=Kunci Pengembalian Semula 1 -ledgerBackupText4=Kunci Pengembalian Semula 2 +ledgerBackupText4=Kunci Pengembalian Semula ledgerBackupText5=Nota: Kunci ini tidak disimpan dalam pelayan Brave. Kunci ini adalah satu-satunya kaedah untuk anda mengembalikan semula dompet Brave anda. Simpan kunci ini di tempat yang selamat, selain daripada pelayar Brave anda. licenseTextOk=Ok listOfAboutPages=Senarai perihal halaman diff --git a/app/extensions/brave/locales/nl-NL/app.properties b/app/extensions/brave/locales/nl-NL/app.properties index da25164fe8a..832fc07ba4c 100644 --- a/app/extensions/brave/locales/nl-NL/app.properties +++ b/app/extensions/brave/locales/nl-NL/app.properties @@ -132,8 +132,7 @@ issuedBy=Uitgegeven Door issuedTo=Uitgegeven Aan ledgerBackupText1=Brave Wallet Herstelsleutels ledgerBackupText2=Datum aangemaakt: -ledgerBackupText3=Herstelsleutel 1: -ledgerBackupText4=Herstelsleutel 2: +ledgerBackupText4=Herstelsleutel: ledgerBackupText5=Let op: Deze sleutels worden niet bewaard op de Brave servers. Deze sleutels zijn de enige manier waarmee u uw Brave wallet kunt herstellen. Bewaar deze sleutels op een veilige plek, afzonderlijk van uw Brave browser. licenseTextOk=Ok listOfAboutPages=Lijst van over pagina's diff --git a/app/extensions/brave/locales/pl-PL/app.properties b/app/extensions/brave/locales/pl-PL/app.properties index f0bbbf380f4..d425998491a 100644 --- a/app/extensions/brave/locales/pl-PL/app.properties +++ b/app/extensions/brave/locales/pl-PL/app.properties @@ -132,8 +132,7 @@ issuedBy=Wydane przez issuedTo=Wystawiony dla ledgerBackupText1=Brave Wallet Recovery Keys ledgerBackupText2=Date created: -ledgerBackupText3=Recovery Key 1: -ledgerBackupText4=Recovery Key 2: +ledgerBackupText4=Recovery Key: ledgerBackupText5=Note: These keys are not stored on Brave servers. These keys are your only method of recovering your Brave wallet. Save these keys in a safe place, separate from your Brave browser. licenseTextOk=Ok listOfAboutPages=Lista stron informacyjnych diff --git a/app/extensions/brave/locales/pt-BR/app.properties b/app/extensions/brave/locales/pt-BR/app.properties index 0ebddb203ee..df850a22eb0 100644 --- a/app/extensions/brave/locales/pt-BR/app.properties +++ b/app/extensions/brave/locales/pt-BR/app.properties @@ -132,8 +132,7 @@ issuedBy=Emitido Por issuedTo=Emitido Para ledgerBackupText1=Chave de Recuperação da Carteira Brave ledgerBackupText2=Data de criação: -ledgerBackupText3=Chave de Recuperação 1 -ledgerBackupText4=Chave de Recuperação 2 +ledgerBackupText4=Chave de Recuperação ledgerBackupText5=Observação: essas chaves não são armazenadas nos servidores do Brave. Elas são o seu único método de recuperar sua carteira Brave. Salve as chaves em um lugar seguro e separado do seu navegador Brave. licenseTextOk=Ok listOfAboutPages=Lista de páginas Sobre diff --git a/app/extensions/brave/locales/ru/app.properties b/app/extensions/brave/locales/ru/app.properties index 9dc25534c22..af42a62531d 100644 --- a/app/extensions/brave/locales/ru/app.properties +++ b/app/extensions/brave/locales/ru/app.properties @@ -132,8 +132,7 @@ issuedBy=Кем выдан issuedTo=Кому выдан ledgerBackupText1=Ключи восстановления Brave кошелька ledgerBackupText2=Дата создания: -ledgerBackupText3=Ключ восстановления 1: -ledgerBackupText4=Ключ восстановления 2: +ledgerBackupText4=Ключ восстановления: ledgerBackupText5=Примечание: Эти ключи не хранятся на серверах Brave. Эти ключи являются единственным способом восстановления вашего Brave кошелька. Сохраните эти ключи в надежном месте отдельно от браузера Brave. licenseTextOk=Ok listOfAboutPages=Список информационных страниц diff --git a/app/extensions/brave/locales/sl/app.properties b/app/extensions/brave/locales/sl/app.properties index 1aba3f64275..65f7a4dab84 100644 --- a/app/extensions/brave/locales/sl/app.properties +++ b/app/extensions/brave/locales/sl/app.properties @@ -132,8 +132,7 @@ issuedBy=Izdajatelj issuedTo=Izdano ledgerBackupText1=Obnovitveni ključi denarnice Brave ledgerBackupText2=Datum izdelave: -ledgerBackupText3=Obnovitveni ključ 1: -ledgerBackupText4=Obnovitveni ključ 2: +ledgerBackupText4=Obnovitveni ključ: ledgerBackupText5=Opomba: Ti ključi niso shranjeni na strežnikih Brave. Tovrstni ključi so edini način, da obnovite svojo denarnico Brave. Shranite jih na varno mesto, ločeno od svojega brskalnika Brave. licenseTextOk=V redu listOfAboutPages=Seznam strani o ... diff --git a/app/extensions/brave/locales/sv-SE/app.properties b/app/extensions/brave/locales/sv-SE/app.properties index b089003ec3c..525c8f791cb 100644 --- a/app/extensions/brave/locales/sv-SE/app.properties +++ b/app/extensions/brave/locales/sv-SE/app.properties @@ -132,8 +132,7 @@ issuedBy=Utfärdats Av issuedTo=Utfärdats Till ledgerBackupText1=Brave plånbokens återställningsnycklar ledgerBackupText2=Datum skapat: -ledgerBackupText3=Återställningsnyckel 1: -ledgerBackupText4=Återställningsnyckel 2: +ledgerBackupText4=Återställningsnyckel: ledgerBackupText5=Obs: Dessa knappar lagras inte på Brave servrar. Dessa koder är ditt enda sätt att få tillbaka din Brave plånbok. Spara dessa nycklar på ett säkert ställe, inte tillsammans med Brave webbläsare. licenseTextOk=Ok listOfAboutPages=Lista över "about" sidor diff --git a/app/extensions/brave/locales/ta/app.properties b/app/extensions/brave/locales/ta/app.properties index 91f02bc7c01..1b776db4a41 100644 --- a/app/extensions/brave/locales/ta/app.properties +++ b/app/extensions/brave/locales/ta/app.properties @@ -132,8 +132,7 @@ issuedBy=ஆல் வழங்கப்பட்டது issuedTo=வழங்கப்பட்டது ledgerBackupText1=Brave Wallet Recovery Keys ledgerBackupText2=Date created: -ledgerBackupText3=Recovery Key 1: -ledgerBackupText4=Recovery Key 2: +ledgerBackupText4=Recovery Key: ledgerBackupText5=Note: These keys are not stored on Brave servers. These keys are your only method of recovering your Brave wallet. Save these keys in a safe place, separate from your Brave browser. licenseTextOk=Ok listOfAboutPages=பற்றிய பக்கங்கள் பட்டியல் diff --git a/app/extensions/brave/locales/te/app.properties b/app/extensions/brave/locales/te/app.properties index 2bf351d5162..ef4a546d79d 100644 --- a/app/extensions/brave/locales/te/app.properties +++ b/app/extensions/brave/locales/te/app.properties @@ -132,8 +132,7 @@ issuedBy=Issued By issuedTo=Issued To ledgerBackupText1=Brave Wallet Recovery Keys ledgerBackupText2=Date created: -ledgerBackupText3=Recovery Key 1: -ledgerBackupText4=Recovery Key 2: +ledgerBackupText4=Recovery Key: ledgerBackupText5=Note: These keys are not stored on Brave servers. These keys are your only method of recovering your Brave wallet. Save these keys in a safe place, separate from your Brave browser. licenseTextOk=Ok listOfAboutPages=పేజిల గురించి జాబితా diff --git a/app/extensions/brave/locales/tr-TR/app.properties b/app/extensions/brave/locales/tr-TR/app.properties index 2bb336fd336..72c41c58367 100644 --- a/app/extensions/brave/locales/tr-TR/app.properties +++ b/app/extensions/brave/locales/tr-TR/app.properties @@ -132,8 +132,7 @@ issuedBy=Issued By issuedTo=Adına Yayınlanan ledgerBackupText1=Brave Wallet Recovery Keys ledgerBackupText2=Date created: -ledgerBackupText3=Recovery Key 1: -ledgerBackupText4=Recovery Key 2: +ledgerBackupText4=Recovery Key: ledgerBackupText5=Note: These keys are not stored on Brave servers. These keys are your only method of recovering your Brave wallet. Save these keys in a safe place, separate from your Brave browser. licenseTextOk=Tamam listOfAboutPages=Sayfalar Hakkında Listesi diff --git a/app/extensions/brave/locales/uk/app.properties b/app/extensions/brave/locales/uk/app.properties index 02005e24074..48de4505a90 100644 --- a/app/extensions/brave/locales/uk/app.properties +++ b/app/extensions/brave/locales/uk/app.properties @@ -132,8 +132,7 @@ issuedBy=Випущено issuedTo=Випущено для ledgerBackupText1=Ключі відновлення гаманця Brave ledgerBackupText2=Дата створення: -ledgerBackupText3=Ключ відновлення 1: -ledgerBackupText4=Ключ відновлення 2: +ledgerBackupText4=Ключ відновлення: ledgerBackupText5=Примітка: Ці ключі не зберігаються на серверах Brave. Вони є лише вашим засобом відновлення гаманця Brave. Збережіть ці ключі в надійне місце, окремо від браузера Brave. licenseTextOk=Ok listOfAboutPages=Список інформаційних сторінок diff --git a/app/extensions/brave/locales/zh-CN/app.properties b/app/extensions/brave/locales/zh-CN/app.properties index 7d5eff50998..b70998c6182 100644 --- a/app/extensions/brave/locales/zh-CN/app.properties +++ b/app/extensions/brave/locales/zh-CN/app.properties @@ -132,8 +132,7 @@ issuedBy=由...发布 issuedTo=发布给 ledgerBackupText1=Brave 钱包恢复秘钥 ledgerBackupText2=创建日期: -ledgerBackupText3=恢复秘钥 1: -ledgerBackupText4=恢复秘钥 2: +ledgerBackupText4=恢复秘钥 : ledgerBackupText5=请注意:上述秘钥未储存于 Brave 服务器上。秘钥是您恢复 Brave 钱包的唯一方式。请将秘钥保存于安全位置,与 Brave 浏览器分离开。 licenseTextOk=确定 listOfAboutPages=关于页面列表 diff --git a/app/filtering.js b/app/filtering.js index 5b99063a90f..9e873b2eb24 100644 --- a/app/filtering.js +++ b/app/filtering.js @@ -115,12 +115,21 @@ function registerForBeforeRequest (session, partition) { } const firstPartyUrl = module.exports.getMainFrameUrl(details) + const url = details.url // this can happen if the tab is closed and the webContents is no longer available if (!firstPartyUrl) { muonCb({ cancel: true }) return } + if (!isPrivate && module.exports.isResourceEnabled('ledger') && module.exports.isResourceEnabled('ledgerMedia')) { + // Ledger media + const provider = ledgerUtil.getMediaProvider(url, firstPartyUrl, details.referrer) + if (provider) { + appActions.onLedgerMediaData(url, provider, details) + } + } + for (let i = 0; i < beforeRequestFilteringFns.length; i++) { let results = beforeRequestFilteringFns[i](details, isPrivate) const isAdBlock = (results.resourceName === appConfig.resourceNames.ADBLOCK) || @@ -201,7 +210,6 @@ function registerForBeforeRequest (session, partition) { } } // Redirect to non-script version of DDG when it's blocked - const url = details.url if (details.resourceType === 'mainFrame' && url.startsWith('https://duckduckgo.com/?q') && module.exports.isResourceEnabled('noScript', url, isPrivate)) { @@ -209,14 +217,6 @@ function registerForBeforeRequest (session, partition) { } else { muonCb({}) } - - if (module.exports.isResourceEnabled('ledger') && module.exports.isResourceEnabled('ledgerMedia')) { - // Ledger media - const provider = ledgerUtil.getMediaProvider(url) - if (provider) { - appActions.onLedgerMediaData(url, provider, details.tabId) - } - } }) } @@ -245,12 +245,13 @@ function registerForBeforeRedirect (session, partition) { module.exports.applyCookieSetting = (requestHeaders, url, firstPartyUrl, isPrivate) => { const cookieSetting = module.exports.isResourceEnabled(appConfig.resourceNames.COOKIEBLOCK, firstPartyUrl, isPrivate) if (cookieSetting) { - const parsedTargetUrl = urlParse(url || '') - const parsedFirstPartyUrl = urlParse(firstPartyUrl) + const targetHostname = urlParse(url || '').hostname + const firstPartyHostname = urlParse(firstPartyUrl).hostname const targetOrigin = getOrigin(url) + const referer = requestHeaders['Referer'] if (cookieSetting === 'blockAllCookies' || - isThirdPartyHost(parsedFirstPartyUrl.hostname, parsedTargetUrl.hostname)) { + isThirdPartyHost(firstPartyHostname, targetHostname)) { let hasCookieException = false const firstPartyOrigin = getOrigin(firstPartyUrl) if (cookieExceptions.hasOwnProperty(firstPartyOrigin)) { @@ -268,20 +269,19 @@ module.exports.applyCookieSetting = (requestHeaders, url, firstPartyUrl, isPriva } } } - // Clear cookie and referer on third-party requests + + // Clear cookie on third-party requests if (requestHeaders['Cookie'] && firstPartyOrigin !== pdfjsOrigin && !hasCookieException) { requestHeaders['Cookie'] = undefined } } - const referer = requestHeaders['Referer'] if (referer && cookieSetting !== 'allowAllCookies' && - !refererExceptions.includes(parsedTargetUrl.hostname) && - targetOrigin !== getOrigin(referer)) { - // Unless the setting is 'allow all cookies', spoof the referer if it - // is a cross-origin referer + !refererExceptions.includes(targetHostname) && + isThirdPartyHost(targetHostname, urlParse(referer).hostname)) { + // Spoof third party referer requestHeaders['Referer'] = targetOrigin } } @@ -364,6 +364,18 @@ function registerForHeadersReceived (session, partition) { muonCb({ cancel: true }) return } + + let parsedTargetUrl = urlParse(details.url || '') + let parsedFirstPartyUrl = urlParse(firstPartyUrl) + const trackableSecurityHeaders = ['Strict-Transport-Security', 'Expect-CT', + 'Public-Key-Pins', 'Public-Key-Pins-Report-Only'] + if (isThirdPartyHost(parsedFirstPartyUrl.hostname, parsedTargetUrl.hostname)) { + trackableSecurityHeaders.forEach(function (header) { + delete details.responseHeaders[header] + delete details.responseHeaders[header.toLowerCase()] + }) + } + for (let i = 0; i < headersReceivedFilteringFns.length; i++) { let results = headersReceivedFilteringFns[i](details, isPrivate) if (!module.exports.isResourceEnabled(results.resourceName, firstPartyUrl, isPrivate)) { @@ -381,7 +393,10 @@ function registerForHeadersReceived (session, partition) { return } } - muonCb({}) + muonCb({ + responseHeaders: details.responseHeaders, + statusLine: details.statusLine + }) }) } @@ -394,7 +409,7 @@ function registerPermissionHandler (session, partition) { const isPrivate = module.exports.isPrivate(partition) // Keep track of per-site permissions granted for this session. let permissions = null - session.setPermissionRequestHandler((origin, mainFrameUrl, permissionTypes, muonCb) => { + session.setPermissionRequestHandler((mainFrameOrigin, requestingUrl, permissionTypes, muonCb) => { if (!permissions) { permissions = { media: { @@ -427,35 +442,11 @@ function registerPermissionHandler (session, partition) { // TODO(bridiver) - the permission handling should be converted to an action because we should never call `appStore.getState()` // Check whether there is a persistent site setting for this host const appState = appStore.getState() - const isBraveOrigin = origin.startsWith(`chrome-extension://${config.braveExtensionId}/`) - const isPDFOrigin = origin.startsWith(`${pdfjsOrigin}/`) - let settings - let tempSettings - if (mainFrameUrl === appUrlUtil.getBraveExtIndexHTML() || isPDFOrigin || isBraveOrigin) { - // lookup, display and store site settings by the origin alias - origin = isPDFOrigin ? 'PDF Viewer' : 'Brave Browser' - // display on all tabs - mainFrameUrl = null - // Lookup by exact host pattern match since 'Brave Browser' is not - // a parseable URL - settings = siteSettings.getSiteSettingsForHostPattern(appState.get('siteSettings'), origin) - tempSettings = siteSettings.getSiteSettingsForHostPattern(appState.get('temporarySiteSettings'), origin) - } else if (mainFrameUrl.startsWith('magnet:')) { - // Show "Allow magnet URL to open an external application?", instead of - // "Allow null to open an external application?" - // This covers an edge case where you open a magnet link tab, then disable Torrent Viewer - // and restart Brave. I don't think it needs localization. See 'Brave Browser' above. - origin = 'Magnet URL' - } else { - // Strip trailing slash - origin = getOrigin(origin) - settings = siteSettings.getSiteSettingsForURL(appState.get('siteSettings'), origin) - tempSettings = siteSettings.getSiteSettingsForURL(appState.get('temporarySiteSettings'), origin) - } - + const isBraveOrigin = mainFrameOrigin.startsWith(`chrome-extension://${config.braveExtensionId}/`) + const isPDFOrigin = mainFrameOrigin.startsWith(`${pdfjsOrigin}/`) let response = [] - if (origin == null) { + if (!requestingUrl) { response = new Array(permissionTypes.length) response.fill(false, 0, permissionTypes.length) muonCb(response) @@ -466,17 +457,42 @@ function registerPermissionHandler (session, partition) { const responseSizeThisIteration = response.length const permission = permissionTypes[i] const alwaysAllowFullscreen = module.exports.alwaysAllowFullscreen() === fullscreenOption.ALWAYS_ALLOW + const isFullscreen = permission === 'fullscreen' + const isOpenExternal = permission === 'openExternal' + + let requestingOrigin + + if (requestingUrl === appUrlUtil.getBraveExtIndexHTML() || isPDFOrigin || isBraveOrigin) { + // lookup, display and store site settings by the origin alias + requestingOrigin = isPDFOrigin ? 'PDF Viewer' : 'Brave Browser' + // display on all tabs + mainFrameOrigin = null + } else if (isOpenExternal) { + // Open external is a special case since we want to apply the permission + // for the entire scheme to avoid cluttering the saved permissions. See + // https://github.com/brave/browser-laptop/issues/13642 + const protocol = urlParse(requestingUrl).protocol + requestingOrigin = protocol ? `${protocol} URLs` : requestingUrl + } else { + requestingOrigin = getOrigin(requestingUrl) || requestingUrl + } + + // Look up by host pattern since requestingOrigin is not necessarily + // a parseable URL + const settings = siteSettings.getSiteSettingsForHostPattern(appState.get('siteSettings'), requestingOrigin) + const tempSettings = siteSettings.getSiteSettingsForHostPattern(appState.get('temporarySiteSettings'), requestingOrigin) + if (!permissions[permission]) { console.warn('WARNING: got unregistered permission request', permission) response.push(false) - } else if (permission === 'fullscreen' && + } else if (isFullscreen && mainFrameOrigin && // The Torrent Viewer extension is always allowed to show fullscreen media - origin.startsWith('chrome-extension://' + config.torrentExtensionId)) { + mainFrameOrigin.startsWith('chrome-extension://' + config.torrentExtensionId)) { response.push(true) - } else if (permission === 'fullscreen' && alwaysAllowFullscreen) { + } else if (isFullscreen && alwaysAllowFullscreen) { // Always allow fullscreen if setting is ON response.push(true) - } else if (permission === 'openExternal' && ( + } else if (isOpenExternal && ( // The Brave extension and PDFJS are always allowed to open files in an external app isPDFOrigin || isBraveOrigin)) { response.push(true) @@ -495,9 +511,7 @@ function registerPermissionHandler (session, partition) { } } - // Display 'Brave Browser' if the origin is null; ex: when a mailto: link - // is opened in a new tab via right-click - const message = locale.translation('permissionMessage').replace(/{{\s*host\s*}}/, origin || 'Brave Browser').replace(/{{\s*permission\s*}}/, permissions[permission].action) + const message = locale.translation('permissionMessage').replace(/{{\s*host\s*}}/, requestingOrigin).replace(/{{\s*permission\s*}}/, permissions[permission].action) // If this is a duplicate, clear the previous callback and use the new one if (permissionCallbacks[message]) { @@ -511,9 +525,9 @@ function registerPermissionHandler (session, partition) { {text: locale.translation('deny')}, {text: locale.translation('allow')} ], - frameOrigin: getOrigin(mainFrameUrl), + frameOrigin: getOrigin(mainFrameOrigin), options: { - persist: !!origin, + persist: !!requestingOrigin, index: i }, message @@ -530,7 +544,7 @@ function registerPermissionHandler (session, partition) { response[index] = result if (persist) { // remember site setting for this host - appActions.changeSiteSetting(origin, permission + 'Permission', result, isPrivate) + appActions.changeSiteSetting(requestingOrigin, permission + 'Permission', result, isPrivate) } if (response.length === permissionTypes.length) { permissionCallbacks[message] = null @@ -582,12 +596,6 @@ function registerForDownloadListener (session) { const hostWebContents = webContents.hostWebContents || webContents const win = BrowserWindow.fromWebContents(hostWebContents) || BrowserWindow.getFocusedWindow() - // TODO(bridiver) - move this fix to muon - const controller = webContents.controller() - if (controller && controller.isValid() && controller.isInitialNavigation()) { - webContents.forceClose() - } - item.setPrompt(getSetting(settings.DOWNLOAD_ALWAYS_ASK) || false) const downloadId = item.getGuid() @@ -776,13 +784,9 @@ module.exports.isResourceEnabled = (resourceName, url, isPrivate) => { if (resourceName === 'pdfjs') { return getSetting(settings.PDFJS_ENABLED, settingsState) } - if (resourceName === 'webtorrent') { - return getSetting(settings.TORRENT_VIEWER_ENABLED, settingsState) - } if (resourceName === 'webtorrent') { - const extension = extensionState.getExtensionById(appState, config.torrentExtensionId) - return extension !== undefined ? extension.get('enabled') : false + return extensionState.isWebTorrentEnabled(appState) } if (resourceName === 'ledger') { @@ -793,6 +797,10 @@ module.exports.isResourceEnabled = (resourceName, url, isPrivate) => { return getSetting(settings.PAYMENTS_ALLOW_MEDIA_PUBLISHERS, settingsState) } + if (resourceName === 'firewall') { + return siteSettings.braveryDefaults(appState, appConfig).firewall + } + const braverySettings = getBraverySettingsForUrl(url, appState, isPrivate) // If full shields are down never enable extra protection @@ -845,6 +853,15 @@ module.exports.clearStorageData = () => { } } +module.exports.clearHSTSData = () => { + for (let partition in registeredSessions) { + let ses = registeredSessions[partition] + setImmediate(() => { + ses.clearHSTSData.bind(ses)(() => {}) + }) + } +} + /** * Clears all session caches. */ diff --git a/app/firewall.js b/app/firewall.js new file mode 100644 index 00000000000..07b22d7f1ae --- /dev/null +++ b/app/firewall.js @@ -0,0 +1,32 @@ +/* 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 ip = require('ip') +const Filtering = require('./filtering') +const {isInternalUrl} = require('../js/lib/urlutil') + +module.exports.resourceName = 'firewall' + +const onHeadersReceived = (details) => { + const result = { resourceName: module.exports.resourceName } + const mainFrameUrl = Filtering.getMainFrameUrl(details) + const isIPInternal = ip.isPrivate(details.ip) + const isUrlInternal = isInternalUrl(details.url) + + if ((isIPInternal || isUrlInternal) && !isInternalUrl(mainFrameUrl)) { + // Block requests to local origins from non-local top-level origins + console.log('firewall blocked request from external IP to internal IP') + result.cancel = true + } else if (isIPInternal && !isUrlInternal) { + // Block requests to an external name that resolves to an internal address + console.log('firewall blocked request for internal IP with external hostname') + result.cancel = true + } + + return result +} + +module.exports.init = () => { + Filtering.registerHeadersReceivedFilteringCB(onHeadersReceived) +} diff --git a/app/index.js b/app/index.js index 597f218585a..dbc444a1787 100644 --- a/app/index.js +++ b/app/index.js @@ -16,7 +16,7 @@ const telemetry = require('./telemetry') telemetry.setCheckpoint('init') const handleUncaughtError = (stack, message) => { - muon.crashReporter.setCrashKeyValue('javascript-info', JSON.stringify({stack, message})) + muon.crashReporter.setJavascriptInfoCrashValue(JSON.stringify({stack, message})) muon.crashReporter.dumpWithoutCrashing() if (!ready) { @@ -64,6 +64,7 @@ const updater = require('./updater') const Importer = require('./importer') const messages = require('../js/constants/messages') const appActions = require('../js/actions/appActions') +const tabActions = require('./common/actions/tabActions') const SessionStore = require('./sessionStore') const {startSessionSaveInterval} = require('./sessionStoreShutdown') const appStore = require('../js/stores/appStore') @@ -73,6 +74,7 @@ const TrackingProtection = require('./trackingProtection') const AdBlock = require('./adBlock') const AdInsertion = require('./browser/ads/adInsertion') const HttpsEverywhere = require('./httpsEverywhere') +const Firewall = require('./firewall') const PDFJS = require('./pdfJS') const SiteHacks = require('./siteHacks') const CmdLine = require('./cmdLine') @@ -141,15 +143,17 @@ const notifyCertError = (webContents, url, error, cert) => { fingerprint: cert.fingerprint } - // Tell the page to show an unlocked icon. Note this is sent to the main - // window webcontents, not the webview webcontents - let sender = webContents.hostWebContents || webContents - sender.send(messages.CERT_ERROR, { + // Load the certificate error page + // and provide details about the error, + // including enough data for in-page actions to force the + // insecure page to load or show the certificate. + tabActions.setContentsError(webContents.getId(), { url, error, cert, tabId: webContents.getId() }) + appActions.loadURLRequested(webContents.getId(), 'about:certerror') } app.on('ready', () => { @@ -202,12 +206,14 @@ app.on('ready', () => { AdBlock.init() AdInsertion.init() PDFJS.init() + Firewall.init() if (!loadedPerWindowImmutableState || loadedPerWindowImmutableState.size === 0) { if (!CmdLine.newWindowURL()) { appActions.newWindow() } } else { + const lastIndex = loadedPerWindowImmutableState.size - 1 loadedPerWindowImmutableState .sort((a, b) => { let comparison = 0 @@ -222,8 +228,12 @@ app.on('ready', () => { return comparison }) - .forEach((wndState) => { - appActions.newWindow(undefined, undefined, wndState) + .forEach((wndState, i) => { + const isLastWindow = i === lastIndex + if (CmdLine.shouldDebugWindowEvents && isLastWindow) { + console.log(`The restored window which should get focus has ${wndState.get('frames').size} frames`) + } + appActions.newWindow(undefined, isLastWindow ? undefined : { inactive: true }, wndState, true) }) } process.emit(messages.APP_INITIALIZED) diff --git a/app/locale.js b/app/locale.js index c5ef2a5b2c5..13c43012a6d 100644 --- a/app/locale.js +++ b/app/locale.js @@ -60,12 +60,15 @@ var rendererIdentifiers = function () { 'deleteBookmarks', 'deleteHistoryEntry', 'deleteHistoryEntries', + 'deleteDomainFromHistory', 'deleteLedgerEntry', 'ledgerBackupText1', 'ledgerBackupText2', 'ledgerBackupText3', 'ledgerBackupText4', 'ledgerBackupText5', + 'backupKeys', + 'backupKeysNow', 'editFolder', 'editBookmark', 'unmuteTabs', @@ -133,6 +136,7 @@ var rendererIdentifiers = function () { 'recentlyClosed', 'recentlyVisited', 'bookmarks', + 'otherBookmarks', 'addToFavoritesBar', 'window', 'minimize', @@ -177,6 +181,7 @@ var rendererIdentifiers = function () { 'forgetLearnedSpelling', 'lookupSelection', 'publisherMediaName', + 'addToPublisherList', // Other identifiers 'aboutBlankTitle', 'urlCopied', @@ -226,13 +231,13 @@ var rendererIdentifiers = function () { 'no', 'noThanks', 'neverForThisSite', - 'walletConvertedBackup', - 'walletConvertedDismiss', - 'walletConvertedLearnMore', - 'walletConvertedToBat', 'dappDetected', 'dappDismiss', 'dappEnableExtension', + 'banSiteConfirmation', + 'paymentsDeleteWalletConfirmation', + 'messageBoxOk', + 'messageBoxCancel', // other 'passwordsManager', 'extensionsManager', @@ -283,7 +288,13 @@ var rendererIdentifiers = function () { 'promotionGeneralErrorText', 'promotionClaimedErrorMessage', 'promotionClaimedErrorText', - 'promotionClaimedErrorTitle' + 'promotionClaimedErrorTitle', + 'corruptedOverlayTitle', + 'corruptedOverlayMessage', + 'corruptedOverlayText', + 'ledgerNetworkErrorTitle', + 'ledgerNetworkErrorMessage', + 'ledgerNetworkErrorText' ].concat(countryCodes).concat(availableLanguages) } diff --git a/app/migrations/20180518_uphold.js b/app/migrations/20180518_uphold.js new file mode 100644 index 00000000000..044caa2b8ad --- /dev/null +++ b/app/migrations/20180518_uphold.js @@ -0,0 +1,39 @@ +/* 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 compareVersions = require('compare-versions') + +// per https://github.com/brave/browser-laptop/issues/14152 +// add fingerprint exception for existing users for uphold.com +module.exports = (data) => { + // don't apply if: + // - user chooses to block all fingerprinting (global setting) + // - user is not upgrading from 0.22.714 or earlier + if ((data.fingerprintingProtectionAll && data.fingerprintingProtectionAll.enabled) || + !data.lastAppVersion) { + return false + } + + let migrationNeeded = false + + try { + migrationNeeded = compareVersions(data.lastAppVersion, '0.22.714') !== 1 + } catch (e) {} + + if (migrationNeeded) { + const pattern = 'https?://uphold.com' + if (!data.siteSettings) { + data.siteSettings = {} + } + if (!data.siteSettings[pattern]) { + data.siteSettings[pattern] = {} + } + let targetSetting = data.siteSettings[pattern] + if (targetSetting.fingerprintingProtection == null) { + targetSetting.fingerprintingProtection = 'allowAllFingerprinting' + } + } + + return migrationNeeded +} diff --git a/app/migrations/pre.js b/app/migrations/pre.js new file mode 100644 index 00000000000..d9486a8148c --- /dev/null +++ b/app/migrations/pre.js @@ -0,0 +1,14 @@ +/* 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/. */ + +module.exports = (data) => { + let migrations = [ + require('./20180518_uphold') + // TODO: put additional migrations here + ] + + migrations.forEach((migration) => { + migration(data) + }) +} diff --git a/app/renderer/about/ledger/printKeys.js b/app/renderer/about/ledger/printKeys.js new file mode 100644 index 00000000000..beea3c99291 --- /dev/null +++ b/app/renderer/about/ledger/printKeys.js @@ -0,0 +1,63 @@ +/* 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 format = require('date-fns/format') +const {StyleSheet, css} = require('aphrodite/no-important') +const ipc = window.chrome.ipcRenderer + +// Constants +const messages = require('../../../../js/constants/messages') + +class PrintKeys extends React.Component { + constructor (props) { + super(props) + this.state = { + passphrase: '' + } + + ipc.on(messages.PRINTKEYS_UPDATED, (e, detail) => { + if (detail) { + this.setState({ + passphrase: detail && detail.passphrase + }) + } + }) + } + + componentDidUpdate (prevProps, prevState) { + if (prevState.passphrase !== this.state.passphrase) { + setTimeout(() => { + window.print() + }, 500) + } + } + + render () { + const date = format(new Date(), 'MM/DD/YYYY') + + return
+
+
+ {date} +
+
+
+ {this.state.passphrase} +
+
+
+
+ } +} + +const styles = StyleSheet.create({ + content: { + fontWeight: '400', + color: '#3b3b3b', + fontSize: '16px' + } +}) + +module.exports = diff --git a/app/renderer/components/autofill/autofillAddressPanel.js b/app/renderer/components/autofill/autofillAddressPanel.js index 1e4d7422bd6..bac1b2c69fc 100644 --- a/app/renderer/components/autofill/autofillAddressPanel.js +++ b/app/renderer/components/autofill/autofillAddressPanel.js @@ -11,9 +11,11 @@ const ReduxComponent = require('../reduxComponent') const Dialog = require('../common/dialog') const Button = require('../common/button') const { - CommonForm, + CommonFormLarge, CommonFormSection, + CommonFormTitle, CommonFormDropdown, + CommonFormButtonWrapper, commonFormStyles } = require('../common/commonForm') @@ -158,8 +160,8 @@ class AutofillAddressPanel extends React.Component { render () { return - - + +
@@ -271,7 +273,7 @@ class AutofillAddressPanel extends React.Component {
- +
} } diff --git a/app/renderer/components/autofill/autofillCreditCardPanel.js b/app/renderer/components/autofill/autofillCreditCardPanel.js index 390055ae68c..064ec72e443 100644 --- a/app/renderer/components/autofill/autofillCreditCardPanel.js +++ b/app/renderer/components/autofill/autofillCreditCardPanel.js @@ -13,8 +13,10 @@ const Button = require('../common/button') const { CommonForm, CommonFormSection, + CommonFormTitle, CommonFormDropdown, CommonFormTextbox, + CommonFormButtonWrapper, commonFormStyles } = require('../common/commonForm') @@ -126,9 +128,9 @@ class AutofillCreditCardPanel extends React.Component { render () { return -
@@ -200,7 +202,7 @@ class AutofillCreditCardPanel extends React.Component {
- +
- + { this.props.editKey != null ? - + } } diff --git a/app/renderer/components/bookmarks/addEditBookmarkForm.js b/app/renderer/components/bookmarks/addEditBookmarkForm.js index ba4da7e1b97..c3115f2e6bb 100644 --- a/app/renderer/components/bookmarks/addEditBookmarkForm.js +++ b/app/renderer/components/bookmarks/addEditBookmarkForm.js @@ -12,6 +12,7 @@ const { CommonFormSection, CommonFormDropdown, CommonFormTextbox, + CommonFormButtonWrapper, commonFormStyles } = require('../common/commonForm') @@ -228,7 +229,7 @@ class AddEditBookmarkForm extends React.Component { - + { this.props.editKey != null ? - + } } diff --git a/app/renderer/components/bookmarks/addEditBookmarkHanger.js b/app/renderer/components/bookmarks/addEditBookmarkHanger.js index 6b89226bc76..3a225f88993 100644 --- a/app/renderer/components/bookmarks/addEditBookmarkHanger.js +++ b/app/renderer/components/bookmarks/addEditBookmarkHanger.js @@ -11,8 +11,8 @@ const ReduxComponent = require('../reduxComponent') const Dialog = require('../common/dialog') const AddEditBookmarkForm = require('./addEditBookmarkForm') const { - CommonFormHanger, - CommonFormSection + CommonFormBookmarkHanger, + CommonFormBottomWrapper } = require('../common/commonForm') // States @@ -108,8 +108,8 @@ class AddEditBookmarkHanger extends React.Component { bookmarkDialog: this.props.isModal, bookmarkHanger: !this.props.isModal, [css(styles.bookmarkHanger)]: !this.props.isModal - })} isClickDismiss> - + })} onHide={this.onClose} isClickDismiss> + { !this.props.isModal ?
: null } - +
{ !this.props.isModal - ? + ?
- + : null } - + } } const styles = StyleSheet.create({ + // Copied from commonForm.js + commonFormSection: { + // PR #7985 + margin: `${globalStyles.spacing.dialogInsideMargin} 30px` + }, + commonFormTitle: { + color: globalStyles.color.braveOrange, + fontSize: '1.2em' + }, + bookmarkHanger: { // See: #9040 justifyContent: 'flex-start !important', @@ -169,7 +182,7 @@ const styles = StyleSheet.create({ position: 'absolute', width: 0, height: 0, - border: `8px solid ${globalStyles.color.commonFormBackgroundColor}`, + border: `8px solid ${globalStyles.color.modalVeryLightGray}`, boxShadow: globalStyles.shadow.bookmarkHangerArrowUpShadow, transformOrigin: '0 0', transform: 'rotate(135deg)' diff --git a/app/renderer/components/bookmarks/bookmarkToolbarButton.js b/app/renderer/components/bookmarks/bookmarkToolbarButton.js index 950730bbbc3..9810feb4597 100644 --- a/app/renderer/components/bookmarks/bookmarkToolbarButton.js +++ b/app/renderer/components/bookmarks/bookmarkToolbarButton.js @@ -22,6 +22,7 @@ const bookmarksState = require('../../../common/state/bookmarksState') const dragTypes = require('../../../../js/constants/dragTypes') const {iconSize} = require('../../../../js/constants/config') const siteTags = require('../../../../js/constants/siteTags') +const {bookmarksToolbarMode} = require('../../../common/constants/settingsEnums') // Utils const {getCurrentWindowId} = require('../../currentWindow') @@ -175,15 +176,17 @@ class BookmarkToolbarButton extends React.Component { mergeProps (state, ownProps) { const currentWindow = state.get('currentWindow') const activeFrame = frameStateUtil.getActiveFrame(currentWindow) || Immutable.Map() + const bookmarkDisplayMode = ownProps.bookmarkDisplayMode const bookmarkKey = ownProps.bookmarkKey let bookmark = bookmarksState.findBookmark(state, bookmarkKey) const draggingOverData = bookmarkUtil.getDNDBookmarkData(state, bookmarkKey) const props = {} + // used in renderer - props.showFavicon = bookmarkUtil.showFavicon() - props.showOnlyFavicon = bookmarkUtil.showOnlyFavicon() + props.showFavicon = bookmarkUtil.showFavicon(state, bookmarkDisplayMode) + props.showOnlyFavicon = (bookmarkDisplayMode === bookmarksToolbarMode.FAVICONS_ONLY) props.favIcon = bookmark.get('favicon') props.title = bookmark.get('title') props.location = bookmark.get('location') @@ -242,6 +245,7 @@ class BookmarkToolbarButton extends React.Component { draggable ref={(node) => { this.bookmarkNode = node }} title={hoverTitle} + data-bookmark-key={this.props.bookmarkKey} onClick={this.onClick} onMouseOver={this.onMouseOver} onDragStart={this.onDragStart} @@ -312,21 +316,22 @@ module.exports = ReduxComponent.connect(BookmarkToolbarButton) const styles = StyleSheet.create({ bookmarkToolbarButton: { - display: 'flex', - alignItems: 'center', + WebkitAppRegion: 'no-drag', boxSizing: 'border-box', borderRadius: '3px', color: globalStyles.color.mediumGray, cursor: 'default', fontSize: globalStyles.spacing.bookmarksItemFontSize, lineHeight: '1.3', - margin: `auto ${globalStyles.spacing.bookmarksItemMargin}`, + // margin-bottom hides the second row of items on the bookmark bar + margin: `0 ${globalStyles.spacing.bookmarksItemMargin} 0 ${globalStyles.spacing.bookmarksItemMargin}`, maxWidth: globalStyles.spacing.bookmarksItemMaxWidth, padding: `2px ${globalStyles.spacing.bookmarksItemPadding}`, textOverflow: 'ellipsis', userSelect: 'none', whiteSpace: 'nowrap', - WebkitAppRegion: 'no-drag', + display: 'flex', + alignItems: 'center', ':hover': { background: '#fff', diff --git a/app/renderer/components/bookmarks/bookmarksToolbar.js b/app/renderer/components/bookmarks/bookmarksToolbar.js index 1608a3e0187..1adcbf51bc5 100644 --- a/app/renderer/components/bookmarks/bookmarksToolbar.js +++ b/app/renderer/components/bookmarks/bookmarksToolbar.js @@ -9,8 +9,8 @@ const Immutable = require('immutable') // Components const ReduxComponent = require('../reduxComponent') -const BrowserButton = require('../common/browserButton') const BookmarkToolbarButton = require('./bookmarkToolbarButton') +const BookmarksToolbarOverflowIcon = require('./bookmarksToolbarOverflowIcon') // Actions const appActions = require('../../../../js/actions/appActions') @@ -18,11 +18,12 @@ const windowActions = require('../../../../js/actions/windowActions') // State const windowState = require('../../../common/state/windowState') -const bookmarkToolbarState = require('../../../common/state/bookmarkToolbarState') +const bookmarksState = require('../../../common/state/bookmarksState') // Constants const dragTypes = require('../../../../js/constants/dragTypes') const siteTags = require('../../../../js/constants/siteTags') +const {bookmarksToolbarMode} = require('../../../common/constants/settingsEnums') // Utils const {isFocused} = require('../../currentWindow') @@ -30,14 +31,37 @@ const contextMenus = require('../../../../js/contextMenus') const dnd = require('../../../../js/dnd') const dndData = require('../../../../js/dndData') const isWindows = require('../../../common/lib/platformUtil').isWindows() -const frameStateUtil = require('../../../../js/state/frameStateUtil') const bookmarkUtil = require('../../../common/lib/bookmarkUtil') +const frameStateUtil = require('../../../../js/state/frameStateUtil') const {elementHasDataset} = require('../../../../js/lib/eventUtil') -const {getCurrentWindowId} = require('../../currentWindow') // Styles const globalStyles = require('../styles/global') +function getHiddenKeys (elements, immutableAllKeys) { + if (!elements || !elements.length) { + return + } + let firstOtherKey + // check again which ones are missing as now the indicator is there, we may have additional + for (let i = 1; i < elements.length; i++) { + // skip first item (0) + const thisElement = elements[i] + if (thisElement.offsetTop > 10) { + // the [i]th item is the first that does not fit + firstOtherKey = thisElement.dataset.bookmarkKey + break + } + } + if (firstOtherKey) { + const firstOtherIndex = immutableAllKeys.indexOf(firstOtherKey) + if (firstOtherIndex !== -1) { + const hiddenKeys = immutableAllKeys.slice(firstOtherIndex) + return hiddenKeys + } + } +} + class BookmarksToolbar extends React.Component { constructor (props) { super(props) @@ -46,6 +70,7 @@ class BookmarksToolbar extends React.Component { this.onDragOver = this.onDragOver.bind(this) this.onContextMenu = this.onContextMenu.bind(this) this.onMoreBookmarksMenu = this.onMoreBookmarksMenu.bind(this) + this.setBookmarksToolbarRef = this.setBookmarksToolbarRef.bind(this) } onDrop (e) { @@ -155,7 +180,7 @@ class BookmarksToolbar extends React.Component { onMoreBookmarksMenu (e) { const rect = e.target.getBoundingClientRect() - windowActions.onMoreBookmarksMenu(this.props.hiddenBookmarks, rect.bottom) + windowActions.onMoreBookmarksMenu(this.hiddenBookmarkKeys, rect.bottom) } onContextMenu (e) { @@ -172,70 +197,167 @@ class BookmarksToolbar extends React.Component { mergeProps (state, ownProps) { const currentWindow = state.get('currentWindow') const activeFrame = frameStateUtil.getActiveFrame(currentWindow) || Immutable.Map() - const currentWindowId = getCurrentWindowId() + const toolbarMode = bookmarkUtil.getBookmarksToolbarMode(state) const props = {} // used in renderer - props.showOnlyFavicon = bookmarkUtil.showOnlyFavicon() - props.showFavicon = bookmarkUtil.showFavicon() props.shouldAllowWindowDrag = !isWindows && windowState.shouldAllowWindowDrag(state, currentWindow, activeFrame, isFocused(state)) - props.visibleBookmarks = bookmarkToolbarState.getToolbar(state, currentWindowId) - props.hiddenBookmarks = bookmarkToolbarState.getOther(state, currentWindowId) - + props.toolbarBookmarks = bookmarksState.getBookmarksWithFolders(state, 0).take(110).map(item => item.get('key')) + props.textOnly = (toolbarMode === bookmarksToolbarMode.TEXT_ONLY) + props.bookmarkDisplayMode = toolbarMode // also forces re-compute toolbar space after change mode // used in other functions - props.activeFrameKey = activeFrame.get('key') props.title = activeFrame.get('title') props.location = activeFrame.get('location') return props } + calculateNonFirstRowItems () { + if (!this.bookmarksToolbarRef) { + return + } + const bookmarkRefs = this.bookmarksToolbarRef.children + const classNameShowOverflow = css(styles.bookmarksToolbar_hasOverflow) + this.hiddenBookmarkKeys = null + this.hasHiddenKeys = false + // first check which items overflow with indicator visible + this.bookmarksToolbarRef.classList.add(classNameShowOverflow) + // and save which keys were hidden for the overflow menu to open + this.hiddenBookmarkKeys = getHiddenKeys(bookmarkRefs, this.props.toolbarBookmarks) + // we don't need indicator if there were no hidden items with / without it + this.bookmarksToolbarRef.classList.remove(classNameShowOverflow) + // if there were hidden items with the indicator + if (this.hiddenBookmarkKeys && this.hiddenBookmarkKeys.size) { + // check again to see if we really need the indicator + const hiddenKeysWithNoIndicator = getHiddenKeys(bookmarkRefs, this.props.toolbarBookmarks) + if (hiddenKeysWithNoIndicator && hiddenKeysWithNoIndicator.size) { + // add overflow indicator as needed + this.hasHiddenKeys = true + this.bookmarksToolbarRef.classList.add(classNameShowOverflow) + } + } + } + + setBookmarksToolbarRef (ref) { + const oldRef = this.bookmarksToolbarRef + this.bookmarksToolbarRef = ref + // handle there was a previous element + if (oldRef) { + // do not monitor size change for old element + // but we can keep ResizeObserver around + // note: this shouldn't happen because we're always returning + // the same root element from this.render(), but it's best practice. + if (this.resizeObserver) { + this.resizeObserver.unobserve(oldRef) + } + } + // handle null element this time + if (!ref) { + return + } + // recalculate which items are not on a single line on resize + let debounceAnimationFrame + this.resizeObserver = this.resizeObserver || new window.ResizeObserver(() => { + // (only once before the next paint frame) + debounceAnimationFrame = debounceAnimationFrame || window.requestAnimationFrame(() => { + debounceAnimationFrame = null + this.calculateNonFirstRowItems() + }) + }) + // observe this ref + this.resizeObserver.observe(this.bookmarksToolbarRef) + } + + componentDidUpdate (prevProps) { + // Only recalc which bookmark items are overflowed if the bookmarks changed + // or the display mode changed. + if (prevProps.bookmarkDisplayMode !== this.props.bookmarkDisplayMode || !prevProps.toolbarBookmarks.equals(this.props.toolbarBookmarks)) { + // No need to wait for the new DOM render result to paint + // before measuring since reading offsetTop of the elements + // will force layout to be computed. + this.calculateNonFirstRowItems() + } + } + render () { this.bookmarkRefs = [] - return
{ - this.props.visibleBookmarks.map((bookmarkKey, i) => - this.bookmarkRefs.push(node)} - key={`toolbar-button-${i}`} - bookmarkKey={bookmarkKey} - />) + this.props.toolbarBookmarks.map((bookmarkKey, i) => + this.bookmarkRefs.push(node)} + key={`toolbar-button-${i}`} + bookmarkKey={bookmarkKey} + bookmarkDisplayMode={this.props.bookmarkDisplayMode} + />) } - { - this.props.hiddenBookmarks.size !== 0 - ? + - : null - } +
} } const styles = StyleSheet.create({ bookmarksToolbar: { + '--bookmarks-toolbar-overflow-indicator-width': '0px', + '--bookmarks-toolbar-height': globalStyles.spacing.bookmarksToolbarHeight, + flex: 1, boxSizing: 'border-box', + height: 'var(--bookmarks-toolbar-height)', display: 'flex', - flex: 1, - alignItems: 'center', // to align bookmarksToolbar__overflowIndicator to the center - padding: `0 ${globalStyles.spacing.bookmarksToolbarPadding}`, - margin: `${globalStyles.spacing.navbarMenubarMargin} 0` + flexDirection: 'row', + flexWrap: 'wrap', + alignItems: 'center', + overflow: 'hidden', + // leave space on the right for the overflow button when appropriate + // aphrodite cannot have a calc in a shorthand padding declaration :-( + paddingRight: `calc(${globalStyles.spacing.bookmarksToolbarPadding} + var(--bookmarks-toolbar-overflow-indicator-width))`, + paddingTop: 0, + paddingBottom: 0, + paddingLeft: globalStyles.spacing.bookmarksToolbarPadding, + margin: `${globalStyles.spacing.navbarMenubarMargin} 0`, + position: 'relative' + }, + + bookmarksToolbar_textOnly: { + '--bookmarks-toolbar-height': globalStyles.spacing.bookmarksToolbarTextOnlyHeight + }, + + bookmarksToolbar_hasOverflow: { + '--bookmarks-toolbar-overflow-indicator-visibility': 'visible', + '--bookmarks-toolbar-overflow-indicator-width': `${globalStyles.spacing.bookmarksToolbarOverflowButtonWidth} !important` }, bookmarksToolbar_allowDragging: { @@ -246,12 +368,30 @@ const styles = StyleSheet.create({ WebkitAppRegion: 'no-drag' }, - bookmarksToolbar_showOnlyFavicon: { - padding: `0 0 0 ${globalStyles.spacing.bookmarksToolbarPadding}` + bookmarksToolbar__overflowIndicator: { + WebkitAppRegion: 'no-drag', + position: 'absolute', + top: 0, + right: 0, + height: 'var(--bookmarks-toolbar-height)', + margin: `0 calc(${globalStyles.spacing.bookmarksToolbarPadding} + 5px) 0 auto`, + visibility: 'var(--bookmarks-toolbar-overflow-indicator-visibility, hidden)', + border: 'none', + background: 'transparent', + padding: 0, + width: 'auto', + outline: 'none', + display: 'flex', + alignItems: 'center', + color: globalStyles.button.color, + ':hover': { + color: globalStyles.button.default.hoverColor + } }, - bookmarksToolbar__overflowIndicator: { - margin: '0 5px 0 auto' + bookmarksToolbar__overflowIndicator__icon: { + width: globalStyles.spacing.bookmarksToolbarOverflowButtonWidth, + height: 'auto' } }) diff --git a/app/renderer/components/bookmarks/bookmarksToolbarOverflowIcon.js b/app/renderer/components/bookmarks/bookmarksToolbarOverflowIcon.js new file mode 100644 index 00000000000..80b295b4fe1 --- /dev/null +++ b/app/renderer/components/bookmarks/bookmarksToolbarOverflowIcon.js @@ -0,0 +1,7 @@ +module.exports = function BookmarksToolbarOverflowIcon ({ className }) { + return ( + + + + ) +} diff --git a/app/renderer/components/common/browserButton.js b/app/renderer/components/common/browserButton.js index 739911fc1c8..5fb0a987ce8 100644 --- a/app/renderer/components/common/browserButton.js +++ b/app/renderer/components/common/browserButton.js @@ -13,6 +13,7 @@ class BrowserButton extends ImmutableComponent { styles.browserButton, this.props.primaryColor && [styles.browserButton_default, styles.browserButton_primaryColor], this.props.secondaryColor && [styles.browserButton_default, styles.browserButton_secondaryColor], + this.props.alertColor && [styles.browserButton_default, styles.browserButton_alertColor], this.props.subtleItem && [styles.browserButton_default, styles.browserButton_subtleItem], // actionItem is just subtleItem with a blue background this.props.actionItem && @@ -61,7 +62,6 @@ class BrowserButton extends ImmutableComponent { return } } diff --git a/app/renderer/components/common/messageBox.js b/app/renderer/components/common/messageBox.js index fa937cecf57..ea70445c699 100644 --- a/app/renderer/components/common/messageBox.js +++ b/app/renderer/components/common/messageBox.js @@ -9,9 +9,9 @@ const {StyleSheet, css} = require('aphrodite/no-important') // Components const ReduxComponent = require('../reduxComponent') const Dialog = require('./dialog') -const FlyoutDialog = require('./flyoutDialog') const BrowserButton = require('../common/browserButton') const SwitchControl = require('./switchControl') +const {PromptTextBox} = require('./textbox') // Actions const appActions = require('../../../../js/actions/appActions') @@ -36,12 +36,21 @@ class MessageBox extends React.Component { super(props) this.onKeyDown = this.onKeyDown.bind(this) this.onSuppressChanged = this.onSuppressChanged.bind(this) + this.state = { + textInput: props.defaultPromptText + } } componentWillMount () { document.addEventListener('keydown', this.onKeyDown) } + componentDidMount () { + if (this.props.allowInput) { + this.inputRef.select() + } + } + componentWillUnmount () { document.removeEventListener('keydown', this.onKeyDown) } @@ -82,6 +91,10 @@ class MessageBox extends React.Component { response.result = buttonId !== this.props.cancelId } + if (this.props.allowInput) { + response.input = this.state.textInput + } + appActions.tabMessageBoxDismissed(tabId, response) } @@ -112,6 +125,8 @@ class MessageBox extends React.Component { // used in renderer props.tabId = tabId props.message = messageBoxDetail.get('message') + props.allowInput = messageBoxDetail.get('allowInput') + props.defaultPromptText = messageBoxDetail.get('defaultPromptText') props.suppress = tabMessageBoxState.getSuppress(state, tabId) props.title = tabMessageBoxState.getTitle(state, tabId) props.showSuppress = tabMessageBoxState.getShowSuppress(state, tabId) @@ -126,8 +141,8 @@ class MessageBox extends React.Component { render () { return -
@@ -151,11 +166,26 @@ class MessageBox extends React.Component { /> : null } + { + this.props.allowInput && ( + { + this.inputRef = ref + }} + onChange={e => { + this.setState({ + textInput: e.target.value + }) + }} + /> + ) + }
{this.messageBoxButtons}
-
+
} } diff --git a/app/renderer/components/common/settings.js b/app/renderer/components/common/settings.js index e484ccedba1..70d62c3dba4 100644 --- a/app/renderer/components/common/settings.js +++ b/app/renderer/components/common/settings.js @@ -178,7 +178,7 @@ class SettingItemIcon extends ImmutableComponent { const styles = StyleSheet.create({ icon: { - backgroundColor: '#5a5a5a', + backgroundColor: 'rgb(90, 90, 98)', height: '16px', width: '16px', display: 'inline-block', diff --git a/app/renderer/components/common/sortableTable.js b/app/renderer/components/common/sortableTable.js index a1afa10e61c..a9b436e524d 100644 --- a/app/renderer/components/common/sortableTable.js +++ b/app/renderer/components/common/sortableTable.js @@ -12,6 +12,7 @@ const globalStyles = require('../styles/global') // Utils const cx = require('../../../../js/lib/classSet') const eventUtil = require('../../../../js/lib/eventUtil') +const ImmutableUtil = require('../../../common/state/immutableUtil') tableSort.extend('number', (item) => { return typeof item === 'number' @@ -32,16 +33,62 @@ class SortableTable extends React.Component { } this.counter = 0 this.sortTable = null + this.dimensionCount = null } componentDidMount () { - this.sortTable = tableSort(this.table) + this.sortTable = tableSort(this.table, { + descending: this.props.defaultHeadingSortOrder === 'desc' + }) return this.sortTable } componentDidUpdate (prevProps) { - if (this.props.rows && - (!prevProps.rows || - prevProps.rows.length !== this.props.rows.length)) { - this.sortTable.refresh() + if (this.isMultiDimensioned) { + let count = 0 + if (this.dimensionCount == null && prevProps.rows) { + for (let i = 0; i < prevProps.rows.length; i++) { + if (this.props.rows[i].length > 0) { + count += this.props.rows[i].length + } + } + this.dimensionCount = count + } + + if (!this.props.rows) { + this.dimensionCount = null + return + } + + count = 0 + for (let i = 0; i < this.props.rows.length; i++) { + if (this.props.rows[i].length > 0) { + count += this.props.rows[i].length + } + } + + if (count !== this.dimensionCount) { + this.sortTable.refresh() + this.dimensionCount = count + return + } + } else { + if ( + this.props.rows && + ( + !prevProps.rows || + prevProps.rows.length !== this.props.rows.length + ) + ) { + this.sortTable.refresh() + return + } + } + + if (this.props.rows && typeof this.props.sortCheck === 'function') { + const shouldSort = this.props.sortCheck(prevProps.rows, this.props.rows) + + if (shouldSort) { + this.sortTable.refresh() + } } } /** @@ -202,11 +249,11 @@ class SortableTable extends React.Component { const tableID = parseInt(tableParts[0]) const rowIndex = parseInt(tableParts[1]) const handlerInput = this.props.totalRowObjects - ? (typeof this.props.totalRowObjects[parseInt(tableID)][rowIndex].toJS === 'function' + ? (ImmutableUtil.isImmutable(this.props.totalRowObjects[parseInt(tableID)][rowIndex]) ? this.props.totalRowObjects[parseInt(tableID)][rowIndex].toJS() : this.props.totalRowObjects[parseInt(tableID)][rowIndex]) : (this.props.rowObjects.size > 0 || this.props.rowObjects.length > 0) - ? (typeof this.props.rowObjects.toJS === 'function' + ? (ImmutableUtil.isImmutable(this.props.rowObjects) ? this.props.rowObjects.get(rowIndex).toJS() : this.props.rowObjects[rowIndex]) : null @@ -305,23 +352,51 @@ class SortableTable extends React.Component { // Object bound to this row. Not passed to multi-select handlers. if (this.isMultiDimensioned) { // Object bound to this row. Not passed to multi-select handlers. - handlerInput = this.props.rowObjects[bodyIndex] && - (this.props.rowObjects[bodyIndex].size > 0 || this.props.rowObjects[bodyIndex].length > 0) - ? (typeof this.props.rowObjects[bodyIndex].toJS === 'function' - ? this.props.rowObjects[bodyIndex].get(index).toJS() - : (typeof this.props.rowObjects[bodyIndex][index].toJS === 'function' - ? this.props.rowObjects[bodyIndex][index].toJS() - : this.props.rowObjects[bodyIndex][index])) - : row + if ( + this.props.rowObjects[bodyIndex] && + ( + this.props.rowObjects[bodyIndex].size > 0 || + this.props.rowObjects[bodyIndex].length > 0 + ) + ) { + let indexObj + if (ImmutableUtil.isImmutable(this.props.rowObjects[bodyIndex])) { + indexObj = this.props.rowObjects[bodyIndex].get(index) + } else { + indexObj = this.props.rowObjects[bodyIndex][index] + } + + handlerInput = indexObj + + if (ImmutableUtil.isImmutable(indexObj)) { + handlerInput = indexObj.toJS() + } + } else { + handlerInput = row + } } else { - handlerInput = this.props.rowObjects && - (this.props.rowObjects.size > 0 || this.props.rowObjects.length > 0) - ? (typeof this.props.rowObjects.toJS === 'function' - ? this.props.rowObjects.get(index).toJS() - : (typeof this.props.rowObjects[index].toJS === 'function' - ? this.props.rowObjects[index].toJS() - : this.props.rowObjects[index])) - : row + if ( + this.props.rowObjects && + ( + this.props.rowObjects.size > 0 || + this.props.rowObjects.length > 0 + ) + ) { + let indexObj + if (ImmutableUtil.isImmutable(this.props.rowObjects)) { + indexObj = this.props.rowObjects.get(index) + } else { + indexObj = this.props.rowObjects[index] + } + + handlerInput = indexObj + + if (ImmutableUtil.isImmutable(indexObj)) { + handlerInput = indexObj.toJS() + } + } else { + handlerInput = row + } } // Allow parent control to optionally specify context @@ -473,9 +548,10 @@ class SortableTable extends React.Component { if (dataType === 'object' && firstEntry.value) { dataType = typeof firstEntry.value } + const defaultSort = (this.sortingDisabled || heading === this.props.defaultHeading) || null const headerClasses = { 'sort-header': true, - 'sort-default': this.sortingDisabled || heading === this.props.defaultHeading, + 'sort-default': defaultSort, [css(styles.table__th, this.props.smallRow && styles.table__th_smallRow)]: true } const isString = typeof heading === 'string' @@ -486,8 +562,9 @@ class SortableTable extends React.Component { return diff --git a/app/renderer/components/common/textbox.js b/app/renderer/components/common/textbox.js index 9438744cdce..d9c6d8fc5a0 100644 --- a/app/renderer/components/common/textbox.js +++ b/app/renderer/components/common/textbox.js @@ -6,6 +6,9 @@ const React = require('react') const ImmutableComponent = require('../immutableComponent') const {StyleSheet, css} = require('aphrodite/no-important') +const ClipboardButton = require('../common/clipboardButton') +const appActions = require('../../../../js/actions/appActions') + const globalStyles = require('../styles/global') const commonStyles = require('../styles/commonStyles') @@ -17,10 +20,17 @@ class Textbox extends ImmutableComponent { styles.textbox, (this.props.readonly || this.props.readOnly) ? styles.readOnly : styles.outlineable, this.props['data-isCommonForm'] && commonStyles.isCommonForm, - this.props['data-isSettings'] && styles.isSettings + this.props['data-isSettings'] && styles.isSettings, + this.props['data-isPrompt'] && styles.isPrompt, + this.props.customClass && this.props.customClass ) - return + const props = Object.assign({}, this.props) + const ref = this.props.inputRef + delete props.customClass + delete props.inputRef + + return } } @@ -59,6 +69,12 @@ class SettingTextbox extends ImmutableComponent { } } +class PromptTextBox extends ImmutableComponent { + render () { + return + } +} + // TextArea class TextArea extends ImmutableComponent { render () { @@ -77,6 +93,65 @@ class DefaultTextArea extends ImmutableComponent { } } +class WordCountTextArea extends React.Component { + constructor () { + super() + this.handleCopyToClipboard = this.handleCopyToClipboard.bind(this) + this.handleOnChange = this.handleOnChange.bind(this) + this.state = { wordCount: 0 } + } + + handleOnChange (e) { + let wordCount = 0 + + if (e.target.value.length > 0) { + wordCount = e.target.value.trim().replace(/\s+/gi, ' ').split(' ').length + } + + this.setState({wordCount}) + + if (this.props.onChangeText) { + this.props.onChangeText() + } + } + + handleCopyToClipboard () { + if (!this.textAreaBox) { + return + } + appActions.clipboardTextCopied(this.textAreaBox.value) + } + + render () { + return ( +
+