BankingClient/ui/BankingiOSApp/BankingiOSApp/BankIconFinder/FaviconFinder.swift

191 lines
6.8 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import Foundation
import SwiftSoup
import fints4k // TODO: get rid of this
class FaviconFinder {
private let webClient = UrlSessionWebClient() // TODO: create interface and pass from SwiftBankIconFinder
private let urlUtil = UrlUtil()
func extractFavicons(url: String, callback: @escaping ([Favicon]) -> Void) {
webClient.getAsync(url) { response in
if response.successful, let html = response.body {
callback(self.extractFavicons(url: url, html: html))
}
else {
callback([])
}
}
return callback([])
}
func extractFavicons(url: String, html: String) -> [Favicon] {
if let document = try? SwiftSoup.parse(html) {
return extractFavicons(document: document, url: url)
}
return []
}
func extractFavicons(document: Document, url: String) -> [Favicon] {
var extractedFavicons = (try? document.head()?.select("link, meta"))?.map { mapElementToFavicon($0, url) }.filter { $0 != nil } as? [Favicon] ?? []
tryToFindDefaultFavicon(url, &extractedFavicons)
return extractedFavicons
}
private func tryToFindDefaultFavicon(_ url: String, _ extractedFavicons: inout [Favicon]) {
let urlInstance = URL(string: url)
let defaultFaviconUrl = "\(urlInstance?.scheme ?? "https")://\(urlInstance?.host ?? "")/favicon.ico"
if containsIconWithUrl(extractedFavicons, defaultFaviconUrl) == false {
let response = webClient.head(defaultFaviconUrl)
if (response.successful) {
extractedFavicons.append(Favicon(url: defaultFaviconUrl, iconType: FaviconType.ShortcutIcon))
}
}
}
private func containsIconWithUrl(_ favicons: [Favicon], _ faviconUrl: String) -> Bool {
return favicons.first(where: { favicon in favicon.url == faviconUrl } ) != nil
}
/**
* Possible formats are documented here https://stackoverflow.com/questions/21991044/how-to-get-high-resolution-website-logo-favicon-for-a-given-url#answer-22007642
* and here https://en.wikipedia.org/wiki/Favicon
*/
private func mapElementToFavicon(_ linkOrMetaElement: Element, _ siteUrl: String) -> Favicon? {
if (linkOrMetaElement.nodeName() == "link") {
return mapLinkElementToFavicon(linkOrMetaElement, siteUrl)
}
else if (linkOrMetaElement.nodeName() == "meta") {
return mapMetaElementToFavicon(linkOrMetaElement, siteUrl)
}
return nil
}
private func mapLinkElementToFavicon(_ linkElement: Element, _ siteUrl: String) -> Favicon? {
if linkElement.hasAttr("rel") {
if let faviconType = getFaviconTypeForLinkElements(linkElement) {
let href = try? linkElement.attr("href")
let sizes = try? linkElement.attr("sizes")
let type = try? linkElement.attr("type")
if href?.starts(with: "data:;base64") == false {
return createFavicon(url: href, siteUrl: siteUrl, iconType: faviconType, sizesString: sizes, type: type)
}
}
}
return nil
}
private func getFaviconTypeForLinkElements(_ linkElement: Element) -> FaviconType? {
if let relValue = try? linkElement.attr("rel") {
switch relValue {
case "icon":
return FaviconType.Icon
case "apple-touch-icon-precomposed":
return FaviconType.AppleTouchPrecomposed
case "apple-touch-icon":
return FaviconType.AppleTouch
case "shortcut icon":
return FaviconType.ShortcutIcon
default:
return nil
}
}
return nil
}
private func mapMetaElementToFavicon(_ metaElement: Element, _ siteUrl: String) -> Favicon? {
if let content = try? metaElement.attr("content") {
if isOpenGraphImageDeclaration(metaElement) {
return createFavicon(url: content, siteUrl: siteUrl, iconType: FaviconType.OpenGraphImage, sizesString: nil, type: nil)
}
else if isMsTileMetaElement(metaElement) {
return createFavicon(url: content, siteUrl: siteUrl, iconType: FaviconType.MsTileImage, sizesString: nil, type: nil)
}
}
return nil
}
private func isOpenGraphImageDeclaration(_ metaElement: Element) -> Bool {
return metaElement.hasAttr("property")
&& "og:image" == (try? metaElement.attr("property"))
&& metaElement.hasAttr("content")
}
private func isMsTileMetaElement(_ metaElement: Element) -> Bool {
return metaElement.hasAttr("name")
&& "msapplication-TileImage" == (try? metaElement.attr("name"))
&& metaElement.hasAttr("content")
}
private func createFavicon(url: String?, siteUrl: String, iconType: FaviconType, sizesString: String?, type: String?) -> Favicon? {
if let url = url {
let urlWithoutQuery = URL(string: url)?.absoluteStringWithouthQueryAndFragment ?? url
let absoluteUrl = makeLinkAbsolute(url: urlWithoutQuery, siteUrl: siteUrl)
let size = extractSizesFromString(sizesString)
return Favicon(url: absoluteUrl, iconType: iconType, size: size, type: type)
}
return nil
}
private func makeLinkAbsolute(url: String, siteUrl: String) -> String {
return urlUtil.makeLinkAbsolute(url: url, siteUrl: siteUrl)
}
private func extractSizesFromString(_ sizesString: String?) -> Size? {
if let sizesString = sizesString {
let sizes = extractSizesFromString(sizesString)
if sizes.isNotEmpty {
return sizes.max(by: { lhs, rhs in lhs >= rhs } )
}
}
return nil
}
private func extractSizesFromString(_ sizesString: String) -> [Size] {
let sizes = sizesString.split(separator: " ").map { mapSizeString(String($0)) } as? [Size] ?? []
return sizes
}
private func mapSizeString(_ sizeString: String) -> Size? {
var parts = sizeString.split(separator: "x")
if parts.count != 2 {
parts = sizeString.split(separator: "×") // actually doesn't meet specification, see https://www.w3schools.com/tags/att_link_sizes.asp, but New York Times uses it
}
if parts.count != 2 {
parts = sizeString.split(separator: "X")
}
if parts.count == 2 {
let width = Int(parts[0])
let height = Int(parts[1])
if let width = width, let height = height {
return Size(width: width, height: height)
}
}
return nil
}
}