-
Notifications
You must be signed in to change notification settings - Fork 27
/
FaviconFetcher.swift
233 lines (198 loc) · 9.4 KB
/
FaviconFetcher.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
/* 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/. */
import Storage
import Shared
import Alamofire
import XCGLogger
import Deferred
import WebImage
private let log = Logger.browserLogger
private let queue = dispatch_queue_create("FaviconFetcher", DISPATCH_QUEUE_CONCURRENT)
class FaviconFetcherErrorType: MaybeErrorType {
let description: String
init(description: String) {
self.description = description
}
}
/* A helper class to find the favicon associated with a URL.
* This will load the page and parse any icons it finds out of it.
* If that fails, it will attempt to find a favicon.ico in the root host domain.
*/
public class FaviconFetcher : NSObject, NSXMLParserDelegate {
public static var userAgent: String = ""
static let ExpirationTime = NSTimeInterval(60*60*24*7) // Only check for icons once a week
private static var characterToFaviconCache = [String : UIImage]()
static var defaultFavicon: UIImage = {
return UIImage(named: "defaultFavicon")!
}()
class func getForURL(url: NSURL, profile: Profile) -> Deferred<Maybe<[Favicon]>> {
let f = FaviconFetcher()
return f.loadFavicons(url, profile: profile)
}
private func loadFavicons(url: NSURL, profile: Profile, oldIcons: [Favicon] = [Favicon]()) -> Deferred<Maybe<[Favicon]>> {
if isIgnoredURL(url) {
return deferMaybe(FaviconFetcherErrorType(description: "Not fetching ignored URL to find favicons."))
}
let deferred = Deferred<Maybe<[Favicon]>>()
var oldIcons: [Favicon] = oldIcons
dispatch_async(queue) { _ in
self.parseHTMLForFavicons(url).bind({ (result: Maybe<[Favicon]>) -> Deferred<[Maybe<Favicon>]> in
var deferreds = [Deferred<Maybe<Favicon>>]()
if let icons = result.successValue {
deferreds = icons.map { self.getFavicon(url, icon: $0, profile: profile) }
}
return all(deferreds)
}).bind({ (results: [Maybe<Favicon>]) -> Deferred<Maybe<[Favicon]>> in
for result in results {
if let icon = result.successValue {
oldIcons.append(icon)
}
}
oldIcons = oldIcons.sort {
return $0.width > $1.width
}
return deferMaybe(oldIcons)
}).upon({ (result: Maybe<[Favicon]>) in
deferred.fill(result)
return
})
}
return deferred
}
lazy private var alamofire: Alamofire.Manager = {
let configuration = NSURLSessionConfiguration.defaultSessionConfiguration()
configuration.timeoutIntervalForRequest = 5
return Alamofire.Manager.managerWithUserAgent(userAgent, configuration: configuration)
}()
private func fetchDataForURL(url: NSURL) -> Deferred<Maybe<NSData>> {
let deferred = Deferred<Maybe<NSData>>()
alamofire.request(.GET, url).response { (request, response, data, error) in
// Don't cancel requests just because our Manager is deallocated.
withExtendedLifetime(self.alamofire) {
if error == nil {
if let data = data {
deferred.fill(Maybe(success: data))
return
}
}
let errorDescription = (error as NSError?)?.description ?? "No content."
deferred.fill(Maybe(failure: FaviconFetcherErrorType(description: errorDescription)))
}
}
return deferred
}
// Loads and parses an html document and tries to find any known favicon-type tags for the page
private func parseHTMLForFavicons(url: NSURL) -> Deferred<Maybe<[Favicon]>> {
return fetchDataForURL(url).bind({ result -> Deferred<Maybe<[Favicon]>> in
var icons = [Favicon]()
if let data = result.successValue where result.isSuccess,
let element = RXMLElement(fromHTMLData: data) where element.isValid {
var reloadUrl: NSURL? = nil
element.iterate("head.meta") { meta in
if let refresh = meta.attribute("http-equiv") where refresh == "Refresh",
let content = meta.attribute("content"),
let index = content.rangeOfString("URL="),
let url = NSURL(string: content.substringFromIndex(index.startIndex.advancedBy(4))) {
reloadUrl = url
}
}
if let url = reloadUrl {
return self.parseHTMLForFavicons(url)
}
var bestType = IconType.NoneFound
element.iterateWithRootXPath("//head//link[contains(@rel, 'icon')]") { link in
var iconType: IconType? = nil
if let rel = link.attribute("rel") {
switch (rel) {
case "shortcut icon":
iconType = .Icon
case "icon":
iconType = .Icon
case "apple-touch-icon":
iconType = .AppleIcon
case "apple-touch-icon-precomposed":
iconType = .AppleIconPrecomposed
default:
iconType = nil
}
}
guard let href = link.attribute("href") where iconType != nil else {
return
}
if (href.endsWith(".ico")) {
iconType = .Guess
}
if let type = iconType where !bestType.isPreferredTo(type),
let iconUrl = NSURL(string: href, relativeToURL: url) {
let icon = Favicon(url: iconUrl.absoluteString, date: NSDate(), type: type)
// If we already have a list of Favicons going already, then add it…
if (type == bestType) {
icons.append(icon)
} else {
// otherwise, this is the first in a new best yet type.
icons = [icon]
bestType = type
}
}
}
// If we haven't got any options icons, then use the default at the root of the domain.
if let url = NSURL(string: "/favicon.ico", relativeToURL: url) where icons.isEmpty {
let icon = Favicon(url: url.absoluteString, date: NSDate(), type: .Guess)
icons = [icon]
}
}
return deferMaybe(icons)
})
}
func getFavicon(siteUrl: NSURL, icon: Favicon, profile: Profile) -> Deferred<Maybe<Favicon>> {
let deferred = Deferred<Maybe<Favicon>>()
let url = icon.url
let manager = SDWebImageManager.sharedManager()
let site = Site(url: siteUrl.absoluteString, title: "")
var fav = Favicon(url: url, type: icon.type)
if let url = url.asURL {
manager.downloadImageWithURL(url,
options: SDWebImageOptions.LowPriority,
progress: nil,
completed: { (img, err, cacheType, success, url) -> Void in
fav = Favicon(url: url.absoluteString,
type: icon.type)
if let img = img {
fav.width = Int(img.size.width)
fav.height = Int(img.size.height)
profile.favicons.addFavicon(fav, forSite: site)
} else {
fav.width = 0
fav.height = 0
}
deferred.fill(Maybe(success: fav))
})
} else {
return deferMaybe(FaviconFetcherErrorType(description: "Invalid URL \(url)"))
}
return deferred
}
// Returns the default favicon for a site based on the first letter of the site's domain
class func getDefaultFavicon(url: NSURL) -> UIImage {
guard let character = url.baseDomain()?.characters.first else {
return defaultFavicon
}
let faviconLetter = String(character).uppercaseString
if let cachedFavicon = characterToFaviconCache[faviconLetter] {
return cachedFavicon
}
var faviconImage = UIImage()
let faviconLabel = UILabel(frame: CGRect(x: 0, y: 0, width: TwoLineCellUX.ImageSize, height: TwoLineCellUX.ImageSize))
faviconLabel.text = faviconLetter
faviconLabel.textAlignment = .Center
faviconLabel.font = UIFont.systemFontOfSize(18, weight: UIFontWeightMedium)
faviconLabel.textColor = UIColor.grayColor()
UIGraphicsBeginImageContextWithOptions(faviconLabel.bounds.size, false, 0.0)
faviconLabel.layer.renderInContext(UIGraphicsGetCurrentContext()!)
faviconImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
characterToFaviconCache[faviconLetter] = faviconImage
return faviconImage
}
}