Implemented SwiftBankIconFinder

This commit is contained in:
dankito 2020-07-30 14:48:18 +02:00
parent e8a27b1a83
commit b898f9d17e
12 changed files with 772 additions and 14 deletions

View File

@ -6,16 +6,15 @@ import net.dankito.banking.persistence.IBankingPersistence
import net.dankito.banking.search.IRemitteeSearcher import net.dankito.banking.search.IRemitteeSearcher
import net.dankito.banking.ui.IRouter import net.dankito.banking.ui.IRouter
import net.dankito.banking.ui.presenter.BankingPresenter import net.dankito.banking.ui.presenter.BankingPresenter
import net.dankito.banking.util.IAsyncRunner import net.dankito.banking.util.*
import net.dankito.banking.util.NoOpBankIconFinder
import net.dankito.banking.util.NoOpSerializer
import net.dankito.banking.util.extraction.NoOpInvoiceDataExtractor import net.dankito.banking.util.extraction.NoOpInvoiceDataExtractor
import net.dankito.banking.util.extraction.NoOpTextExtractorRegistry import net.dankito.banking.util.extraction.NoOpTextExtractorRegistry
import net.dankito.utils.multiplatform.File import net.dankito.utils.multiplatform.File
class BankingPresenterSwift(dataFolder: File, router: IRouter, webClient: IWebClient, persistence: IBankingPersistence, remitteeSearcher: IRemitteeSearcher, asyncRunner: IAsyncRunner) class BankingPresenterSwift(dataFolder: File, router: IRouter, webClient: IWebClient, persistence: IBankingPersistence,
: BankingPresenter(fints4kBankingClientCreator(NoOpSerializer(), webClient), InMemoryBankFinder(), dataFolder, persistence, router, remitteeSearcher: IRemitteeSearcher, bankIconFinder: IBankIconFinder, serializer: ISerializer, asyncRunner: IAsyncRunner)
remitteeSearcher, NoOpBankIconFinder(), NoOpTextExtractorRegistry(), NoOpInvoiceDataExtractor(), NoOpSerializer(), asyncRunner) { : BankingPresenter(fints4kBankingClientCreator(serializer, webClient), InMemoryBankFinder(), dataFolder, persistence, router,
remitteeSearcher, bankIconFinder, NoOpTextExtractorRegistry(), NoOpInvoiceDataExtractor(), serializer, asyncRunner) {
} }

View File

