diff --git a/_locales/en/messages.json b/_locales/en/messages.json index bd019b4ee..c4da83e77 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -525,6 +525,10 @@ "description": "as in '5 (trackers) BLOCKED'", "message": "Blocked" }, + "blocking_category_whitelisted": { + "description": "as in '5 (trackers) WHITELISTED'", + "message": "Whitelisted" + }, "panel_create_account": { "message": "Create Account" }, @@ -717,6 +721,9 @@ "panel_tracker_trust_tooltip": { "message": "Trust on this site" }, + "panel_tracker_scrub_tooltip": { + "message": "Scrub on this site" + }, "panel_detail_menu_list_title": { "message": "List View" }, @@ -2247,5 +2254,14 @@ }, "ghostery_rewards": { "message": "ghostery rewards" + }, + "enhanced_anti_tracking": { + "message": "Enhanced Anti-Tracking" + }, + "unknown": { + "message": "Unknown" + }, + "unknown_description": { + "message": "Unknown trackers scrubbed by Anti-Tracking" } } diff --git a/app/images/panel/anti_tracking_unknown.svg b/app/images/panel/anti_tracking_unknown.svg new file mode 100644 index 000000000..936cfeb8c --- /dev/null +++ b/app/images/panel/anti_tracking_unknown.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/app/panel/actions/BlockingActions.js b/app/panel/actions/BlockingActions.js index 0a7621c91..5e4c273f9 100644 --- a/app/panel/actions/BlockingActions.js +++ b/app/panel/actions/BlockingActions.js @@ -15,9 +15,11 @@ import { UPDATE_BLOCKING_DATA, UPDATE_BLOCK_ALL_TRACKERS, UPDATE_CATEGORIES, + UPDATE_ANTI_TRACKING_HIDE, UPDATE_CATEGORY_BLOCKED, UPDATE_TRACKER_BLOCKED, UPDATE_TRACKER_TRUST_RESTRICT, + UPDATE_ANTI_TRACKING_WHITELIST, TOGGLE_EXPAND_ALL } from '../constants/constants'; @@ -56,6 +58,19 @@ export function updateCategories(data) { }; } +/** + * Called from Blocking setShow functions + * Hits the Summary reducer, as that is where the AntiTracking data is stored + * @param {Object} data + * @return {Object} + */ +export function updateAntiTrackingHide(data) { + return { + type: UPDATE_ANTI_TRACKING_HIDE, + data, + }; +} + /** * Called from Category.clickCategoryStatus() * @param {Object} data @@ -103,6 +118,22 @@ export function updateTrackerTrustRestrict(data) { }; } +/** + * Called from Tracker.handleAntiTrackingWhitelist() + * @param {Object} data + * @return {Object} dispatch + */ +export function updateAntiTrackingWhitelist(unknownTracker) { + return function(dispatch, getState) { + // use redux-thunk to get pageHost from summary + const { pageHost } = getState().summary; + dispatch({ + type: UPDATE_ANTI_TRACKING_WHITELIST, + data: { unknownTracker, pageHost }, + }); + }; +} + /** * Called from BlockingHeader.clickExpandAll() * @param {Object} data diff --git a/app/panel/components/Blocking.jsx b/app/panel/components/Blocking.jsx index 3819b02c0..a26c4c2fe 100644 --- a/app/panel/components/Blocking.jsx +++ b/app/panel/components/Blocking.jsx @@ -93,6 +93,7 @@ class Blocking extends React.Component { */ setShow(filterName) { const updated_categories = JSON.parse(JSON.stringify(this.props.categories)); // deep clone + const updatedAntiTracking = JSON.parse(JSON.stringify(this.props.antiTracking)); // deep clone updated_categories.forEach((category) => { let count = 0; @@ -111,7 +112,9 @@ class Blocking extends React.Component { category.num_shown = (show) ? count : 0; }); + updatedAntiTracking.hide = !(filterName === 'all' || filterName === 'unknown'); this.props.actions.updateCategories(updated_categories); + this.props.actions.updateAntiTrackingHide(updatedAntiTracking); } /** @@ -281,6 +284,8 @@ class Blocking extends React.Component { const { actions, categories, + antiTracking, + enable_anti_tracking, expand_all_trackers, is_expanded, language, @@ -309,25 +314,27 @@ class Blocking extends React.Component { smartBlockActive={smartBlockActive} smartBlock={smartBlock} /> - {(disableBlocking && is_expanded) ? + {(disableBlocking && is_expanded) ? ( - : ( -
- { categories.length > 0 && ( - - )} -
- )} + ) : ( +
+ {categories.length > 0 && ( + + )} +
+ )} ); } diff --git a/app/panel/components/Blocking/BlockingHeader.jsx b/app/panel/components/Blocking/BlockingHeader.jsx index ab059af79..6bd2e6e5a 100644 --- a/app/panel/components/Blocking/BlockingHeader.jsx +++ b/app/panel/components/Blocking/BlockingHeader.jsx @@ -32,6 +32,7 @@ class BlockingHeader extends React.Component { filtered: false, searchValue: '', filterMenuOpened: false, + blockAllDisabled: true, }; // event bindings @@ -93,7 +94,12 @@ class BlockingHeader extends React.Component { }); }); if (this.state.fromHere || totalShown === totalBlocked || totalBlocked === 0) { - this.setState({ allBlocked: (totalShown === totalBlocked), filtered, fromHere: false }); + this.setState({ + allBlocked: (totalShown === totalBlocked), + blockAllDisabled: !totalShown, + fromHere: false, + filtered + }); } } } @@ -256,9 +262,14 @@ class BlockingHeader extends React.Component {
- {this.props.categories && this.props.categories.length > 0 && -
{blockText}
- } + {this.props.categories && this.props.categories.length > 0 && ( +
+ {blockText} +
+ )}
{ diff --git a/app/panel/components/Blocking/Categories.jsx b/app/panel/components/Blocking/Categories.jsx index 193f233fd..db1605fee 100644 --- a/app/panel/components/Blocking/Categories.jsx +++ b/app/panel/components/Blocking/Categories.jsx @@ -29,28 +29,78 @@ class Categories extends React.Component { * @return {ReactComponent} ReactComponent instance */ render() { - const { categories, expandAll } = this.props; + const { + categories, + expandAll, + antiTracking, + enable_anti_tracking, + } = this.props; const globalBlocking = !!this.props.globalBlocking; const filtered = !!this.props.filtered; - const categoryList = categories.map((cat, index) => ( - - )); - return
{ categoryList }
; + + const renderCategory = (category, index, isUnknown) => { + let whitelistedTotal = 0; + const unknownCategoryMapping = isUnknown ? ( + { + id: 'anti_tracking_unknown', + name: t('unknown'), + description: t('unknown_description'), + img_name: 'anti_tracking_unknown', + num_total: antiTracking.unknownTrackers.length, + num_blocked: antiTracking.unknownTrackerCount, + num_shown: antiTracking.hide ? 0 : antiTracking.unknownTrackers.length, + trackers: antiTracking.unknownTrackers.map((unknownTracker) => { + if (unknownTracker.whitelisted) { whitelistedTotal++; } + return { + name: unknownTracker.name, + domains: unknownTracker.domains, + whitelisted: unknownTracker.whitelisted, + blocked: false, + catId: 'anti_tracking_unknown', + description: '', + id: unknownTracker.name + unknownTracker.domains[0], + shouldShow: true, + cliqzAdCount: unknownTracker.ads, + cliqzCookieCount: unknownTracker.cookies, + cliqzFingerprintCount: unknownTracker.fingerprints, + }; + }), + whitelistedTotal, + } + ) : null; + + return ( + + ); + }; + + const categoryList = categories.map((category, index) => renderCategory(category, index)); + const unknownCategory = antiTracking && antiTracking.unknownTrackers.length + ? renderCategory(null, categoryList.length, true) : null; + + return ( +
+ {categoryList} + {unknownCategory} +
+ ); } } diff --git a/app/panel/components/Blocking/Category.jsx b/app/panel/components/Blocking/Category.jsx index e5866c089..65934f84e 100644 --- a/app/panel/components/Blocking/Category.jsx +++ b/app/panel/components/Blocking/Category.jsx @@ -13,6 +13,8 @@ import React from 'react'; import Trackers from './Trackers'; +import { CliqzFeature } from '../BuildingBlocks'; +import Globals from '../../../../src/classes/Globals'; /** * @class Implement Category component, which represents a @@ -159,10 +161,20 @@ class Category extends React.Component { * @return {ReactComponent} ReactComponent instance */ render() { - const { category, paused_blocking, sitePolicy } = this.props; + const { + category, + paused_blocking, + sitePolicy, + enable_anti_tracking, + actions, + isUnknown, + } = this.props; + const globalBlocking = !!this.props.globalBlocking; - const filteredText = { color: 'red' }; + const checkBoxStyle = `${(this.state.totalShownBlocked && this.state.allShownBlocked) ? 'all-blocked ' : (this.state.totalShownBlocked ? 'some-blocked ' : '')} checkbox-container`; + const caretClasses = (this.state.isExpanded ? 'caret-up' : 'caret-down') + (isUnknown ? ' Category__antiTrackingCaret' : ''); + const filteredText = { color: 'red' }; let trackersBlockedCount; if (paused_blocking || sitePolicy === 2) { @@ -173,88 +185,126 @@ class Category extends React.Component { trackersBlockedCount = category.num_blocked || 0; } + const clickCliqzFeature = (options) => { + const { feature, status, text } = options; + this.props.actions.showNotification({ + updated: feature, + reload: true, + text, + }); + actions.toggleCliqzFeature(feature, status); + }; + const cliqzInactive = paused_blocking || sitePolicy || Globals.IS_CLIQZ; + return (
-
+
+ {isUnknown && ( +
+

+ {t('enhanced_anti_tracking')} +

+
+ )}
-
+
- { category.name } + {category.name}
- { - this.props.filtered && ( - - t(`blocking_category_tracker_found`) - - )} + {this.props.filtered && ( + + {t('blocking_category_tracker_found')} + + )} {`${category.num_total} `} { (category.num_total === 1) ? t('blocking_category_tracker') : t('blocking_category_trackers') }
- { - !!trackersBlockedCount && ( -
- {`${trackersBlockedCount} `} - { t('blocking_category_blocked') } -
- )} + {((!isUnknown && !!trackersBlockedCount) + || (isUnknown && !!category.whitelistedTotal)) && ( +
+ + {isUnknown ? `${category.whitelistedTotal} ` : `${trackersBlockedCount} `} + + + {isUnknown ? t('blocking_category_whitelisted') : t('blocking_category_blocked') } + +
+ )}
-
-
- - - - - - - - - - - - - - - - - - - - - - - -
+
+ {!isUnknown && ( +
+ + + + + + + + + + + + {/* DO WE NEED THE TWO SVGS BELOW? THEY ARE NEVER DISPLAYED */} + + + + + + + + + + + + +
+ )} + {isUnknown && ( +
+ +
+ )}
- { - this.state.isExpanded && ( - - )} + {this.state.isExpanded && ( + + )}
); } diff --git a/app/panel/components/Blocking/Tracker.jsx b/app/panel/components/Blocking/Tracker.jsx index 634549af3..da04eae41 100644 --- a/app/panel/components/Blocking/Tracker.jsx +++ b/app/panel/components/Blocking/Tracker.jsx @@ -15,9 +15,12 @@ import React from 'react'; import ReactSVG from 'react-svg'; +import ClassNames from 'classnames'; + import globals from '../../../../src/classes/Globals'; import { log } from '../../../../src/utils/common'; import { sendMessageInPromise } from '../../utils/msg'; +import { renderKnownTrackerButtons, renderUnknownTrackerButtons } from './trackerButtonRenderHelpers'; /** * @class Implement Tracker component which represents single tracker * in the Blocking view. @@ -39,6 +42,7 @@ class Tracker extends React.Component { this.clickTrackerStatus = this.clickTrackerStatus.bind(this); this.clickTrackerTrust = this.clickTrackerTrust.bind(this); this.clickTrackerRestrict = this.clickTrackerRestrict.bind(this); + this.handleAntiTrackingWhitelist = this.handleAntiTrackingWhitelist.bind(this); } /** @@ -208,6 +212,21 @@ class Tracker extends React.Component { }); } + /** + * Implement handler for clicking on the trust or scrub SVGs for an unknown tracker + * Trigger actions which persist the new setting and notify user + * that the page should be reloaded. + */ + handleAntiTrackingWhitelist() { + const { tracker } = this.props; + + this.props.actions.updateAntiTrackingWhitelist(tracker); + this.props.actions.showNotification({ + updated: `${tracker.name}-whitelisting-status-changed`, + reload: true, + }); + } + _renderCliqzStatsContainer() { const { tracker } = this.props; const { cliqzAdCount, cliqzCookieCount, cliqzFingerprintCount } = tracker; @@ -274,9 +293,9 @@ class Tracker extends React.Component { * @return {ReactComponent} ReactComponent instance */ render() { - const { tracker } = this.props; - let sources; + const { tracker, isUnknown } = this.props; + let sources; if (tracker.sources) { sources = tracker.sources.map((source, index) => ( )); + } else if (tracker.domains) { + sources = tracker.domains.map((domain, index) => ( +

{domain}

+ )); } + + const trackerNameClasses = ClassNames('trk-name', { + 'is-whitelisted': tracker.whitelisted, + }); + return (
@@ -298,78 +326,47 @@ class Tracker extends React.Component {
-
{ tracker.name }
- {this._renderCliqzStatsContainer()} +
+ {tracker.name} +
+ {!tracker.whitelisted && this._renderCliqzStatsContainer()}
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - { if (this.props.tracker.ss_allowed || this.props.tracker.ss_blocked) { return; } this.clickTrackerStatus(); }} width="20px" height="20px" viewBox="0 0 20 20"> - - - - - - - - - - - - - - - - - - -
+ {!isUnknown ? renderKnownTrackerButtons( + this.props.tracker.ss_allowed, + this.props.tracker.ss_blocked, + this.clickTrackerTrust, + this.clickTrackerRestrict, + this.clickTrackerStatus, + ) : renderUnknownTrackerButtons( + this.handleAntiTrackingWhitelist, + tracker.whitelisted, + )}
- { - this.state.showMoreInfo && ( -
- ); } diff --git a/app/panel/components/Blocking/Trackers.jsx b/app/panel/components/Blocking/Trackers.jsx index fa1c4e7a9..5b9bc0dce 100644 --- a/app/panel/components/Blocking/Trackers.jsx +++ b/app/panel/components/Blocking/Trackers.jsx @@ -14,6 +14,7 @@ import React from 'react'; import Tracker from './Tracker'; import GlobalTracker from './GlobalTracker'; + /** * @class Implement Trackers component which represents a container for trackers * in both Blocking view and Global Blocking subview of Settings. @@ -39,7 +40,7 @@ class Trackers extends React.Component { * @return {ReactComponent} ReactComponent instance */ render() { - const { trackers } = this.props; + const { trackers, isUnknown } = this.props; let trackerList; if (this.props.globalBlocking) { const trackersToShow = []; @@ -73,6 +74,7 @@ class Trackers extends React.Component { language={this.props.language} smartBlockActive={this.props.smartBlockActive} smartBlock={this.props.smartBlock} + isUnknown={isUnknown} /> )); } diff --git a/app/panel/components/Blocking/trackerButtonRenderHelpers.jsx b/app/panel/components/Blocking/trackerButtonRenderHelpers.jsx new file mode 100644 index 000000000..2ea712a7c --- /dev/null +++ b/app/panel/components/Blocking/trackerButtonRenderHelpers.jsx @@ -0,0 +1,90 @@ +import React from 'react'; +import ClassNames from 'classnames'; + +export const renderKnownTrackerButtons = ( + ss_allowed, ss_blocked, clickTrackerTrust, clickTrackerRestrict, clickTrackerStatus +) => ( +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + { if (ss_allowed || ss_blocked) { return; } clickTrackerStatus(); }} width="20px" height="20px" viewBox="0 0 20 20"> + + + + + + + + + + + + + + + + + + +
+); + +export const renderUnknownTrackerButtons = (handleAntiTrackingWhitelist, whitelisted) => { + const svgContainerClasses = ClassNames('unknown-svg-container', { whitelisted }); + + return ( +
+ {/* USE INLINE SVG FOR TRUST CIRCLE TO CHANGE COLORS WITH CSS */} + + + + + + + + + + + + + {/* USE INLINE SVG FOR ANTI-TRACKING SHIELD TO CHANGE COLORS WITH CSS */} + + + + + + + + + + + + + +
+ ); +}; diff --git a/app/panel/components/BuildingBlocks/CliqzFeature.jsx b/app/panel/components/BuildingBlocks/CliqzFeature.jsx index 93d786c15..72cfe1372 100644 --- a/app/panel/components/BuildingBlocks/CliqzFeature.jsx +++ b/app/panel/components/BuildingBlocks/CliqzFeature.jsx @@ -82,6 +82,7 @@ class CliqzFeature extends React.Component { cliqzInactive, isSmaller, isCondensed, + isTiny, isTooltipBody, isTooltipHeader, tooltipPosition, @@ -92,6 +93,7 @@ class CliqzFeature extends React.Component { 'CliqzFeature--normal': !isSmaller && !isCondensed, 'CliqzFeature--smaller': isSmaller, 'CliqzFeature--condensed': isCondensed, + 'CliqzFeature--tiny': isTiny, 'CliqzFeature--active': active, 'CliqzFeature--inactive': !active, clickable: !cliqzInactive, @@ -111,6 +113,7 @@ class CliqzFeature extends React.Component { header={this._getTooltipHeaderText(isTooltipHeader, type)} body={this._getTooltipBodyText(active, isTooltipBody, type)} position={tooltipPosition} + // className={isTiny ? 'CliqzFeature--tooltipUp' : ''} />
diff --git a/app/panel/components/BuildingBlocks/DonutGraph.jsx b/app/panel/components/BuildingBlocks/DonutGraph.jsx index 516d997ba..f22a79e84 100644 --- a/app/panel/components/BuildingBlocks/DonutGraph.jsx +++ b/app/panel/components/BuildingBlocks/DonutGraph.jsx @@ -56,6 +56,8 @@ class DonutGraph extends React.Component { return '#87d7ef'; case 'social_media': return '#388ee8'; + case 'unknown': + return '#8459a5'; default: return '#e8e8e8'; } @@ -74,6 +76,7 @@ class DonutGraph extends React.Component { componentDidMount() { const { categories, + antiTracking, renderRedscale, renderGreyscale, isSmall, @@ -87,7 +90,7 @@ class DonutGraph extends React.Component { .value(d => d.value); this.prepareDonutContainer(isSmall); - this.bakeDonut(categories, { + this.bakeDonut(categories, antiTracking, { renderRedscale, renderGreyscale }); @@ -99,6 +102,7 @@ class DonutGraph extends React.Component { componentWillReceiveProps(nextProps) { const { categories, + antiTracking, renderRedscale, renderGreyscale, ghosteryFeatureSelect, @@ -115,12 +119,20 @@ class DonutGraph extends React.Component { return; } - // componentWillReceiveProps gets called many times during page load as new trackers are found + // componentWillReceiveProps gets called many times during page load as new trackers or unsafe data points are found // so only compare tracker totals if we don't already have to redraw anyway as a result of the cheaper checks above const trackerTotal = categories.reduce((total, category) => total + category.num_total, 0); const nextTrackerTotal = nextProps.categories.reduce((total, category) => total + category.num_total, 0); if (trackerTotal !== nextTrackerTotal) { this.nextPropsDonut(nextProps); + return; + } + + if (!antiTracking.unknownTrackerCount && !nextProps.antiTracking.unknownTrackerCount) { return; } + const unknownDataPoints = antiTracking.unknownTrackerCount; + const nextUnknownDataPoints = nextProps.antiTracking.unknownTrackerCount; + if (unknownDataPoints !== nextUnknownDataPoints) { + this.nextPropsDonut(nextProps); } } @@ -142,7 +154,7 @@ class DonutGraph extends React.Component { * Helper function that updates donut with nextProps values */ nextPropsDonut(nextProps) { - this.bakeDonut(nextProps.categories, { + this.bakeDonut(nextProps.categories, nextProps.antiTracking, { renderRedscale: nextProps.renderRedscale, renderGreyscale: nextProps.renderGreyscale, isSmall: nextProps.isSmall, @@ -181,7 +193,7 @@ class DonutGraph extends React.Component { */ bakeDonut = throttle(this._bakeDonut.bind(this), 600, { leading: true, trailing: true }) // eslint-disable-line react/sort-comp - _bakeDonut(categories, options) { + _bakeDonut(categories, antiTracking, options) { const { renderRedscale, renderGreyscale, @@ -209,6 +221,14 @@ class DonutGraph extends React.Component { graphData.sort((a, b) => a.value < b.value); } + if (antiTracking.unknownTrackerCount) { + graphData.push({ + id: 'unknown', + name: 'Unknown', + value: antiTracking.unknownTrackerCount, + }); + } + const trackerArc = arc() .innerRadius(this.donutRadius - 13) .outerRadius(this.donutRadius); @@ -315,7 +335,12 @@ class DonutGraph extends React.Component { * @return {JSX} JSX for rendering the donut-graph portion of the Summary View */ render() { - const { isSmall, totalCount } = this.props; + const { + isSmall, + categories, + antiTracking, + totalCount, + } = this.props; const componentClasses = ClassNames('DonutGraph', { 'DonutGraph--big': !isSmall, 'DonutGraph--small': isSmall, @@ -325,11 +350,24 @@ class DonutGraph extends React.Component { return (
- {this.props.categories.map(cat => ( - + {categories.map(cat => ( + {cat.name} ))} + {!!antiTracking.unknownTrackerCount && ( + + {t('unknown')} + + )}
{ this.node = node; }} />
diff --git a/app/panel/components/Summary.jsx b/app/panel/components/Summary.jsx index 794c68810..97983aaf5 100644 --- a/app/panel/components/Summary.jsx +++ b/app/panel/components/Summary.jsx @@ -428,6 +428,7 @@ class Summary extends React.Component { _renderDonut() { const { categories, + antiTracking, is_expert, paused_blocking, sitePolicy, @@ -437,6 +438,7 @@ class Summary extends React.Component {
Object.assign({}, state.blocking, { is_expanded: state.panel.is_expanded, language: state.panel.language, + smartBlock: state.panel.smartBlock, + enable_anti_tracking: state.panel.enable_anti_tracking, pageHost: state.summary.pageHost, paused_blocking: state.summary.paused_blocking, sitePolicy: state.summary.sitePolicy, smartBlockActive: state.panel.enable_smart_block, - smartBlock: state.panel.smartBlock, }); /** * Bind Blocking view component action creators using Redux's bindActionCreators @@ -43,7 +44,14 @@ const mapStateToProps = state => Object.assign({}, state.blocking, { * @return {function} to be used as an argument in redux connect call */ const mapDispatchToProps = dispatch => ({ - actions: bindActionCreators(Object.assign(blockingActions, { updateTrackerCounts, showNotification }), dispatch) + actions: bindActionCreators( + Object.assign(blockingActions, { + updateTrackerCounts, + showNotification, + toggleCliqzFeature + }), + dispatch + ) }); /** * Connects Blocking component to the Redux store. diff --git a/app/panel/reducers/__tests__/summary.js b/app/panel/reducers/__tests__/summary.js index fe34442fa..412422c68 100644 --- a/app/panel/reducers/__tests__/summary.js +++ b/app/panel/reducers/__tests__/summary.js @@ -35,6 +35,11 @@ const initialState = Immutable({ blocked: 0, }, tab_id: 0, + antiTracking: { + totalUnsafeCount: 0, + totalUnknownCount: 0, + unknownTrackerCount: 0, + }, }); describe('app/panel/reducers/summary.js', () => { @@ -57,11 +62,10 @@ describe('app/panel/reducers/summary.js', () => { changedData: true, newData: true }, - antitracking: { - totalUnsafeCount: 3, - unchangedData: false, - changedData: true, - newData: true + antiTracking: { + totalUnsafeCount: 5, + totalUnknownCount: 3, + unknownTrackerCount: 1 } }; const action = { data, type: UPDATE_CLIQZ_MODULE_DATA }; @@ -73,14 +77,14 @@ describe('app/panel/reducers/summary.js', () => { }, antiTracking: { totalUnsafeCount: 1, - unchangedData: false, - changedData: false + totalUnknownCount: 0, + unknownTrackerCount: 0 } }); const updatedState = Immutable.merge(initState, { adBlock: data.adblock, - antiTracking: data.antitracking + antiTracking: data.antiTracking }); expect(summaryReducer(initState, action)).toEqual(updatedState); diff --git a/app/panel/reducers/blocking.js b/app/panel/reducers/blocking.js index c0fb0e59a..c47e5fcbd 100644 --- a/app/panel/reducers/blocking.js +++ b/app/panel/reducers/blocking.js @@ -17,10 +17,13 @@ import { FILTER_TRACKERS, UPDATE_BLOCK_ALL_TRACKERS, UPDATE_CATEGORIES, + UPDATE_ANTI_TRACKING_HIDE, UPDATE_CATEGORY_BLOCKED, UPDATE_TRACKER_BLOCKED, UPDATE_TRACKER_TRUST_RESTRICT, - TOGGLE_EXPAND_ALL + UPDATE_ANTI_TRACKING_WHITELIST, + TOGGLE_EXPAND_ALL, + UPDATE_CLIQZ_MODULE_DATA } from '../constants/constants'; import { updateTrackerBlocked, updateCategoryBlocked, updateBlockAllTrackers, toggleExpandAll @@ -37,6 +40,14 @@ const initialState = { }, site_specific_unblocks: {}, site_specific_blocks: {}, + antiTracking: { + totalUnsafeCount: 0, // The amount of data points scrubbed by Anti-Tracking + totalUnknownCount: 0, // The amount of data points scrubbed by Anti-Tracking for Trackers not in the Ghostery DB + unknownTrackerCount: 0, // The amount of trackers blocked by Anti-Tracking + unknownTrackers: [], // An array of objects associated with each unknown Tracker (includes both blocked and whitelisted trackers for this site) + whitelistedUrls: {}, // An object of whitelisted url domains pointing to an object with the associated tracker name and an array of whitelisted host domains + hide: false, // Whether or not to display the Anti-Tracking blocking category + } }; /** @@ -66,6 +77,9 @@ export default (state = initialState, action) => { case UPDATE_CATEGORIES: { return Object.assign({}, state, { categories: action.data }); } + case UPDATE_ANTI_TRACKING_HIDE: { + return Object.assign({}, state, { antiTracking: action.data }); + } case UPDATE_CATEGORY_BLOCKED: { const updated = updateCategoryBlocked(state, action); return Object.assign({}, state, updated); @@ -82,6 +96,16 @@ export default (state = initialState, action) => { const updated = _updateTrackerTrustRestrict(state, action); return Object.assign({}, state, updated); } + case UPDATE_ANTI_TRACKING_WHITELIST: { + const antiTracking = _updateAntiTrackingWhitelist(state, action); + return Object.assign({}, state, { antiTracking }); + } + case UPDATE_CLIQZ_MODULE_DATA: { + const { hide } = state.antiTracking; + return Object.assign({}, state, { + antiTracking: Object.assign({}, action.data.antiTracking, { hide }) + }); + } default: return state; } @@ -152,3 +176,65 @@ const _updateTrackerTrustRestrict = (state, action) => { site_specific_blocks: updated_site_specific_blocks, }; }; + +/** + * Update site_specific_blocks/unblocks for anit-tracking whitelist + * @memberOf PanelReactReducers + * @private + * @param {Object} state current state + * @param {Object} action action which provides data + * @return {Object} updated categories and site-specific blocking counters + */ +const _updateAntiTrackingWhitelist = (state, action) => { + const updatedAntiTracking = JSON.parse(JSON.stringify(state.antiTracking)); + const { whitelistedUrls } = updatedAntiTracking; + const { unknownTracker, pageHost } = action.data; + + const addToWhitelist = () => { + unknownTracker.domains.forEach((domain) => { + if (whitelistedUrls.hasOwnProperty(domain)) { + whitelistedUrls[domain].name = unknownTracker.name; + whitelistedUrls[domain].hosts.push(pageHost); + } else { + whitelistedUrls[domain] = { + name: unknownTracker.name, + hosts: [pageHost], + }; + } + }); + }; + + const removeFromWhitelist = (domain) => { + if (!whitelistedUrls[domain]) { return; } + + whitelistedUrls[domain].hosts = whitelistedUrls[domain].hosts.filter(hostUrl => ( + hostUrl !== pageHost + )); + + if (whitelistedUrls[domain].hosts.length === 0) { + delete whitelistedUrls[domain]; + } + }; + + if (unknownTracker.whitelisted) { + unknownTracker.domains.forEach(removeFromWhitelist); + + Object.keys(whitelistedUrls).forEach((domain) => { + if (whitelistedUrls[domain].name === unknownTracker.name) { + removeFromWhitelist(domain); + } + }); + } else { + addToWhitelist(); + } + + updatedAntiTracking.unknownTrackers.forEach((tracker) => { + if (tracker.name === unknownTracker.name) { + tracker.whitelisted = !tracker.whitelisted; + } + }); + + sendMessage('setPanelData', { anti_tracking_whitelist: whitelistedUrls }); + + return updatedAntiTracking; +}; diff --git a/app/panel/reducers/summary.js b/app/panel/reducers/summary.js index e94a644dc..1aef810d4 100644 --- a/app/panel/reducers/summary.js +++ b/app/panel/reducers/summary.js @@ -36,6 +36,11 @@ const initialState = { blocked: 0, }, tab_id: 0, + antiTracking: { + totalUnsafeCount: 0, // The amount of data points scrubbed by Anti-Tracking + totalUnknownCount: 0, // The amount of data points scrubbed by Anti-Tracking for Trackers not in the Ghostery DB + unknownTrackerCount: 0, // The amount of trackers blocked by Anti-Tracking + } }; /** * Default export for summary view reducer. @@ -51,7 +56,11 @@ export default (state = initialState, action) => { return Object.assign({}, state, action.data); } case UPDATE_CLIQZ_MODULE_DATA: { - return Object.assign({}, state, { adBlock: action.data.adblock, antiTracking: action.data.antitracking }); + const { totalUnsafeCount, totalUnknownCount, unknownTrackerCount } = action.data.antiTracking; + return Object.assign({}, state, { + adBlock: action.data.adblock, + antiTracking: { totalUnsafeCount, totalUnknownCount, unknownTrackerCount }, + }); } case UPDATE_GHOSTERY_PAUSED: { return Object.assign({}, state, { paused_blocking: action.data.ghosteryPaused, paused_blocking_timeout: action.data.time }); @@ -77,7 +86,7 @@ export default (state = initialState, action) => { }; /** - * Update blacklist / whitelist + * Update site blacklist / whitelist * @memberOf PanelReactReducers * @private * diff --git a/app/scss/partials/_blocking_category.scss b/app/scss/partials/_blocking_category.scss index 637aa399f..9f0cb86cc 100644 --- a/app/scss/partials/_blocking_category.scss +++ b/app/scss/partials/_blocking_category.scss @@ -5,7 +5,7 @@ * https://www.ghostery.com/ * * Copyright 2019 Ghostery, Inc. All rights reserved. - * + * * 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 @@ -56,6 +56,13 @@ top: 0; z-index: 1; height: 62px; + + &.anti-tracking-header { + height: 80px; + border-top: 0; + padding-top: 0; + } + .caret-down { @extend %pointer; display: inline-block; @@ -119,6 +126,10 @@ max-width: 125px; overflow-y: hidden; @extend %nowrap; + + &.ghosty-blue { + color: #00AEF0; + } } } .checkbox-container { @@ -129,4 +140,28 @@ fill: $alabaster; } } + .Category__antiTrackingDivider { + background-color: #00AEF0; + width: 110%; + height: 18px; + margin-bottom: 9px; + } + .Category__antiTrackingDividerText { + margin: 0; + padding-top: 2px; + padding-left: 15px; + font-size: 10px; + color: white; + } + .Category__antiTrackingCaret { + position: relative; + bottom: 14px; + left: 8px; + } + .Category__antiTrackingButton { + display: inline-block; + position: relative; + left: 4px; + bottom: 4px + } } diff --git a/app/scss/partials/_blocking_header.scss b/app/scss/partials/_blocking_header.scss index 1c8a43972..5962e457d 100644 --- a/app/scss/partials/_blocking_header.scss +++ b/app/scss/partials/_blocking_header.scss @@ -61,6 +61,8 @@ line-height: 19px; &.disabled { color: #acacac; + pointer-events: none; + visibility: hidden; } } .caret-down { diff --git a/app/scss/partials/_blocking_tracker.scss b/app/scss/partials/_blocking_tracker.scss index 08668f579..33b5af48c 100644 --- a/app/scss/partials/_blocking_tracker.scss +++ b/app/scss/partials/_blocking_tracker.scss @@ -27,6 +27,9 @@ font-weight: 400; color: #4a4a4a; @extend %nowrap; + &.is-whitelisted { + color: #00AEF0; + } } .trk-cliqz-stats-outer-container { // prevent jitter from breaking to new line @@ -108,6 +111,10 @@ margin-right: 10px; overflow-y: hidden; @extend %nowrap; + &.unknown { + margin: 0 10px 0 0; + color: #4a4a4a; + } } } svg.blocking-icons { @@ -127,6 +134,57 @@ display: block; } } + + .unknown-svg-container { + position: relative; + top: 3px; + width: 42px; + display: flex; + align-items: center; + justify-content: space-between; + + &:not(.whitelisted) { + .anti-track-trust { + visibility: hidden; + cursor: pointer; + .border { stroke: #d8d8d8; } + .background { fill: #f7f7f7; } + .trust-circle { stroke: #9B9B9B; } + } + + .anti-track-scrub { + pointer-events: none; + } + } + + &.whitelisted { + flex-direction: row-reverse; + + .anti-track-trust { + pointer-events: none; + } + .anti-track-scrub { + visibility: hidden; + pointer-events: auto; + cursor: pointer; + .border { stroke: #d8d8d8; } + .background { fill: #f7f7f7; } + .shield { stroke: #9B9B9B; } + } + } + + &:hover { + .anti-track-trust { + visibility: visible; + } + } + + &.whitelisted:hover { + .anti-track-scrub { + visibility: visible; + } + } + } } // SVG icons shared with _blocking_category @@ -167,3 +225,7 @@ svg.blocking-icons { stroke-linecap: square; } } + +.OtherDataPoint__svgGroup { + padding-top: 5px; +} diff --git a/app/scss/partials/_cliqz_feature.scss b/app/scss/partials/_cliqz_feature.scss index 5379840d0..d88f0b5e2 100644 --- a/app/scss/partials/_cliqz_feature.scss +++ b/app/scss/partials/_cliqz_feature.scss @@ -73,6 +73,19 @@ $cliqz-feature--darker-gray: #a4a4a4; .CliqzFeature__feature-name { display: none; } } +.CliqzFeature--tiny { + .CliqzFeature__status { + line-height: 17px; + font-size: 9px; + } + .CliqzFeature__icon { + height: 28px; + width: 28px; + background-size: 26px 26px; + } + .CliqzFeature__feature-name { display: none; } +} + // Modifications to element styling based on state-related block-level modifiers // The CliqzFeature block is always either active or inactive // and always either clickable or not-clickable diff --git a/app/scss/partials/_tooltip.scss b/app/scss/partials/_tooltip.scss index dbb63dac8..9ae6d4711 100644 --- a/app/scss/partials/_tooltip.scss +++ b/app/scss/partials/_tooltip.scss @@ -201,3 +201,7 @@ margin-left: 0 !important; } } + +.Category__antiTrackingButton .tooltip-content.left { + top: -27px; +} diff --git a/src/background.js b/src/background.js index 423a41ba0..6172b3b43 100644 --- a/src/background.js +++ b/src/background.js @@ -1277,9 +1277,11 @@ function initialiseWebRequestPipeline() { * @return {boolean} */ function isWhitelisted(state) { - const url = state.tabUrl; + const hostUrl = utils.processUrl(state.tabUrl).host; + const trackerUrl = utils.processUrl(state.url).host; + // state.ghosteryWhitelisted is sometimes undefined so force to bool - return Boolean(globals.SESSION.paused_blocking || events.policy.getSitePolicy(url) === 2 || state.ghosteryWhitelisted); + return Boolean(globals.SESSION.paused_blocking || events.policy.getSitePolicy(hostUrl, trackerUrl) === 2 || state.ghosteryWhitelisted); } /** diff --git a/src/classes/BrowserButton.js b/src/classes/BrowserButton.js index ffdc820aa..8a419945d 100644 --- a/src/classes/BrowserButton.js +++ b/src/classes/BrowserButton.js @@ -17,7 +17,7 @@ import conf from './Conf'; import foundBugs from './FoundBugs'; import rewards from './Rewards'; import Policy from './Policy'; -import { getCliqzAntiTrackingCount, getCliqzAdBlockingCount } from '../utils/cliqzModulesData'; +import { getCliqzAntiTrackingData, getCliqzAdBlockingCount } from '../utils/cliqzModulesData'; import { getTab } from '../utils/utils'; import { log } from '../utils/common'; import globals from './Globals'; @@ -146,7 +146,7 @@ class BrowserButton { const { appsCount, appsAlertCount } = this._getTrackerCount(tabId); const adBlockingCount = getCliqzAdBlockingCount(tabId).totalCount; - const antiTrackingCount = getCliqzAntiTrackingCount(tabId).totalUnsafeCount; + const antiTrackingCount = getCliqzAntiTrackingData(tabId).totalUnsafeCount; alert = (appsAlertCount > 0); trackerCount = (appsCount + antiTrackingCount + adBlockingCount).toString(); @@ -155,7 +155,7 @@ class BrowserButton { if (trackerCount === '') { this._setIcon(false, tabId, trackerCount, alert); } else { - this._setIcon(!globals.SESSION.paused_blocking && !this.policy.whitelisted(tab.url), tabId, trackerCount, alert); + this._setIcon(!globals.SESSION.paused_blocking && !this.policy.checkSiteWhitelist(tab.url), tabId, trackerCount, alert); } } diff --git a/src/classes/ConfData.js b/src/classes/ConfData.js index 54c86231d..ebce5ac25 100644 --- a/src/classes/ConfData.js +++ b/src/classes/ConfData.js @@ -150,6 +150,7 @@ class ConfData { _initProperty('site_specific_blocks', {}); _initProperty('site_specific_unblocks', {}); _initProperty('site_whitelist', []); + _initProperty('anti_tracking_whitelist', {}); _initProperty('surrogates', {}); _initProperty('version_history', []); _initProperty('account', null); diff --git a/src/classes/PanelData.js b/src/classes/PanelData.js index b84c929cb..123705a70 100644 --- a/src/classes/PanelData.js +++ b/src/classes/PanelData.js @@ -550,7 +550,11 @@ class PanelData { _postCliqzModulesData() { if (!this._panelPort || !this._activeTab) { return; } - sendCliqzModuleCounts(this._activeTab.id, this.postMessageToSummary); + sendCliqzModuleCounts( + this._activeTab.id, + this._activeTab.pageHost, + this.postMessageToSummary, + ); } /** diff --git a/src/classes/Policy.js b/src/classes/Policy.js index e982e93f7..bfc02f996 100644 --- a/src/classes/Policy.js +++ b/src/classes/Policy.js @@ -17,7 +17,6 @@ import c2pDb from './Click2PlayDb'; import conf from './Conf'; -import { processUrl } from '../utils/utils'; import globals from './Globals'; /** @@ -46,32 +45,32 @@ class Policy { * @param {string} url site url * @return {boolean} */ - getSitePolicy(url) { - if (this.blacklisted(url)) { + getSitePolicy(hostUrl, trackerUrl) { + if (this.blacklisted(hostUrl)) { return globals.BLACKLISTED; } - if (this.whitelisted(url)) { + if (this.checkSiteWhitelist(hostUrl) + || this.checkAntiTrackingWhitelist(hostUrl, trackerUrl)) { return globals.WHITELISTED; } return false; } /** - * Check given url against whitelist + * Check given url against site whitelist * @param {string} url site url * @return {string|boolean} corresponding whitelist entry or false, if none */ - whitelisted(url) { + checkSiteWhitelist(url) { if (url) { - url = processUrl(url).hostname; - url = url.replace(/^www\./, ''); + const replacedUrl = url.replace(/^www\./, ''); const sites = conf.site_whitelist || []; const num_sites = sites.length; // TODO: speed up for (let i = 0; i < num_sites; i++) { // TODO match from the beginning of the string to avoid false matches (somewhere in the querystring for instance) - if (url === sites[i]) { + if (replacedUrl === sites[i]) { return sites[i]; } } @@ -80,6 +79,28 @@ class Policy { return false; } + /** + * Check given url against anti-tracking whitelist + * @param {string} url site url + * @return {string|boolean} corresponding whitelist entry or false, if none + */ + checkAntiTrackingWhitelist(hostUrl, trackerUrl) { + let isWhitelisted = false; + const antiTrackingWhitelist = conf.anti_tracking_whitelist; + + if (antiTrackingWhitelist[trackerUrl]) { + antiTrackingWhitelist[trackerUrl].hosts.some((host) => { + if (host === hostUrl) { + isWhitelisted = true; + return true; + } + return false; + }); + } + + return isWhitelisted; + } + /** * Check given url against blacklist * @param {string} url site url @@ -87,15 +108,14 @@ class Policy { */ blacklisted(url) { if (url) { - url = processUrl(url).hostname; - url = url.replace(/^www\./, ''); + const replacedUrl = url.replace(/^www\./, ''); const sites = conf.site_blacklist || []; const num_sites = sites.length; // TODO: speed up for (let i = 0; i < num_sites; i++) { // TODO match from the beginning of the string to avoid false matches (somewhere in the querystring for instance) - if (url === sites[i]) { + if (replacedUrl === sites[i]) { return sites[i]; } } diff --git a/src/classes/PurpleBox.js b/src/classes/PurpleBox.js index 5e8cfa659..8f3cf7bbb 100644 --- a/src/classes/PurpleBox.js +++ b/src/classes/PurpleBox.js @@ -48,7 +48,7 @@ class PurpleBox { // Skip in the event of pause, trust, prefetching, newtab page, or Firefox about:pages if (!conf.show_alert || globals.SESSION.paused_blocking || - (conf.hide_alert_trusted && !!this.policy.whitelisted(tab.url)) || + (conf.hide_alert_trusted && !!this.policy.checkSiteWhitelist(tab.url)) || !tab || tab.purplebox || tab.path.includes('_/chrome/newtab') || tab.protocol === 'about' || globals.EXCLUDES.includes(tab.host)) { return Promise.resolve(false); } diff --git a/src/utils/cliqzModulesData.js b/src/utils/cliqzModulesData.js index b22974133..84b8f2282 100644 --- a/src/utils/cliqzModulesData.js +++ b/src/utils/cliqzModulesData.js @@ -26,25 +26,64 @@ const { adblocker, antitracking } = cliqz.modules; * @param {int} tabId * @return {object} totalUnsafeCount */ -export function getCliqzAntiTrackingCount(tabId) { - let count = 0; +export function getCliqzAntiTrackingData(tabId, tabHostUrl) { + let totalUnsafeCount = 0; + let totalUnknownCount = 0; + let unknownTrackerCount = 0; if (!conf.enable_anti_tracking || !antitracking.background) { return { - totalUnsafeCount: count + totalUnsafeCount, + totalUnknownCount, }; } // Count up number of fingerprints and cookies found const { bugs, others } = antitracking.background.actions.getGhosteryStats(tabId); - const allStats = Object.assign({}, bugs, others); - const values = Object.values(allStats); + const bugsValues = Object.values(bugs); + const othersValues = Object.values(others); - for (const val of values) { - count += val.cookies + val.fingerprints; + const unknownTrackers = []; + + for (const bug of bugsValues) { + totalUnsafeCount += bug.cookies + bug.fingerprints; + } + + for (const other of othersValues) { + let whitelisted = false; + const scrubbed = other.cookies || other.fingerprints; + + other.domains.some((domain) => { + if (conf.anti_tracking_whitelist[domain] + && conf.anti_tracking_whitelist[domain].hosts.includes(tabHostUrl)) { + whitelisted = true; + return true; + } + return false; + }); + + if (scrubbed) { + totalUnsafeCount += other.cookies + other.fingerprints; + totalUnknownCount += other.cookies + other.fingerprints; + unknownTrackerCount += 1; + } + + if (scrubbed || whitelisted) { + const { + name, domains, ads, cookies, fingerprints + } = other; + + unknownTrackers.push({ + name, domains, ads, cookies, fingerprints, whitelisted + }); + } } return { - totalUnsafeCount: count + totalUnsafeCount, + unknownTrackers, + unknownTrackerCount, + totalUnknownCount, + whitelistedUrls: conf.anti_tracking_whitelist, }; } @@ -92,10 +131,10 @@ export function getCliqzGhosteryBugs(tabId) { * @param {int} tabId * @param {Function} callback */ -export function sendCliqzModuleCounts(tabId, callback) { +export function sendCliqzModuleCounts(tabId, tabHostUrl, callback) { const modules = { adblock: {}, antitracking: {} }; modules.adblock = getCliqzAdBlockingCount(tabId); - modules.antitracking = getCliqzAntiTrackingCount(tabId); + modules.antiTracking = getCliqzAntiTrackingData(tabId, tabHostUrl); callback(modules); }