Skip to content

Commit c8516f7

Browse files
committed
Prevent tracking from l.facebook.com and from google search
1 parent d2c46d2 commit c8516f7

14 files changed

+326
-85
lines changed

Clean Links Extension/Base.lproj/SafariExtensionViewController.xib

+7-7
Original file line numberDiff line numberDiff line change
@@ -15,25 +15,25 @@
1515
<customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
1616
<customObject id="-3" userLabel="Application" customClass="NSObject"/>
1717
<customView id="c22-O7-iKe">
18-
<rect key="frame" x="0.0" y="0.0" width="320" height="346"/>
18+
<rect key="frame" x="0.0" y="0.0" width="420" height="350"/>
1919
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
2020
<subviews>
2121
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="4Iy-aV-wGF">
22-
<rect key="frame" x="18" y="320" width="284" height="16"/>
22+
<rect key="frame" x="18" y="324" width="384" height="16"/>
2323
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="center" title="Clean Links" id="2Ec-kd-q2K">
2424
<font key="font" metaFont="system"/>
2525
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
2626
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
2727
</textFieldCell>
2828
</textField>
29-
<scrollView wantsLayer="YES" borderType="none" autohidesScrollers="YES" horizontalLineScroll="10" horizontalPageScroll="10" verticalLineScroll="10" verticalPageScroll="10" usesPredominantAxisScrolling="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Lm4-Zx-gFr">
30-
<rect key="frame" x="20" y="20" width="280" height="280"/>
29+
<scrollView wantsLayer="YES" borderType="none" autohidesScrollers="YES" horizontalLineScroll="10" horizontalPageScroll="10" verticalLineScroll="10" verticalPageScroll="10" hasHorizontalScroller="NO" usesPredominantAxisScrolling="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Lm4-Zx-gFr">
30+
<rect key="frame" x="20" y="20" width="380" height="284"/>
3131
<clipView key="contentView" id="QO7-Bv-rf1">
32-
<rect key="frame" x="0.0" y="0.0" width="280" height="280"/>
32+
<rect key="frame" x="0.0" y="0.0" width="380" height="284"/>
3333
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
3434
<subviews>
3535
<collectionView id="gig-jR-nb2">
36-
<rect key="frame" x="0.0" y="0.0" width="280" height="158"/>
36+
<rect key="frame" x="0.0" y="0.0" width="380" height="284"/>
3737
<autoresizingMask key="autoresizingMask" widthSizable="YES"/>
3838
<collectionViewFlowLayout key="collectionViewLayout" minimumInteritemSpacing="10" minimumLineSpacing="10" id="83i-BK-0Zv">
3939
<size key="itemSize" width="50" height="50"/>
@@ -43,7 +43,7 @@
4343
</subviews>
4444
</clipView>
4545
<scroller key="horizontalScroller" hidden="YES" wantsLayer="YES" verticalHuggingPriority="750" horizontal="YES" id="wYb-Xg-RvU">
46-
<rect key="frame" x="0.0" y="264" width="280" height="16"/>
46+
<rect key="frame" x="-100" y="-100" width="280" height="16"/>
4747
<autoresizingMask key="autoresizingMask"/>
4848
</scroller>
4949
<scroller key="verticalScroller" hidden="YES" wantsLayer="YES" verticalHuggingPriority="750" doubleValue="1" horizontal="NO" id="Rf7-cz-SYA">

Clean Links Extension/CleanedLinkItem.swift