@ -3,7 +3,7 @@
archiveVersion = 1; archiveVersion = 1;
classes = { classes = {
}; };
objectVersion = 50; objectVersion = 52;
objects = { objects = {
/* Begin PBXBuildFile section */ /* Begin PBXBuildFile section */
@ -51,6 +51,14 @@
36BE069124CEF52800CBBB68 /* UpdateButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36BE069024CEF52800CBBB68 /* UpdateButton.swift */; }; 36BE069124CEF52800CBBB68 /* UpdateButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36BE069024CEF52800CBBB68 /* UpdateButton.swift */; };
36BE06B324CF133400CBBB68 /* JsonEncoderSerializer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36BE06B224CF133400CBBB68 /* JsonEncoderSerializer.swift */; }; 36BE06B324CF133400CBBB68 /* JsonEncoderSerializer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36BE06B224CF133400CBBB68 /* JsonEncoderSerializer.swift */; };
36BE06B524CF85A300CBBB68 /* AmountLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36BE06B424CF85A300CBBB68 /* AmountLabel.swift */; }; 36BE06B524CF85A300CBBB68 /* AmountLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36BE06B424CF85A300CBBB68 /* AmountLabel.swift */; };
36BE06B824D077EC00CBBB68 /* SwiftBankIconFinder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36BE06B724D077EC00CBBB68 /* SwiftBankIconFinder.swift */; };
36BE06BA24D0783900CBBB68 /* FaviconFinder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36BE06B924D0783800CBBB68 /* FaviconFinder.swift */; };
36BE06C024D07CCD00CBBB68 /* SwiftSoup in Frameworks */ = {isa = PBXBuildFile; productRef = 36BE06BF24D07CCD00CBBB68 /* SwiftSoup */; };
36BE06C224D07FB100CBBB68 /* Favicon.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36BE06C124D07FB100CBBB68 /* Favicon.swift */; };
36BE06C424D0801A00CBBB68 /* Size.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36BE06C324D0801A00CBBB68 /* Size.swift */; };
36BE06C624D080C900CBBB68 /* FaviconType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36BE06C524D080C900CBBB68 /* FaviconType.swift */; };
36BE06C824D0DE7400CBBB68 /* UIKitTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36BE06C724D0DE7400CBBB68 /* UIKitTextField.swift */; };
36C4009824D23580005227AD /* SwiftBankIconFinderTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36C4009724D23580005227AD /* SwiftBankIconFinderTest.swift */; };
36E7BA1424B3D05C00757859 /* ViewExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36E7BA1324B3D05C00757859 /* ViewExtensions.swift */; }; 36E7BA1424B3D05C00757859 /* ViewExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36E7BA1324B3D05C00757859 /* ViewExtensions.swift */; };
36FC929C24B39A05002B12E9 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36FC929B24B39A05002B12E9 /* AppDelegate.swift */; }; 36FC929C24B39A05002B12E9 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36FC929B24B39A05002B12E9 /* AppDelegate.swift */; };
36FC929E24B39A05002B12E9 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36FC929D24B39A05002B12E9 /* SceneDelegate.swift */; }; 36FC929E24B39A05002B12E9 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36FC929D24B39A05002B12E9 /* SceneDelegate.swift */; };
@ -145,6 +153,13 @@
36BE069024CEF52800CBBB68 /* UpdateButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateButton.swift; sourceTree = "<group>"; }; 36BE069024CEF52800CBBB68 /* UpdateButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateButton.swift; sourceTree = "<group>"; };
36BE06B224CF133400CBBB68 /* JsonEncoderSerializer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JsonEncoderSerializer.swift; sourceTree = "<group>"; }; 36BE06B224CF133400CBBB68 /* JsonEncoderSerializer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JsonEncoderSerializer.swift; sourceTree = "<group>"; };
36BE06B424CF85A300CBBB68 /* AmountLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AmountLabel.swift; sourceTree = "<group>"; }; 36BE06B424CF85A300CBBB68 /* AmountLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AmountLabel.swift; sourceTree = "<group>"; };
36BE06B724D077EC00CBBB68 /* SwiftBankIconFinder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftBankIconFinder.swift; sourceTree = "<group>"; };
36BE06B924D0783800CBBB68 /* FaviconFinder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FaviconFinder.swift; sourceTree = "<group>"; };
36BE06C124D07FB100CBBB68 /* Favicon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Favicon.swift; sourceTree = "<group>"; };
36BE06C324D0801A00CBBB68 /* Size.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Size.swift; sourceTree = "<group>"; };
36BE06C524D080C900CBBB68 /* FaviconType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FaviconType.swift; sourceTree = "<group>"; };
36BE06C724D0DE7400CBBB68 /* UIKitTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIKitTextField.swift; sourceTree = "<group>"; };
36C4009724D23580005227AD /* SwiftBankIconFinderTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = SwiftBankIconFinderTest.swift; path = BankingiOSAppTests/BankIconFinder/SwiftBankIconFinderTest.swift; sourceTree = SOURCE_ROOT; };
36E7BA1324B3D05C00757859 /* ViewExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewExtensions.swift; sourceTree = "<group>"; }; 36E7BA1324B3D05C00757859 /* ViewExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewExtensions.swift; sourceTree = "<group>"; };
36E7BA1824B9E70C00757859 /* xcode-frameworks */ = {isa = PBXFileReference; lastKnownFileType = folder; name = "xcode-frameworks"; path = "../../tools/BankFinder/build/xcode-frameworks"; sourceTree = "<group>"; }; 36E7BA1824B9E70C00757859 /* xcode-frameworks */ = {isa = PBXFileReference; lastKnownFileType = folder; name = "xcode-frameworks"; path = "../../tools/BankFinder/build/xcode-frameworks"; sourceTree = "<group>"; };
36FC929824B39A05002B12E9 /* BankingiOSApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = BankingiOSApp.app; sourceTree = BUILT_PRODUCTS_DIR; }; 36FC929824B39A05002B12E9 /* BankingiOSApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = BankingiOSApp.app; sourceTree = BUILT_PRODUCTS_DIR; };
@ -181,6 +196,7 @@
36FC92D024B39C47002B12E9 /* fints4k.framework in Frameworks */, 36FC92D024B39C47002B12E9 /* fints4k.framework in Frameworks */,
36BCF87024BB0F8A005BEC29 /* fints4kBankingClient.framework in Frameworks */, 36BCF87024BB0F8A005BEC29 /* fints4kBankingClient.framework in Frameworks */,
36BCF87324BB2706005BEC29 /* BankingUiSwift.framework in Frameworks */, 36BCF87324BB2706005BEC29 /* BankingUiSwift.framework in Frameworks */,
36BE06C024D07CCD00CBBB68 /* SwiftSoup in Frameworks */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
@ -213,6 +229,18 @@
path = persistence; path = persistence;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
36BE06B624D077B400CBBB68 /* BankIconFinder */ = {
isa = PBXGroup;
children = (
36BE06C124D07FB100CBBB68 /* Favicon.swift */,
36BE06C524D080C900CBBB68 /* FaviconType.swift */,
36BE06C324D0801A00CBBB68 /* Size.swift */,
36BE06B924D0783800CBBB68 /* FaviconFinder.swift */,
36BE06B724D077EC00CBBB68 /* SwiftBankIconFinder.swift */,
);
path = BankIconFinder;
sourceTree = "<group>";
};
36FC928F24B39A05002B12E9 = { 36FC928F24B39A05002B12E9 = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@ -237,8 +265,9 @@
36FC929A24B39A05002B12E9 /* BankingiOSApp */ = { 36FC929A24B39A05002B12E9 /* BankingiOSApp */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
36BCF87924BFA679005BEC29 /* persistence */,
36FC92D424B3A389002B12E9 /* fints4k */, 36FC92D424B3A389002B12E9 /* fints4k */,
36BCF87924BFA679005BEC29 /* persistence */,
36BE06B624D077B400CBBB68 /* BankIconFinder */,
36FC92D924B3A479002B12E9 /* ui */, 36FC92D924B3A479002B12E9 /* ui */,
36FC929B24B39A05002B12E9 /* AppDelegate.swift */, 36FC929B24B39A05002B12E9 /* AppDelegate.swift */,
36FC929D24B39A05002B12E9 /* SceneDelegate.swift */, 36FC929D24B39A05002B12E9 /* SceneDelegate.swift */,
@ -321,6 +350,7 @@
36BE068C24CE41E700CBBB68 /* Styles.swift */, 36BE068C24CE41E700CBBB68 /* Styles.swift */,
36BE069024CEF52800CBBB68 /* UpdateButton.swift */, 36BE069024CEF52800CBBB68 /* UpdateButton.swift */,
36BE06B424CF85A300CBBB68 /* AmountLabel.swift */, 36BE06B424CF85A300CBBB68 /* AmountLabel.swift */,
36BE06C724D0DE7400CBBB68 /* UIKitTextField.swift */,
); );
path = ui; path = ui;
sourceTree = "<group>"; sourceTree = "<group>";
@ -334,6 +364,7 @@
36BCF88224C098BB005BEC29 /* BankListItem.swift */, 36BCF88224C098BB005BEC29 /* BankListItem.swift */,
36BCF88424C098C8005BEC29 /* BankAccountListItem.swift */, 36BCF88424C098C8005BEC29 /* BankAccountListItem.swift */,
36BCF88A24C0BD2D005BEC29 /* AccountTransactionsDialog.swift */, 36BCF88A24C0BD2D005BEC29 /* AccountTransactionsDialog.swift */,
36C4009724D23580005227AD /* SwiftBankIconFinderTest.swift */,
36BE066424CDE62800CBBB68 /* AccountTransactionListItem.swift */, 36BE066424CDE62800CBBB68 /* AccountTransactionListItem.swift */,
36BCF88C24C1C1EA005BEC29 /* TransferMoneyDialog.swift */, 36BCF88C24C1C1EA005BEC29 /* TransferMoneyDialog.swift */,
366FA4DB24C479120094F009 /* BankInfoListItem.swift */, 366FA4DB24C479120094F009 /* BankInfoListItem.swift */,
@ -366,6 +397,9 @@
dependencies = ( dependencies = (
); );
name = BankingiOSApp; name = BankingiOSApp;
packageProductDependencies = (
36BE06BF24D07CCD00CBBB68 /* SwiftSoup */,
);
productName = BankingiOSApp; productName = BankingiOSApp;
productReference = 36FC929824B39A05002B12E9 /* BankingiOSApp.app */; productReference = 36FC929824B39A05002B12E9 /* BankingiOSApp.app */;
productType = "com.apple.product-type.application"; productType = "com.apple.product-type.application";
@ -439,6 +473,9 @@
de, de,
); );
mainGroup = 36FC928F24B39A05002B12E9; mainGroup = 36FC928F24B39A05002B12E9;
packageReferences = (
36BE06BE24D07CCC00CBBB68 /* XCRemoteSwiftPackageReference "SwiftSoup" */,
);
productRefGroup = 36FC929924B39A05002B12E9 /* Products */; productRefGroup = 36FC929924B39A05002B12E9 /* Products */;
projectDirPath = ""; projectDirPath = "";
projectRoot = ""; projectRoot = "";
@ -505,16 +542,21 @@
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
36BE065924CA3CAB00CBBB68 /* UIKitSearchBar.swift in Sources */, 36BE065924CA3CAB00CBBB68 /* UIKitSearchBar.swift in Sources */,
36BE06C824D0DE7400CBBB68 /* UIKitTextField.swift in Sources */,
36BE064F24C9A17F00CBBB68 /* ImageTanView.swift in Sources */, 36BE064F24C9A17F00CBBB68 /* ImageTanView.swift in Sources */,
36BE068D24CE41E700CBBB68 /* Styles.swift in Sources */, 36BE068D24CE41E700CBBB68 /* Styles.swift in Sources */,
366FA4E224C4ED6C0094F009 /* EnterTanDialog.swift in Sources */, 366FA4E224C4ED6C0094F009 /* EnterTanDialog.swift in Sources */,
36FC92DC24B3A4A0002B12E9 /* AccountsTab.swift in Sources */, 36FC92DC24B3A4A0002B12E9 /* AccountsTab.swift in Sources */,
36BCF86E24BA691B005BEC29 /* DependencyInjector.swift in Sources */, 36BCF86E24BA691B005BEC29 /* DependencyInjector.swift in Sources */,
36BE06C224D07FB100CBBB68 /* Favicon.swift in Sources */,
36BE06B824D077EC00CBBB68 /* SwiftBankIconFinder.swift in Sources */,
36BCF89124C25971005BEC29 /* CoreDataBankingPersistence.swift in Sources */, 36BCF89124C25971005BEC29 /* CoreDataBankingPersistence.swift in Sources */,
36FC92A124B39A05002B12E9 /* BankingiOSApp.xcdatamodeld in Sources */, 36FC92A124B39A05002B12E9 /* BankingiOSApp.xcdatamodeld in Sources */,
36BE06C624D080C900CBBB68 /* FaviconType.swift in Sources */,
36BCF89324C25BC3005BEC29 /* Mapper.swift in Sources */, 36BCF89324C25BC3005BEC29 /* Mapper.swift in Sources */,
36FC92D724B3A3BA002B12E9 /* NSUrlWebClient.swift in Sources */, 36FC92D724B3A3BA002B12E9 /* NSUrlWebClient.swift in Sources */,
36BE06B324CF133400CBBB68 /* JsonEncoderSerializer.swift in Sources */, 36BE06B324CF133400CBBB68 /* JsonEncoderSerializer.swift in Sources */,
36BE06BA24D0783900CBBB68 /* FaviconFinder.swift in Sources */,
36BCF89524C31F02005BEC29 /* AppData.swift in Sources */, 36BCF89524C31F02005BEC29 /* AppData.swift in Sources */,
36BE065B24CA4B3500CBBB68 /* SelectBankDialog.swift in Sources */, 36BE065B24CA4B3500CBBB68 /* SelectBankDialog.swift in Sources */,
36BE068924CE288800CBBB68 /* CollapsibleText.swift in Sources */, 36BE068924CE288800CBBB68 /* CollapsibleText.swift in Sources */,
@ -532,6 +574,7 @@
36FC929C24B39A05002B12E9 /* AppDelegate.swift in Sources */, 36FC929C24B39A05002B12E9 /* AppDelegate.swift in Sources */,
36BCF88B24C0BD2D005BEC29 /* AccountTransactionsDialog.swift in Sources */, 36BCF88B24C0BD2D005BEC29 /* AccountTransactionsDialog.swift in Sources */,
36BCF87624BF114F005BEC29 /* UrlSessionWebClient.swift in Sources */, 36BCF87624BF114F005BEC29 /* UrlSessionWebClient.swift in Sources */,
36BE06C424D0801A00CBBB68 /* Size.swift in Sources */,
36FC92A324B39A05002B12E9 /* ContentView.swift in Sources */, 36FC92A324B39A05002B12E9 /* ContentView.swift in Sources */,
366FA4E624C6EBF40094F009 /* EnterTanState.swift in Sources */, 366FA4E624C6EBF40094F009 /* EnterTanState.swift in Sources */,
36BE065724C9E04800CBBB68 /* UIKitImageView.swift in Sources */, 36BE065724C9E04800CBBB68 /* UIKitImageView.swift in Sources */,
@ -552,6 +595,7 @@
isa = PBXSourcesBuildPhase; isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
36C4009824D23580005227AD /* SwiftBankIconFinderTest.swift in Sources */,
36FC92B624B39A08002B12E9 /* BankingiOSAppTests.swift in Sources */, 36FC92B624B39A08002B12E9 /* BankingiOSAppTests.swift in Sources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
@ -895,6 +939,25 @@
}; };
/* End XCConfigurationList section */ /* End XCConfigurationList section */
/* Begin XCRemoteSwiftPackageReference section */
36BE06BE24D07CCC00CBBB68 /* XCRemoteSwiftPackageReference "SwiftSoup" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/scinfu/SwiftSoup.git";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 2.3.2;
};
};
/* End XCRemoteSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */
36BE06BF24D07CCD00CBBB68 /* SwiftSoup */ = {
isa = XCSwiftPackageProductDependency;
package = 36BE06BE24D07CCC00CBBB68 /* XCRemoteSwiftPackageReference "SwiftSoup" */;
productName = SwiftSoup;
};
/* End XCSwiftPackageProductDependency section */
/* Begin XCVersionGroup section */ /* Begin XCVersionGroup section */
36FC929F24B39A05002B12E9 /* BankingiOSApp.xcdatamodeld */ = { 36FC929F24B39A05002B12E9 /* BankingiOSApp.xcdatamodeld */ = {
isa = XCVersionGroup; isa = XCVersionGroup;

View File

@ -0,0 +1,23 @@
import SwiftUI
class Favicon {
let url: String
let iconType: FaviconType
let size: Size?
let type: String?
init(url: String, iconType: FaviconType, size: Size? = nil, type: String? = nil) {
self.url = url
self.iconType = iconType
self.size = size
self.type = type
}
}

View File

@ -0,0 +1,186 @@
import Foundation
import SwiftSoup
import fints4k // TODO: get rid of this
class FaviconFinder {
private let webClient = UrlSessionWebClient() // TODO: create interface and pass from SwiftBankIconFinder
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) } 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 Favicon(url: makeLinkAbsolute(url: content, siteUrl: siteUrl), iconType: FaviconType.OpenGraphImage)
}
else if isMsTileMetaElement(metaElement) {
return Favicon(url: makeLinkAbsolute(url: content, siteUrl: siteUrl), iconType: FaviconType.MsTileImage)
}
}
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 absoluteUrl = makeLinkAbsolute(url: url, 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 url // TODO
}
private func extractSizesFromString(_ sizesString: String?) -> Size? {
if let sizesString = sizesString {
let sizes = extractSizesFromString(sizesString)
if sizes.isEmpty == false {
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
}
}

View File

@ -0,0 +1,18 @@
import SwiftUI
enum FaviconType {
case ShortcutIcon
case Icon
case OpenGraphImage
case AppleTouch
case AppleTouchPrecomposed
case MsTileImage
}

View File

@ -0,0 +1,42 @@
import SwiftUI
class Size : Comparable, CustomStringConvertible {
let width: Int
let height: Int
init(width: Int, height: Int) {
self.width = width
self.height = height
}
var isSquare: Bool {
return width == height
}
var displayText: String {
return "\(width) x \(height)"
}
static func < (lhs: Size, rhs: Size) -> Bool {
return lhs.width < rhs.width
&& lhs.height < rhs.height
}
static func == (lhs: Size, rhs: Size) -> Bool {
return lhs.width == rhs.width
&& lhs.height == rhs.height
}
var description: String {
return displayText
}
}

View File

@ -0,0 +1,279 @@
import Foundation
import SwiftSoup
import BankingUiSwift
import MultiplatformUtils
class SwiftBankIconFinder : IBankIconFinder {
static let SearchBankWebsiteBaseUrlQwant = "https://lite.qwant.com/?l=de&t=mobile&q="
static let SearchBankWebsiteBaseUrlEcosia = "https://www.ecosia.org/search?q="
static let SearchBankWebsiteBaseUrlDuckDuckGo = "https://duckduckgo.com/html/?q="
//static let ReplaceGfRegex = Pattern.compile(" \\(Gf [\\w]+\\)").toRegex()
private let log = LoggerFactory.Companion().getLogger(name: "BankIconFinder")
private let webClient = UrlSessionWebClient()
private let faviconFinder = FaviconFinder()
// private let faviconComparator = FaviconComparator(webClient)
func findIconForBankAsync(bankName: String, prefSize: Int32, result: @escaping (String?) -> Void) {
DispatchQueue.global(qos: .background).async {
let bestUrl = self.findIconForBank(bankName: bankName, prefSize: prefSize)
DispatchQueue.main.async { // dispatch reuslt back to main thread as mutable BankingPresenter / result callback can only be used on the thread they have been created on
result(bestUrl)
}
}
}
func findIconForBank(bankName: String, prefSize: Int32) -> String? {
if let bankUrl = findBankWebsite(bankName: bankName) {
if let bankHomepageResponse = webClient.get(bankUrl).body {
if let document = try? SwiftSoup.parse(bankHomepageResponse) {
let favicons = faviconFinder.extractFavicons(document: document, url: bankUrl)
// TODO:
// faviconComparator.getBestIcon(favicons, prefSize, prefSize + 32, true)?.let { prefFavicon ->
// return prefFavicon.url
// }
//
// return faviconComparator.getBestIcon(favicons, 16)?.url
return favicons.first?.url
}
}
}
return nil
}
func findBankWebsite(bankName: String) -> String? {
let adjustedBankName = bankName.replacingOccurrences(of: "-alt-", with: "")/*.replace(ReplaceGfRegex, "") */ // TODO
if let url = findBankWebsiteWithQwant(adjustedBankName) {
return url
}
print("Could not find bank website with Qwant for '\(bankName)'")
if let url = findBankWebsiteWithEcosia(adjustedBankName) {
return url
}
print("Could not find bank website with Ecosia for '\(bankName)'")
if let url = findBankWebsiteWithDuckDuckGo(adjustedBankName) {
return url
}
print("Could not find bank website with DuckDuckGo for '\(bankName)'")
return nil
}
private func findBankWebsiteWithQwant(_ bankName: String) -> String? {
return findBankWebsite(bankName, Self.SearchBankWebsiteBaseUrlQwant) { searchResponseDoc in
findLinksInQwantWebpage(searchResponseDoc)
?? []
}
}
private func findLinksInQwantWebpage(_ searchResponseDoc: Document) -> [String]? {
return (try? searchResponseDoc.select(".url"))?
.filter { (try? $0.select("span").first()) == nil }
.map { (try? $0.text()) ?? "" }
.filter { $0.isNotBlank }
}
private func findBankWebsiteWithEcosia(_ bankName: String) -> String? {
return findBankWebsite(bankName, Self.SearchBankWebsiteBaseUrlEcosia) { searchResponseDoc in
(try? searchResponseDoc.select(".js-result-url"))?
.map { (try? $0.attr("href")) ?? "" }
.filter { $0.isNotBlank }
?? []
}
}
private func findBankWebsiteWithDuckDuckGo(_ bankName: String) -> String? {
return findBankWebsite(bankName, Self.SearchBankWebsiteBaseUrlDuckDuckGo) { searchResponseDoc in
(try? searchResponseDoc.select(".result__url"))?
.map { (try? $0.attr("href")) ?? "" }
.filter { $0.isNotBlank }
?? []
}
}
private func findBankWebsite(_ bankName: String, _ searchBaseUrl: String, extractUrls: (Document) -> [String]) -> String? {
let encodedBankName = bankName.replacingOccurrences(of: " ", with: "+")
let exactSearchUrl = searchBaseUrl + "\"" + encodedBankName + "\""
if let searchResponseDocument = getSearchResultForBank(exactSearchUrl) {
if let bestUrl = findBestUrlForBank(bankName: bankName, unmappedUrls: extractUrls(searchResponseDocument)) {
return bestUrl
}
}
let searchUrl = searchBaseUrl + encodedBankName
if let searchResponseDocument = getSearchResultForBank(searchUrl) {
return findBestUrlForBank(bankName: bankName, unmappedUrls: extractUrls(searchResponseDocument))
}
return nil
}
private func getSearchResultForBank(_ searchUrl: String) -> Document? {
let response = webClient.get(searchUrl)
if let responseBody = response.body {
return try? SwiftSoup.parse(responseBody)
}
return nil
}
private func findBestUrlForBank(bankName: String, unmappedUrls: [String]) -> String? {
let urlCandidates = getUrlCandidates(unmappedUrls)
let urlCandidatesWithoutUnlikely = urlCandidates.filter { isUnlikelyBankUrl(bankName: bankName, urlCandidate: $0) == false }
let urlForBank = findUrlThatContainsBankName(bankName: bankName, urlCandidates: urlCandidatesWithoutUnlikely)
// cut off stuff like 'filalsuche' etc., they most like don't contain as many favicons as main page
return getMainPageForBankUrl(urlForBank: urlForBank, urlCandidates: urlCandidatesWithoutUnlikely) ?? urlForBank
}
private func getUrlCandidates(_ urls: [String?]) -> [String] {
return urls.map { fixUrl($0) }.filter { $0 != nil} as? [String] ?? []
}
private func fixUrl(_ url: String?) -> String? {
if let url = url, url.isNotBlank {
let urlEncoded = url.replacingOccurrences(of: " ", with: "%20F")
if urlEncoded.starts(with: "http") {
return urlEncoded
}
else {
return "https://" + urlEncoded
}
}
return nil
}
private func findUrlThatContainsBankName(bankName: String, urlCandidates: [String]) -> String? {
let bankNameParts = bankName.replacingOccurrences(of: ",", with: "")
.replacingOccurrences(of: "-", with: " ") // to find 'Sparda-Bank' in 'sparda.de'
.replacingOccurrences(of: "ä", with: "ae").replacingOccurrences(of: "Ä", with: "ae")
.replacingOccurrences(of: "ö", with: "oe").replacingOccurrences(of: "Ö", with: "oe")
.replacingOccurrences(of: "ü", with: "ue").replacingOccurrences(of: "Ü", with: "ue")
.split(separator: " ").map { String($0) }
.filter { $0.isBlank == false }
var urlsContainsPartsOfBankName = [Int : [String]]()
urlCandidates.forEach { urlCandidate in
if let containingCountParts = findBankNameInUrlHost(urlCandidate: urlCandidate, bankNameParts: bankNameParts) {
if (urlsContainsPartsOfBankName.keys.contains(containingCountParts) == false) {
urlsContainsPartsOfBankName[containingCountParts] = [urlCandidate]
}
else {
urlsContainsPartsOfBankName[containingCountParts]?.append(urlCandidate)
}
}
}
if let countMostMatches = urlsContainsPartsOfBankName.keys.max() {
return urlsContainsPartsOfBankName[countMostMatches]?.first
}
return nil
}
private func findBankNameInUrlHost(urlCandidate: String, bankNameParts: [String]) -> Int? {
if let candidateHost = URL(string: urlCandidate.replacingOccurrences(of: "onlinebanking-", with: ""))?.host {
return bankNameParts.filter { part in candidateHost.localizedCaseInsensitiveContains(part) }.count
}
return nil
}
private func getMainPageForBankUrl(urlForBank: String?, urlCandidates: [String]) -> String? {
if let urlForBank = urlForBank {
if isHomePage(urlForBank) {
return urlForBank
}
if let bankUri = URL(string: urlForBank) {
if let bankUriHost = bankUri.host {
let candidateUrl = urlCandidates.first { candidateUrl in
return URL(string: candidateUrl)?.host == bankUriHost && isHomePage(candidateUrl)
}
if let candidateUrl = candidateUrl {
return candidateUrl
}
}
}
}
if let urlForBank = urlForBank {
if let bankUri = URL(string: urlForBank) {
return "\(bankUri.scheme ?? "")://\(bankUri.host ?? "")"
}
}
return nil
}
private func isHomePage(_ url: String) -> Bool {
let uri = URL(string: url)
if let uri = uri, uri.path.isBlank && uri.host?.starts(with: "www.") == true {
return true
}
return false
}
private func isUnlikelyBankUrl(bankName: String, urlCandidate: String) -> Bool {
return urlCandidate.contains("meinprospekt.de/")
|| urlCandidate.contains("onlinestreet.de/")
|| urlCandidate.contains("iban-blz.de/")
|| urlCandidate.contains("bankleitzahlen.ws/")
|| urlCandidate.contains("bankleitzahl-finden.de/")
|| urlCandidate.contains("bankleitzahl-bic.de/")
|| urlCandidate.contains("bankleitzahlensuche.org/")
|| urlCandidate.contains("bankleitzahlensuche.com/")
|| urlCandidate.contains("bankverzeichnis.com")
|| urlCandidate.contains("banksuche.com/")
|| urlCandidate.contains("bank-code.net/")
|| urlCandidate.contains("thebankcodes.com/")
|| urlCandidate.contains("zinsen-berechnen.de/")
|| urlCandidate.contains("kredit-anzeiger.com/")
|| urlCandidate.contains("kreditbanken.de/")
|| urlCandidate.contains("nifox.de/")
|| urlCandidate.contains("wikipedia.org/")
|| urlCandidate.contains("transferwise.com/")
|| urlCandidate.contains("wogibtes.info/")
|| urlCandidate.contains("11880.com/")
|| urlCandidate.contains("kaufda.de/")
|| urlCandidate.contains("boomle.com/")
|| urlCandidate.contains("berlin.de/")
|| urlCandidate.contains("berliner-zeitung.de")
}
}

View File

@ -42,7 +42,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
let dataFolder = URL(fileURLWithPath: "data", isDirectory: true, relativeTo: URL(fileURLWithPath: appDataFolder)) let dataFolder = URL(fileURLWithPath: "data", isDirectory: true, relativeTo: URL(fileURLWithPath: appDataFolder))
let presenter = BankingPresenterSwift(dataFolder: dataFolder, router: SwiftUiRouter(), webClient: UrlSessionWebClient(), persistence: persistence, remitteeSearcher: persistence, serializer: JsonEncoderSerializer(), asyncRunner: DispatchQueueAsyncRunner()) let presenter = BankingPresenterSwift(dataFolder: dataFolder, router: SwiftUiRouter(), webClient: UrlSessionWebClient(), persistence: persistence, remitteeSearcher: persistence, bankIconFinder: SwiftBankIconFinder(), serializer: NoOpSerializer(), asyncRunner: DispatchQueueAsyncRunner())
DependencyInjector.register(dependency: persistence) DependencyInjector.register(dependency: persistence)
DependencyInjector.register(dependency: presenter) DependencyInjector.register(dependency: presenter)

View File

@ -6,12 +6,51 @@ import MultiplatformUtils
class UrlSessionWebClient : Fints4kIWebClient { class UrlSessionWebClient : Fints4kIWebClient {
func post(url: String, body: String, contentType: String, userAgent: String, callback: @escaping (Fints4kWebClientResponse) -> Void) { func post(url: String, body: String, contentType: String, userAgent: String, callback: @escaping (Fints4kWebClientResponse) -> Void) {
guard let requestUrl = URL(string: url) else { fatalError() } let request = requestFor(url, "POST", body)
executeRequestAsync(request, callback)
}
func getAsync(_ url: String, callback: @escaping (Fints4kWebClientResponse) -> Void) {
let request = requestFor(url, "GET")
executeRequestAsync(request, callback)
}
func get(_ url: String) -> Fints4kWebClientResponse {
let request = requestFor(url, "GET")
return executeRequestSynchronous(request)
}
func getData(_ url: String) -> Data? {
let request = requestFor(url, "GET")
return executeDataRequestSynchronous(request)
}
func head(_ url: String) -> Fints4kWebClientResponse {
let request = requestFor(url, "HEAD")
return executeRequestSynchronous(request)
}
func requestFor(_ url: String, _ method: String, _ body: String? = nil) -> URLRequest {
guard let requestUrl = URL.encoded(url) else { fatalError() }
var request = URLRequest(url: requestUrl) var request = URLRequest(url: requestUrl)
request.httpMethod = "POST" request.httpMethod = method
request.httpBody = body.data(using: String.Encoding.utf8)
if let body = body {
request.httpBody = body.data(using: String.Encoding.utf8)
}
return request
}
func executeRequestAsync(_ request: URLRequest, _ callback: @escaping (Fints4kWebClientResponse) -> Void) {
let dataTask = URLSession.shared.dataTask(with: request) { (data: Data?, response: URLResponse?, error: Error?) in let dataTask = URLSession.shared.dataTask(with: request) { (data: Data?, response: URLResponse?, error: Error?) in
// we have to dispatch response back to main thread as in Kotlin/Native objects can only be changed on one thread -> do all logic on main thread only network access in backbackground thread // we have to dispatch response back to main thread as in Kotlin/Native objects can only be changed on one thread -> do all logic on main thread only network access in backbackground thread
DispatchQueue.main.async { DispatchQueue.main.async {
@ -28,4 +67,57 @@ class UrlSessionWebClient : Fints4kIWebClient {
dataTask.resume() dataTask.resume()
} }
} func executeRequestSynchronous(_ request: URLRequest) -> Fints4kWebClientResponse {
var data: Data?, urlResponse: URLResponse?, error: Error?
let semaphore = DispatchSemaphore(value: 0)
let dataTask = URLSession.shared.dataTask(with: request) {
data = $0
urlResponse = $1
error = $2
semaphore.signal()
}
dataTask.resume()
_ = semaphore.wait(timeout: .distantFuture)
return mapResponse(data, urlResponse, error)
}
private func mapResponse(_ data: Data?, _ response: URLResponse?, _ error: Error?) -> Fints4kWebClientResponse {
if let httpResponse = response as? HTTPURLResponse {
if let data = data {
return Fints4kWebClientResponse(successful: true, responseCode: Int32(httpResponse.statusCode), error: nil, body: String(data: data, encoding: .ascii))
}
else {
return Fints4kWebClientResponse(successful: true, responseCode: Int32(httpResponse.statusCode), error: nil, body: nil)
}
}
else {
return Fints4kWebClientResponse(successful: false, responseCode: -1, error: KotlinException(message: error?.localizedDescription), body: nil)
}
}
func executeDataRequestSynchronous(_ request: URLRequest) -> Data? {
var data: Data?
let semaphore = DispatchSemaphore(value: 0)
let dataTask = URLSession.shared.dataTask(with: request) { receivedData, _, _ in
data = receivedData
semaphore.signal()
}
dataTask.resume()
_ = semaphore.wait(timeout: .distantFuture)
return data
}
}

View File

@ -1,5 +1,6 @@
import Foundation import Foundation
import CoreData import CoreData
import UIKit
import BankingUiSwift import BankingUiSwift
@ -70,7 +71,15 @@ class CoreDataBankingPersistence: IBankingPersistence, IRemitteeSearcher {
} }
func saveUrlToFile(url: String, file: URL) { func saveUrlToFile(url: String, file: URL) {
// TODO let response = UrlSessionWebClient().getData(url)
if let response = response {
do {
try UIImage(data: response)?.pngData()?.write(to: file)
} catch {
NSLog("Could not save url '\(url)' to file '\(file): \(error)")
}
}
} }

View File

@ -88,3 +88,21 @@ extension Array where Element == NSDecimalNumber {
} }
} }
extension NSObject {
var className: String {
return String(describing: type(of: self))
}
}
extension URL {
static func encoded(_ url: String) -> URL? {
return URL(string: url.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? url)
}
}

View File

@ -0,0 +1,29 @@
import XCTest
@testable import BankingiOSApp
class SwiftBankIconFinderTest: XCTestCase {
private let underTest = SwiftBankIconFinder()
override func setUpWithError() throws {
// Put setup code here. This method is called before the invocation of each test method in the class.
}
override func tearDownWithError() throws {
// Put teardown code here. This method is called after the invocation of each test method in the class.
}
func testBerlinerSparkasse() throws {
// when
let result = underTest.findBankWebsite(bankName: "Sparkasse Berlin")
// then
XCTAssertNotNil(result)
XCTAssertEqual(result!, "https://www.berliner-sparkasse.de")
}
}