diff --git a/Example/Pods/Pods.xcodeproj/project.pbxproj b/Example/Pods/Pods.xcodeproj/project.pbxproj index 74a2401..9369d95 100644 --- a/Example/Pods/Pods.xcodeproj/project.pbxproj +++ b/Example/Pods/Pods.xcodeproj/project.pbxproj @@ -130,6 +130,8 @@ BEB1C97E265E629C009697FB /* ICOFaviconFinder.swift in Sources */ = {isa = PBXBuildFile; fileRef = BEB1C97C265E629B009697FB /* ICOFaviconFinder.swift */; }; BEB1C984265E88C7009697FB /* FaviconURL.swift in Sources */ = {isa = PBXBuildFile; fileRef = BEB1C983265E88C7009697FB /* FaviconURL.swift */; }; BEB1C985265E88C7009697FB /* FaviconURL.swift in Sources */ = {isa = PBXBuildFile; fileRef = BEB1C983265E88C7009697FB /* FaviconURL.swift */; }; + BEB65B9726DDAD31001338BB /* WebApplicationManifestFaviconFinder.swift in Sources */ = {isa = PBXBuildFile; fileRef = BEB65B9626DDAD31001338BB /* WebApplicationManifestFaviconFinder.swift */; }; + BEB65B9826DDAD31001338BB /* WebApplicationManifestFaviconFinder.swift in Sources */ = {isa = PBXBuildFile; fileRef = BEB65B9626DDAD31001338BB /* WebApplicationManifestFaviconFinder.swift */; }; BF74BC1464A98A74BA573CD6149946EE /* SwiftSoup-iOS-dummy.m in Sources */ = {isa = PBXBuildFile; fileRef = B70DF4D0948BF795F8D2D859BB9F4E4C /* SwiftSoup-iOS-dummy.m */; }; C2814E0C1202BCA447C16F063D7A2B15 /* StringBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A843541F902172708C58DEF21537516 /* StringBuilder.swift */; }; C5DA133AC3A8F997C9FAEC89E0F03393 /* OrderedSet.swift in Sources */ = {isa = PBXBuildFile; fileRef = A48C0E7903C6D039BC6F9872364C34A7 /* OrderedSet.swift */; }; @@ -333,6 +335,7 @@ BEB1C979265E60B9009697FB /* Favicon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Favicon.swift; sourceTree = ""; }; BEB1C97C265E629B009697FB /* ICOFaviconFinder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ICOFaviconFinder.swift; sourceTree = ""; }; BEB1C983265E88C7009697FB /* FaviconURL.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FaviconURL.swift; sourceTree = ""; }; + BEB65B9626DDAD31001338BB /* WebApplicationManifestFaviconFinder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebApplicationManifestFaviconFinder.swift; sourceTree = ""; }; BFD26115771F0D79527937F6C1889B8B /* CharacterReader.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = CharacterReader.swift; path = Sources/CharacterReader.swift; sourceTree = ""; }; C0F6AC2632ABB4644644EB19F23B863D /* SwiftSoup-iOS-umbrella.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "SwiftSoup-iOS-umbrella.h"; sourceTree = ""; }; C14FE914D8AD4EC4FCB41207D11E9AE0 /* Connection.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Connection.swift; path = Sources/Connection.swift; sourceTree = ""; }; @@ -635,6 +638,7 @@ children = ( BEB1C973265E6009009697FB /* HTMLFaviconFinder.swift */, BEB1C97C265E629B009697FB /* ICOFaviconFinder.swift */, + BEB65B9626DDAD31001338BB /* WebApplicationManifestFaviconFinder.swift */, ); path = Finders; sourceTree = ""; @@ -1037,6 +1041,7 @@ B9DB71936593F19E137ED3A31348CAC6 /* FaviconError.swift in Sources */, DE18ACF3BDC3EB054A1FC3AA87C7ECCF /* FaviconFinder-iOS-dummy.m in Sources */, 8C4D05F1FB3BCC79D8AA1D6F089FAB6C /* FaviconFinder.swift in Sources */, + BEB65B9726DDAD31001338BB /* WebApplicationManifestFaviconFinder.swift in Sources */, BEB1C974265E6009009697FB /* HTMLFaviconFinder.swift in Sources */, BEB1C96A265E582F009697FB /* FaviconType.swift in Sources */, BEB1C977265E601D009697FB /* FaviconFinderProtocol.swift in Sources */, @@ -1057,6 +1062,7 @@ 9D3FD2EC45768D6C101E3672DDB7D114 /* FaviconError.swift in Sources */, 0A26803FBA8447F956FF8FFF1622C214 /* FaviconFinder-macOS-dummy.m in Sources */, D8488464572613A993C0F776DEE2A7BA /* FaviconFinder.swift in Sources */, + BEB65B9826DDAD31001338BB /* WebApplicationManifestFaviconFinder.swift in Sources */, BEB1C975265E6009009697FB /* HTMLFaviconFinder.swift in Sources */, BEB1C96B265E582F009697FB /* FaviconType.swift in Sources */, BEB1C978265E601D009697FB /* FaviconFinderProtocol.swift in Sources */, diff --git a/Example/Tests/FaviconFinderTests.swift b/Example/Tests/FaviconFinderTests.swift index 3ba1dad..e2e0d4a 100644 --- a/Example/Tests/FaviconFinderTests.swift +++ b/Example/Tests/FaviconFinderTests.swift @@ -11,9 +11,11 @@ import XCTest @testable import FaviconFinder class FaviconFinderTests: XCTestCase { + let googleUrl = URL(string: "https://google.com")! let appleUrl = URL(string: "https://apple.com")! let realFaviconGeneratorUrl = URL(string: "https://realfavicongenerator.net/blog/apple-touch-icon-the-good-the-bad-the-ugly/")! + let webApplicationManifestUrl = URL(string: "https://googlechrome.github.io/samples/web-application-manifest/")! override func setUp() { @@ -26,30 +28,103 @@ class FaviconFinderTests: XCTestCase { func testFaviconIcoFind() { let expectation = self.expectation(description: "Favicon.ico FaviconFind") - FaviconFinder(url: self.googleUrl).downloadFavicon { result in + FaviconFinder( + url: self.googleUrl, + preferredType: .ico, + preferences: [:], + logEnabled: true + ).downloadFavicon { result in switch result { - case .success: + case .success(let favicon): + // Ensure that our favicon is actually valid + XCTAssertTrue(favicon.image.isValidImage) + + // Ensure that our favicon was retrieved from the desired source + XCTAssertTrue(favicon.downloadType == .ico) + + // Let the test know that we got our favicon back expectation.fulfill() + case .failure(let error): XCTAssert(false, "Failed to download favicon.ico file: \(error)") } } - - waitForExpectations(timeout: 20.0, handler: nil) + + waitForExpectations(timeout: 5.0, handler: nil) } func testFaviconHtmlFind() { let expectation = self.expectation(description: "HTML FaviconFind") - FaviconFinder(url: self.realFaviconGeneratorUrl).downloadFavicon { result in + FaviconFinder( + url: self.realFaviconGeneratorUrl, + preferredType: .html, + preferences: [:], + logEnabled: true + ).downloadFavicon { result in switch result { - case .success: + case .success(let favicon): + + // Ensure that our favicon is actually valid + XCTAssertTrue(favicon.image.isValidImage) + + // Ensure that our favicon was retrieved from the desired source + XCTAssertTrue(favicon.downloadType == .html) + + // Let the test know that we got our favicon back expectation.fulfill() + case .failure(let error): XCTAssert(false, "Failed to download favicon from HTML header: \(error.localizedDescription)") } } - waitForExpectations(timeout: 20.0, handler: nil) + waitForExpectations(timeout: 5.0, handler: nil) + } + + func testFaviconWebApplicationManifestFileFind() { + let expectation = self.expectation(description: "WebApplicationManifestFile FaviconFind") + + FaviconFinder( + url: self.webApplicationManifestUrl, + preferredType: .webApplicationManifestFile, + preferences: [:], + logEnabled: true + ).downloadFavicon { result in + switch result { + case .success(let favicon): + + // Ensure that our favicon is actually valid + XCTAssertTrue(favicon.image.isValidImage) + + // Ensure that our favicon was retrieved from the desired source + XCTAssertTrue(favicon.downloadType == .webApplicationManifestFile) + + // Let the test know that we got our favicon back + expectation.fulfill() + + case .failure(let error): + XCTAssert(false, "Failed to download favicon from WebApplicationManifestFile header: \(error.localizedDescription)") + } + } + + waitForExpectations(timeout: 5.0, handler: nil) } } + +private extension FaviconImage { + + var isValidImage: Bool { + #if targetEnvironment(macCatalyst) + return self.isValid + + #elseif canImport(AppKit) + return self.isValid + + #elseif canImport(UIKit) + return self.isValid + + #endif + } + +} diff --git a/FaviconFinder.podspec b/FaviconFinder.podspec index 5012431..8bddaa6 100644 --- a/FaviconFinder.podspec +++ b/FaviconFinder.podspec @@ -9,7 +9,7 @@ Pod::Spec.new do |s| s.name = "FaviconFinder" - s.version = "3.1.0" + s.version = "3.2.0" s.summary = "A pure Swift library to detect favicons use by a website." s.homepage = "https://github.com/will-lumley/FaviconFinder.git" s.license = { :type => 'MIT', :file => 'LICENSE.txt' } diff --git a/README.md b/README.md index 2f6eb21..bfcf7ae 100644 --- a/README.md +++ b/README.md @@ -13,19 +13,20 @@ FaviconFinder is a small, pure Swift library designed for iOS and macOS applications that allows you to detect favicons used by a website. -Why not just download the file that exists at `https://site.com/favicon.ico`? There are multiple places that a developer can place there favicon, not just at the root directory with the specific filename of `fav.ico`. FaviconFinder handles the dirty work for you and iterates through the numerous locations that the favicon could be located at, and simply delivers the image to you in a closure, once the image is found. +Why not just download the file that exists at `https://site.com/favicon.ico`? There are multiple places that a developer can place there favicon, not just at the root directory with the specific filename of `favicon.ico`. The favicons address may be linked within the HTML header tags, or it may be within a web application manifest JSON file, or it could even be a file with a custom filename. +FaviconFinder handles the dirty work for you and iterates through the numerous locations that the favicon could be located at, and simply delivers the image to you in a closure, once the image is found. FaviconFinder will: - [x] Detect the favicon in the root directory of the URL provided -- [x] Will automatically check if the favicon is located within the root URL if the subdomain failed (Will check `https://site.com/favicon.ico` if `https://subdomain.site.com/favicon.ico` fails) +- [x] Automatically check if the favicon is located within the root URL if the subdomain failed (Will check `https://site.com/favicon.ico` if `https://subdomain.site.com/favicon.ico` fails) - [x] Detect and parse the HTML at the URL for the declaration of the favicon -- [x] Is able to read the favicon URL, even if it's a relative URL to the subdomain that you're querying +- [x] Resolve the favicon URL for you, even if it's a relative URL to the subdomain that you're querying - [x] Allow you to prioritise which format of favicon you would like served +- [x] Detect and parse web application manifest JSON files for favicon locations To do: -- [ ] Detect and parse web application manifest JSON files - [ ] Detect and parse web application Microsoft browser configuration XML ## Usage @@ -46,28 +47,55 @@ FaviconFinder(url: url).downloadFavicon { result in } ``` -However if you're the type to want to have some fine-tuned control over what sort of favicon's we're after, you can do so. Just insert this code into your project: +## Advanced Usage + +However if you're the type to want to have some fine-tuned control over what sort of favicon's we're after, you can do so. + +FaviconFinder allows you to specify which download type you'd prefer (HTML, actual file, or web application manifest file), and then allows you to specify which favicon type you'd prefer for each download type. + +For example, you can specify that you'd prefer a HTML tag favicon, with the type of `appleTouchIcon`. FaviconFinder will then search through the HTML favicon tags for the `appleTouchIcon` type. If it cannot find the `appleTouchIcon` type, it will search for the other HTML favicon tag types. + +If the URL does not have a HTML tag that specifies the favicon, FaviconFinder will default to other download types, and will search the URL for each favicon download type until it finds one, or it'll return an error. + +Just like how you can specify which HTML favicon tag you'd prefer, you can set which filename you'd prefer when search for actual files. + +Similarly, you can specify which JSON key you'd prefer when iterating through the web application manifest file. + + +For the `.ico` download type, you can request FaviconFinder searchs for a filename of your choosing. + + +Here's how you'd make that request: + ```swift -FaviconFinder(url: url, preferredType: .html, preferences: [ - .html: FaviconType.appleTouchIcon.rawValue, - .ico: "favicon.ico" -]).downloadFavicon { result in - switch result { - case .success(let favicon): - print("URL of Favicon: \(favicon.url)") - DispatchQueue.main.async { - self.imageView.image = favicon.image + FaviconFinder( + url: url, + preferredType: .html, + preferences: [ + .html: FaviconType.appleTouchIcon.rawValue, + .ico: "favicon.ico", + .webApplicationManifestFile: FaviconType.launcherIcon4x.rawValue + ], + logEnabled: true + ).downloadFavicon { result in + switch result { + case .success(let favicon): + print("URL of Favicon: \(favicon.url)") + DispatchQueue.main.async { + self.imageView.image = favicon.image + } + + case .failure(let error): + print("Error: \(error)") } - - case .failure(let error): - print("Error: \(error)") } -} ``` This allows you to control: - What type of download type FaviconFinder will use first -- When iterating through each download type, what sub-type to look for. For the HTML download type, this allows you to prioritise different "rel" types. For the file .ico type, this allows you to choose the filename. +- When iterating through each download type, what sub-type to look for. For the HTML download type, this allows you to prioritise different "rel" types. For the file.ico type, this allows you to choose the filename. + +If your desired download type doesn't exist for your URL (ie. you requested the favicon that exists as a file but there's no file), FaviconFinder will automatically try all other methods of favicon storage for you. ## Example Project @@ -84,7 +112,7 @@ FaviconFinder is available through [CocoaPods](http://cocoapods.org). To install it, simply add the following line to your Podfile: ```ruby -pod 'FaviconFinder', '3.1.0' +pod 'FaviconFinder', '3.2.0' ``` ### Carthage @@ -92,7 +120,7 @@ FaviconFinder is also available through [Carthage](https://github.com/Carthage/C it, simply add the following line to your Cartfile: ```ruby -github "will-lumley/FaviconFinder" == 3.1.0 +github "will-lumley/FaviconFinder" == 3.2.0 ``` ### Swift Package Manager @@ -102,7 +130,7 @@ To install it, simply add the dependency to your Package.Swift file: ```swift ... dependencies: [ - .package(url: "https://github.com/will-lumley/FaviconFinder.git", from: "3.1.0"), + .package(url: "https://github.com/will-lumley/FaviconFinder.git", from: "3.2.0"), ], targets: [ .target( name: "YourTarget", dependencies: ["FaviconFinder"]), diff --git a/Sources/FaviconFinder/Classes/FaviconFinder.swift b/Sources/FaviconFinder/Classes/FaviconFinder.swift index 2906f8b..8f2b2aa 100644 --- a/Sources/FaviconFinder/Classes/FaviconFinder.swift +++ b/Sources/FaviconFinder/Classes/FaviconFinder.swift @@ -147,7 +147,9 @@ private extension FaviconFinder { print("Successfully extracted favicon from url: \(self.url)") } - let favicon = Favicon(image: image, url: url, type: type) + let downloadType = FaviconDownloadType(type: type) + + let favicon = Favicon(image: image, url: url, type: type, downloadType: downloadType) onDownload(.success(favicon)) }).resume() diff --git a/Sources/FaviconFinder/Classes/Finders/HTMLFaviconFinder.swift b/Sources/FaviconFinder/Classes/Finders/HTMLFaviconFinder.swift index 591b6a5..59d3d5e 100644 --- a/Sources/FaviconFinder/Classes/Finders/HTMLFaviconFinder.swift +++ b/Sources/FaviconFinder/Classes/Finders/HTMLFaviconFinder.swift @@ -157,29 +157,34 @@ private extension HTMLFaviconFinder { let href = mostPreferrableIcon.icon.href - //If we don't have a http or https prepended to our href, prepend our base domain - if !Regex.testForHttpsOrHttp(input: href) { - let baseRef = { () -> URL in - if let baseRef = try? html.head()?.getElementsByTag("base").attr("href"), - let baseRefUrl = URL(string: baseRef, relativeTo: self.url) { + var hrefUrl: URL? + + // If we don't have a http or https prepended to our href, prepend our base domain + if Regex.testForHttpsOrHttp(input: href) == false { + let baseRef = {() -> URL in + // Try and get the base URL from a HTML tag if we can + if let baseRef = try? html.head()?.getElementsByTag("base").attr("href"), let baseRefUrl = URL(string: baseRef, relativeTo: self.url) { return baseRefUrl - } else { + } + + // We couldn't get the base URL from a HTML tag, so we'll use the base URL that we have on hand + else { return self.url } } - guard let url = URL(string: href, relativeTo: baseRef()) else { - return nil - } - - let faviconURL = FaviconURL(url: url, type: mostPreferrableIcon.type) - return faviconURL + hrefUrl = URL(string: href, relativeTo: baseRef()) } - guard let url = URL(string: href) else { - return nil + // Our href is a proper URL, nevermind + else { + hrefUrl = URL(string: href) } + guard let url = hrefUrl else { + return nil + } + let faviconURL = FaviconURL(url: url, type: mostPreferrableIcon.type) return faviconURL } @@ -190,15 +195,28 @@ private extension HTMLFaviconFinder { - returns: The most preferred image link from our aray of icons */ func mostPreferrableIcon(icons: [HTMLFaviconReference]) -> (icon: HTMLFaviconReference, type: FaviconType)? { - if let icon = icons.first(where: { FaviconType(rawValue: $0.rel) == .appleTouchIcon }) { + + // Check for the users preferred type + if let icon = icons.first(where: { FaviconType(rawValue: $0.rel)?.rawValue == preferredType }) { + return (icon: icon, type: FaviconType(rawValue: icon.rel)!) + } + + // Check for appleTouchIcon type + else if let icon = icons.first(where: { FaviconType(rawValue: $0.rel) == .appleTouchIcon }) { return (icon: icon, type: FaviconType(rawValue: icon.rel)!) } + + // Check for appleTouchIconPrecomposed type else if let icon = icons.first(where: { FaviconType(rawValue: $0.rel) == .appleTouchIconPrecomposed }) { return (icon: icon, type: FaviconType(rawValue: icon.rel)!) } + + // Check for shortcutIcon type else if let icon = icons.first(where: { FaviconType(rawValue: $0.rel) == .shortcutIcon }) { return (icon: icon, type: FaviconType(rawValue: icon.rel)!) } + + // Check for icon type else if let icon = icons.first(where: { FaviconType(rawValue: $0.rel) == .icon }) { return (icon: icon, type: FaviconType(rawValue: icon.rel)!) } diff --git a/Sources/FaviconFinder/Classes/Finders/WebApplicationManifestFaviconFinder.swift b/Sources/FaviconFinder/Classes/Finders/WebApplicationManifestFaviconFinder.swift new file mode 100644 index 0000000..8b965ff --- /dev/null +++ b/Sources/FaviconFinder/Classes/Finders/WebApplicationManifestFaviconFinder.swift @@ -0,0 +1,323 @@ +// +// HTMLFaviconFinder.swift +// Pods +// +// Created by William Lumley on 26/5/21. +// + +import Foundation + +#if canImport(SwiftSoup) +import SwiftSoup + +#endif + +class WebApplicationManifestFaviconFinder: FaviconFinderProtocol { + + // MARK: - Types + + struct WebApplicationManifestFileReference { + let rel: String + let href: String + } + + // MARK: - Properties + + var url: URL + var preferredType: String + var logEnabled: Bool + + /// The preferred type of favicon we're after + //var preferredType: String? = FaviconType.appleTouchIcon.rawValue + + required init(url: URL, preferredType: String?, logEnabled: Bool) { + self.url = url + self.preferredType = preferredType ?? FaviconType.launcherIcon4x.rawValue //Default to `launcherIcon4x` type if user does not present us with one + self.logEnabled = logEnabled + } + + func search(onFind: @escaping ((Result) -> Void)) { + + let strongSelf = self + + // Download the web page at our URL + URLSession.shared.dataTask(with: `self`.url, completionHandler: { [unowned self] (data, response, error) in + + // Make sure our data exists + guard let data = data else { + if self.logEnabled { + print("Could NOT get favicon from url: \(self.url), Data was nil.") + } + onFind(.failure(.emptyData)) + return + } + + // Make sure we can parse the response into a string + guard let html = String(data: data, encoding: String.Encoding.utf8) else { + if self.logEnabled { + print("Could NOT get favicon from url: \(self.url), could not parse HTML.") + } + onFind(.failure(.failedToParseHTML)) + return + } + + // Get a hold of where our manifest URL is + guard let manifestURL = strongSelf.manifestUrl(from: html) else { + if self.logEnabled { + print("Could NOT get manifest file from url: \(self.url), failed to parse favicon from WebApplicationManifestFile.") + } + onFind(.failure(.failedToFindWebApplicationManifestFile)) + return + } + + // Download the manifest file + self.downloadManifestFile(from: manifestURL, onSuccess: { [unowned self] manifestData in + + // Make sure we can find a favicon in our retrieved manifest data + guard let faviconURL = strongSelf.faviconURL(from: manifestData) else { + if self.logEnabled { + print("Could NOT get favicon from url: \(self.url), failed to parse favicon from manifest data.") + } + onFind(.failure(.failedToDownloadFavicon)) + return + } + + // We found our favicon, let's download it + if self.logEnabled { + print("Extracted favicon: \(faviconURL.url.absoluteString)") + } + onFind(.success(faviconURL)) + }, onError: { error in + onFind(.failure(error)) + }) + + }).resume() + } + +} + +private extension WebApplicationManifestFaviconFinder { + + /** + Parses the provided HTML for the manifest file URL + - parameter htmlStr: The HTML that we will be parsing and iterating through to find the favicon + - returns: The URL that the manifest file can be found at + */ + func manifestUrl(from htmlStr: String) -> URL? { + var htmlOpt: Document? + do { + htmlOpt = try SwiftSoup.parse(htmlStr) + } + catch let error { + if logEnabled { + print("Could NOT parse HTML due to error: \(error). HTML: \(htmlStr)") + } + return nil + } + + guard let html = htmlOpt else { + if logEnabled { + print("Could NOT parse HTML from string: \(htmlStr)") + } + return nil + } + + guard let head = html.head() else { + if logEnabled { + print("Could NOT parse HTML head from string: \(htmlStr)") + } + return nil + } + + // Where we're going to store our HTML favicons + var fileReference: WebApplicationManifestFileReference? + + var allLinks = Elements() + do { + allLinks = try head.select("link") + } + catch let error { + if logEnabled { + print("Could NOT parse HTML due to error: \(error). HTML: \(htmlStr)") + } + return nil + } + + //Extract the 'manifest' href tag + for element in allLinks { + do { + let rel = try element.attr("rel") + let href = try element.attr("href") + + //If this is our manifest href tag + if rel == "manifest" { + fileReference = WebApplicationManifestFileReference(rel: rel, href: href) + } + } + catch let error { + if logEnabled { + print("Could NOT parse HTML due to error: \(error). HTML: \(htmlStr)") + } + continue + } + } + + guard let fileReference = fileReference else { + if logEnabled { + print("Could NOT find any HTML href tag that points to a manifest file") + } + return nil + } + + let href = fileReference.href + + var hrefUrl: URL? + + // If we don't have a http or https prepended to our href, prepend our base domain + if Regex.testForHttpsOrHttp(input: href) == false { + let baseRef = {() -> URL in + // Try and get the base URL from a HTML tag if we can + if let baseRef = try? html.head()?.getElementsByTag("base").attr("href"), let baseRefUrl = URL(string: baseRef, relativeTo: self.url) { + return baseRefUrl + } + + // We couldn't get the base URL from a HTML tag, so we'll use the base URL that we have on hand + else { + return self.url + } + } + + hrefUrl = URL(string: href, relativeTo: baseRef()) + } + + // Our href is a proper URL, nevermind + else { + hrefUrl = URL(string: href) + } + + return hrefUrl + } + + /** + Fetches and parses the manifest file from the URL provided + - parameter manifestURL: The URL that the manifest file is supposedly located at + - parameter onSuccess: The closure that will be called once we find a valid manifest file + - parameter onError: The closure that will be called if we fail to find a valid manifest file + */ + func downloadManifestFile(from manifestURL: URL, onSuccess: @escaping (Dictionary) -> (), onError: @escaping (FaviconError) -> ()) { + let request = URLRequest(url: manifestURL) + let session = URLSession(configuration: URLSessionConfiguration.default) + + let completionHandler = {(data : Data?, response : URLResponse?, error : Error?) in + + //If we can convert the NSURLResponse to an NSHTTPURLResponse + guard let urlResponse = response as? HTTPURLResponse else { + print("Could not create URLResponse from request: \(request): \(String(describing: error))") + onError(.failedToDownloadWebApplicationManifestFile) + return + } + + print("Received URL response of \(urlResponse.statusCode) for URL: \(request.url!.absoluteString)") + + guard let data = data else { + onError(.emptyData) + return + } + + do { + guard let manifestData = try JSONSerialization.jsonObject(with: data, options: .allowFragments) as? [String: Any] else { + onError(.failedToParseWebApplicationManifestFile) + return + } + + onSuccess(manifestData) + } + catch { + onError(.failedToParseWebApplicationManifestFile) + } + } + + DispatchQueue.global().async { + session.dataTask(with: request, completionHandler: completionHandler).resume() + } + } + + /** + Parses the provided manifest data for the favicon URL + - parameter htmlStr: The manifest data that we will be parsing and iterating through to find the favicon + - returns: The URL that the favicon can be found at + */ + func faviconURL(from manifestData: Dictionary) -> FaviconURL? { + guard let icons = manifestData["icons"] as? Array> else { + return nil + } + + // Get the most preferred icon + guard let mostPreferrableIcon = self.mostPreferrableIcon(iconInfos: icons) else { + return nil + } + + // Build our URL from the icon + guard let iconUrl = URL(string: mostPreferrableIcon.iconKey, relativeTo: self.url) else { + return nil + } + + return FaviconURL(url: iconUrl, type: mostPreferrableIcon.type) + } + + /** + Returns the most desirable FaviconRelType from an array of FaviconRelType + - parameter icons: Our array of our image links that we have to choose a desirable one from + - returns: The most preferred image link from our aray of icons + */ + func mostPreferrableIcon(iconInfos: Array>) -> (iconKey: String, type: FaviconType)? { + + // Check for the users preferred type + if let iconInfo = iconInfos.first(where: { iconInfo in + guard let icon = iconInfo["src"] else { return false } + return FaviconType(rawValue: icon)?.rawValue == preferredType + }) { + guard let src = iconInfo["src"] else { return nil } + return (iconKey: src, type: FaviconType(rawValue: src)!) + } + + // Check for launcherIcon4x type + else if let iconInfo = iconInfos.first(where: { iconInfo in + guard let icon = iconInfo["src"] else { return false } + return FaviconType(rawValue: icon) == .launcherIcon4x + }) { + guard let src = iconInfo["src"] else { return nil } + return (iconKey: src, type: FaviconType(rawValue: src)!) + } + + // Check for launcherIcon3x type + else if let iconInfo = iconInfos.first(where: { iconInfo in + guard let icon = iconInfo["src"] else { return false } + return FaviconType(rawValue: icon) == .launcherIcon3x + }) { + guard let src = iconInfo["src"] else { return nil } + return (iconKey: src, type: FaviconType(rawValue: src)!) + } + + // Check for launcherIcon2x type + else if let iconInfo = iconInfos.first(where: { iconInfo in + guard let icon = iconInfo["src"] else { return false } + return FaviconType(rawValue: icon) == .launcherIcon2x + }) { + guard let src = iconInfo["src"] else { return nil } + return (iconKey: src, type: FaviconType(rawValue: src)!) + } + + // Check for launcherIcon1x type + else if let iconInfo = iconInfos.first(where: { iconInfo in + guard let icon = iconInfo["src"] else { return false } + return FaviconType(rawValue: icon) == .launcherIcon1x + }) { + guard let src = iconInfo["src"] else { return nil } + return (iconKey: src, type: FaviconType(rawValue: src)!) + } + + return nil + } + +} diff --git a/Sources/FaviconFinder/Classes/Types/Favicon.swift b/Sources/FaviconFinder/Classes/Types/Favicon.swift index a7853fc..80c4a45 100644 --- a/Sources/FaviconFinder/Classes/Types/Favicon.swift +++ b/Sources/FaviconFinder/Classes/Types/Favicon.swift @@ -17,4 +17,7 @@ public struct Favicon { /// The type of favicon we extracted public let type: FaviconType + + /// The download type of the favicon we extracted + public let downloadType: FaviconDownloadType } diff --git a/Sources/FaviconFinder/Classes/Types/FaviconDownloadType.swift b/Sources/FaviconFinder/Classes/Types/FaviconDownloadType.swift index 8f27851..3fb5b7c 100644 --- a/Sources/FaviconFinder/Classes/Types/FaviconDownloadType.swift +++ b/Sources/FaviconFinder/Classes/Types/FaviconDownloadType.swift @@ -11,6 +11,7 @@ import Foundation public enum FaviconDownloadType { case html case ico + case webApplicationManifestFile static let allTypes: [FaviconDownloadType] = [ .html, @@ -22,10 +23,20 @@ internal extension FaviconDownloadType { init(type: FaviconType) { switch type { - case .ico: - self = .ico - default: - self = .html + // ICO + case .ico: self = .ico + + // HTML + case .appleTouchIcon: self = .html + case .appleTouchIconPrecomposed: self = .html + case .shortcutIcon: self = .html + case .icon: self = .html + + // Web Application Manifest File + case .launcherIcon1x: self = .webApplicationManifestFile + case .launcherIcon2x: self = .webApplicationManifestFile + case .launcherIcon3x: self = .webApplicationManifestFile + case .launcherIcon4x: self = .webApplicationManifestFile } } @@ -39,6 +50,8 @@ internal extension FaviconDownloadType { return ICOFaviconFinder(url: url, preferredType: preferredType, logEnabled: logEnabled) case .html: return HTMLFaviconFinder(url: url, preferredType: preferredType, logEnabled: logEnabled) + case .webApplicationManifestFile: + return WebApplicationManifestFaviconFinder(url: url, preferredType: preferredType, logEnabled: logEnabled) } } diff --git a/Sources/FaviconFinder/Classes/Types/FaviconError.swift b/Sources/FaviconFinder/Classes/Types/FaviconError.swift index e1a13c0..7368663 100644 --- a/Sources/FaviconFinder/Classes/Types/FaviconError.swift +++ b/Sources/FaviconFinder/Classes/Types/FaviconError.swift @@ -17,4 +17,8 @@ public enum FaviconError: Error case emptyFavicon case invalidImage case other + + case failedToFindWebApplicationManifestFile + case failedToDownloadWebApplicationManifestFile + case failedToParseWebApplicationManifestFile } diff --git a/Sources/FaviconFinder/Classes/Types/FaviconType.swift b/Sources/FaviconFinder/Classes/Types/FaviconType.swift index 04baeea..c26e6ca 100644 --- a/Sources/FaviconFinder/Classes/Types/FaviconType.swift +++ b/Sources/FaviconFinder/Classes/Types/FaviconType.swift @@ -9,6 +9,7 @@ import Foundation public enum FaviconType: String { + // HTML Types case appleTouchIcon = "apple-touch-icon" case appleTouchIconPrecomposed = "apple-touch-icon-precomposed" @@ -16,14 +17,26 @@ public enum FaviconType: String { case icon = "icon" // Filetype (ico) - case ico = "ico" + case ico = "ico" + + // Web Application Manifest File + case launcherIcon1x = "launcher-icon-1x.png" + case launcherIcon2x = "launcher-icon-2x.png" + case launcherIcon3x = "launcher-icon-3x.png" + case launcherIcon4x = "launcher-icon-4x.png" static let allTypes: [FaviconType] = [ .appleTouchIcon, .appleTouchIconPrecomposed, .shortcutIcon, .icon, - .ico + + .ico, + + .launcherIcon1x, + .launcherIcon2x, + .launcherIcon3x, + .launcherIcon4x, ] }