+2
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ class CleanedLinkItem: NSCollectionViewItem {
1212

1313
@IBOutlet weak var label: NSTextField!
1414

15+
@IBOutlet weak var image: NSImageView!
16+
1517
override func viewDidLoad() {
1618
super.viewDidLoad()
1719

Clean Links Extension/CleanedLinkItem.xib

+8-6
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@
88
<objects>
99
<customObject id="-2" userLabel="File's Owner" customClass="CleanedLinkItem" customModule="Clean_Links_Extension" customModuleProvider="target">
1010
<connections>
11-
<outlet property="label" destination="EFE-mq-Ih1" id="kKJ-bT-aqe"/>
11+
<outlet property="image" destination="ZDh-QL-aK6" id="tlX-DP-Ujk"/>
12+
<outlet property="label" destination="uOL-7w-uw3" id="FzO-fi-cO2"/>
1213
<outlet property="view" destination="Hz6-mo-xeY" id="0bl-1N-x8E"/>
1314
</connections>
1415
</customObject>
@@ -25,21 +26,22 @@
2526
</constraints>
2627
<imageCell key="cell" refusesFirstResponder="YES" alignment="left" imageScaling="proportionallyDown" image="ShieldCheck" id="yaI-I7-GOp"/>
2728
</imageView>
28-
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" misplaced="YES" translatesAutoresizingMaskIntoConstraints="NO" id="EFE-mq-Ih1">
29-
<rect key="frame" x="36" y="7" width="246" height="16"/>
30-
<textFieldCell key="cell" alignment="left" title="Label" id="SmV-5r-EeC">
29+
<textField verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" translatesAutoresizingMaskIntoConstraints="NO" id="uOL-7w-uw3">
30+
<rect key="frame" x="33" y="11" width="244" height="16"/>
31+
<textFieldCell key="cell" selectable="YES" alignment="left" title="Multiline Label" id="CCb-DO-I7G">
3132
<font key="font" metaFont="system"/>
3233
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
3334
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
3435
</textFieldCell>
3536
</textField>
3637
</subviews>
3738
<constraints>
38-
<constraint firstItem="ZDh-QL-aK6" firstAttribute="centerY" secondItem="EFE-mq-Ih1" secondAttribute="centerY" id="5Ac-Ze-4RM"/>
39-
<constraint firstItem="EFE-mq-Ih1" firstAttribute="leading" secondItem="ZDh-QL-aK6" secondAttribute="trailing" constant="8" symbolic="YES" id="6d4-ZS-ien"/>
39+
<constraint firstItem="uOL-7w-uw3" firstAttribute="leading" secondItem="ZDh-QL-aK6" secondAttribute="trailing" constant="5" id="8wk-I0-d07"/>
40+
<constraint firstAttribute="trailing" secondItem="uOL-7w-uw3" secondAttribute="trailing" constant="5" id="AZZ-SY-evf"/>
4041
<constraint firstAttribute="bottom" secondItem="ZDh-QL-aK6" secondAttribute="bottom" id="JeM-kv-Qjm"/>
4142
<constraint firstItem="ZDh-QL-aK6" firstAttribute="leading" secondItem="Hz6-mo-xeY" secondAttribute="leading" id="UWF-MX-h6E"/>
4243
<constraint firstItem="ZDh-QL-aK6" firstAttribute="top" secondItem="Hz6-mo-xeY" secondAttribute="top" id="jAe-eT-Nd9"/>
44+
<constraint firstItem="uOL-7w-uw3" firstAttribute="top" secondItem="Hz6-mo-xeY" secondAttribute="top" constant="5" id="r8f-G8-mic"/>
4345
</constraints>
4446
<point key="canvasLocation" x="139" y="154"/>
4547
</customView>

Clean Links Extension/DataStructures.swift

+9-3
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,13 @@
88

99
import Foundation
1010

11-
struct CleanedParameter {
12-
let name: String
13-
let path: String
11+
enum EventType {
12+
case parameter
13+
case linkTracker
14+
}
15+
16+
struct Event {
17+
let type: EventType
18+
let domain: String
19+
let value: String
1420
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
//
2+
// ExternalLinkGuard.swift
3+
// Clean Links Extension
4+
//
5+
// Created by Radoslav Vitanov on 15.03.20.
6+
// Copyright © 2020 Radoslav Vitanov. All rights reserved.
7+
//
8+
9+
import Foundation
10+
11+
class ExternalLinkGuard {
12+
var components: URLComponents
13+
14+
init(components: URLComponents) {
15+
self.components = components
16+
}
17+
18+
func cleanUrl() -> String? {
19+
if isGoogleTrackingUrl() {
20+
return removeGoogleRedirect()
21+
}
22+
23+
if isFacebookTrackingUrl() {
24+
return removeFacebookRedirect()
25+
}
26+
27+
return nil
28+
}
29+
30+
func isGoogleTrackingUrl() -> Bool {
31+
NSLog("\(components.path)")
32+
if (components.host!.contains("google") && components.path == "/url") {
33+
return true
34+
}
35+
36+
return false
37+
}
38+
39+
func isFacebookTrackingUrl() -> Bool {
40+
if (components.host! == "l.facebook.com") {
41+
return true
42+
}
43+
44+
return false
45+
}
46+
47+
func removeGoogleRedirect() -> String {
48+
if components.query == nil {
49+
return components.string!
50+
}
51+
52+
let urlParam = components.queryItems?.first(where: { (item) -> Bool in
53+
item.name == "url"
54+
})
55+
56+
if let url = urlParam?.value {
57+
return url
58+
}
59+
60+
return components.string!
61+
}
62+
63+
func removeFacebookRedirect() -> String {
64+
if components.query == nil {
65+
return components.string!
66+
}
67+
68+
let urlParam = components.queryItems?.first(where: { (item) -> Bool in
69+
item.name == "u"
70+
})
71+
72+
if let url = urlParam?.value {
73+
return url
74+
}
75+
76+
return components.string!
77+
}
78+
}

Clean Links Extension/SafariExtensionHandler.swift

+26-4
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ class SafariExtensionHandler: SFSafariExtensionHandler {
4141
*/
4242
static var willOpenNewTab: Bool = false
4343

44-
static var cleanedParameters: [CleanedParameter] = [];
44+
static var events: [Event] = [];
4545

4646
/**
4747
* Handle messages coming from the DOM
@@ -50,14 +50,23 @@ class SafariExtensionHandler: SFSafariExtensionHandler {
5050
if (messageName == "keypress") {
5151
SafariExtensionHandler.willOpenNewTab = userInfo!["new-tab"] as! Bool;
5252
}
53+
54+
if (messageName == "prevented-redirect") {
55+
let domain = userInfo!["domain"] as! String
56+
let url = userInfo!["url"] as! String
57+
58+
SafariExtensionHandler.events.insert(
59+
Event(type: EventType.linkTracker, domain: domain, value: url),
60+
at: 0)
61+
}
5362
}
5463

5564
override func toolbarItemClicked(in window: SFSafariWindow) {
5665

5766
}
5867

5968
override func validateToolbarItem(in window: SFSafariWindow, validationHandler: @escaping ((Bool, String) -> Void)) {
60-
let badge = SafariExtensionHandler.cleanedParameters.count > 0 ? String(SafariExtensionHandler.cleanedParameters.count) : ""
69+
let badge = SafariExtensionHandler.events.count > 0 ? String(SafariExtensionHandler.events.count) : ""
6170

6271
validationHandler(true, "\(badge)")
6372
}
@@ -67,7 +76,7 @@ class SafariExtensionHandler: SFSafariExtensionHandler {
6776
}
6877

6978
override func popoverWillShow(in window: SFSafariWindow) {
70-
SafariExtensionViewController.shared.items = SafariExtensionHandler.cleanedParameters
79+
SafariExtensionViewController.shared.items = SafariExtensionHandler.events
7180
}
7281

7382
override func page(_ page: SFSafariPage, willNavigateTo url: URL?) {
@@ -91,14 +100,27 @@ class SafariExtensionHandler: SFSafariExtensionHandler {
91100
return
92101
}
93102

103+
let linkGuard = ExternalLinkGuard(components: components)
104+
105+
if let link = linkGuard.cleanUrl() {
106+
let parsedLink = URLComponents(string: link)!
107+
SafariExtensionHandler.events.insert(Event(type: EventType.linkTracker, domain: components.host!, value: parsedLink.host!
108+
), at: 0)
109+
110+
page.getContainingTab(completionHandler: {
111+
$0.navigate(to: URL(string: link)!)
112+
})
113+
return
114+
}
115+
94116
// Parse the query parameters, if there are any
95117
if let queryItems = components.queryItems {
96118
// Filter out the blacklisted params
97119
components.queryItems = queryItems.filter({
98120
let isBlacklisted = blacklist.contains($0.name);
99121

100122
if (isBlacklisted) {
101-
SafariExtensionHandler.cleanedParameters.insert(CleanedParameter(name: $0.name, path: components.host!
123+
SafariExtensionHandler.events.insert(Event(type: EventType.parameter, domain: components.host!, value: $0.name
102124
), at: 0)
103125
}
104126

Clean Links Extension/SafariExtensionViewController.swift

+49-23
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import SafariServices
1010

1111
class SafariExtensionViewController: SFSafariExtensionViewController {
1212

13-
var items: [CleanedParameter] = [] {
13+
var items: [Event] = [] {
1414
didSet {
1515
DispatchQueue.main.async {
1616
self.collectionView.reloadData()
@@ -32,7 +32,7 @@ class SafariExtensionViewController: SFSafariExtensionViewController {
3232
collectionView.backgroundColors = [.clear]
3333
collectionView.dataSource = self
3434
collectionView.delegate = self
35-
collectionView.register(CleanedLinkItem.self, forItemWithIdentifier: NSUserInterfaceItemIdentifier(rawValue: "CleanedLinkItem")
35+
collectionView.register(CleanedLinkItem.self, forItemWithIdentifier: NSUserInterfaceItemIdentifier(rawValue: "Event")
3636
)
3737

3838
}
@@ -63,30 +63,52 @@ extension NSMutableAttributedString {
6363
self.append(NSAttributedString(string: value, attributes:attributes))
6464
return self
6565
}
66+
}
6667

67-
func underlined(_ value:String) -> NSMutableAttributedString {
68-
69-
let attributes:[NSAttributedString.Key : Any] = [
70-
.font : normalFont,
71-
.underlineStyle : NSUnderlineStyle.single.rawValue
68+
extension SafariExtensionViewController: NSCollectionViewDelegateFlowLayout, NSCollectionViewDataSource {
69+
private func getLabel(item: Event) -> NSMutableAttributedString {
70+
switch item.type {
71+
case EventType.linkTracker:
72+
return NSMutableAttributedString()
73+
.normal("Prevented redirect from ")
74+
.bold("\(item.domain)")
75+
.normal(" to ")
76+
.bold("\(item.value)")
77+
default:
78+
return NSMutableAttributedString()
79+
.normal("Removed ")
80+
.bold("\(item.value)")
81+
.normal(" from ")
82+
.bold("\(item.domain)")
83+
}
84+
}
85+
86+
private func estimateFrameForText(text: String) -> CGRect {
87+
let height: CGFloat = 30
7288

73-
]
89+
let size = CGSize(width: collectionView.frame.size.width, height: height)
90+
let options = NSString.DrawingOptions.usesFontLeading.union(.usesLineFragmentOrigin)
91+
let attributes = [NSAttributedString.Key.font: NSFont.boldSystemFont(ofSize: 14)]
7492

75-
self.append(NSAttributedString(string: value, attributes:attributes))
76-
return self
93+
return NSString(string: text).boundingRect(with: size, options: options, attributes: attributes, context: nil)
7794
}
78-
}
79-
80-
extension SafariExtensionViewController: NSCollectionViewDelegateFlowLayout, NSCollectionViewDataSource {
95+
8196
func collectionView(_ collectionView: NSCollectionView, itemForRepresentedObjectAt indexPath: IndexPath) -> NSCollectionViewItem {
8297

83-
let item = collectionView.makeItem(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: "CleanedLinkItem"), for: indexPath) as! CleanedLinkItem
98+
let item = collectionView.makeItem(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: "Event"), for: indexPath) as! CleanedLinkItem
8499

85-
item.label.attributedStringValue = NSMutableAttributedString()
86-
.normal("Removed ")
87-
.bold("\(items[indexPath.item].name)")
88-
.normal(" from ")
89-
.bold("\(items[indexPath.item].path)")
100+
var image: NSImage? = nil
101+
let label: NSMutableAttributedString = getLabel(item: items[indexPath.item])
102+
103+
switch items[indexPath.item].type {
104+
case EventType.linkTracker:
105+
image = NSImage(named: NSImage.Name("Link"))
106+
default:
107+
image = NSImage(named: NSImage.Name("ShieldCheck"))
108+
}
109+
110+
item.image.image = image
111+
item.label.attributedStringValue = label
90112

91113
return item
92114
}
@@ -96,10 +118,14 @@ extension SafariExtensionViewController: NSCollectionViewDelegateFlowLayout, NSC
96118
}
97119

98120
func collectionView(_ collectionView: NSCollectionView, layout collectionViewLayout: NSCollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> NSSize {
121+
//we are just measuring height so we add a padding constant to give the label some room to breathe!
122+
let padding: CGFloat = 5
123+
124+
//estimate each cell's height
125+
let text = getLabel(item: items[indexPath.item])
126+
let options = NSString.DrawingOptions.usesFontLeading.union(.usesLineFragmentOrigin)
127+
let height = text.boundingRect(with: collectionView.frame.size, options: options)
99128

100-
return NSSize(
101-
width: collectionView.frame.size.width,
102-
height: 30
103-
)
129+
return CGSize(width: collectionView.frame.size.width, height: height.size.height + padding)
104130
}
105131
}

0 commit comments

Comments
 (0)