@objc(WMFRouter) public class Router: NSObject { public enum Destination: Equatable { case inAppLink(_: URL) case externalLink(_: URL) case article(_: URL) case articleHistory(_: URL, articleTitle: String) case articleDiffCompare(_: URL, fromRevID: Int?, toRevID: Int?) case articleDiffSingle(_: URL, fromRevID: Int?, toRevID: Int?) case talk(_: URL) case userTalk(_: URL) case search(_: URL, term: String?) case audio(_: URL) case onThisDay(_: Int?) case readingListsImport(encodedPayload: String) case login } unowned let configuration: Configuration required init(configuration: Configuration) { self.configuration = configuration } // MARK: Public /// Gets the appropriate in-app destination for a given URL public func destination(for url: URL, loggedInUsername: String?) -> Destination { guard let siteURL = url.wmf_site, let project = WikimediaProject(siteURL: siteURL) else { guard url.isWikimediaHostedAudioFileLink else { return webViewDestinationForHostURL(url) } return .audio(url.byMakingAudioFileCompatibilityAdjustments) } return destinationForHostURL(url, project: project, loggedInUsername: loggedInUsername) } public func doesOpenInBrowser(for url: URL, loggedInUsername: String?) -> Bool { return [.externalLink(url), .inAppLink(url)].contains(destination(for: url, loggedInUsername: loggedInUsername)) } // MARK: Internal and Private private let mobilediffRegexCompare = try! NSRegularExpression(pattern: "^mobilediff/([0-9]+)\\.\\.\\.([0-9]+)", options: .caseInsensitive) private let mobilediffRegexSingle = try! NSRegularExpression(pattern: "^mobilediff/([0-9]+)", options: .caseInsensitive) private let historyRegex = try! NSRegularExpression(pattern: "^history/(.*)", options: .caseInsensitive) internal func destinationForWikiResourceURL(_ url: URL, project: WikimediaProject, loggedInUsername: String?) -> Destination? { guard let path = url.wikiResourcePath else { return nil } let language = project.languageCode ?? "en" let namespaceAndTitle = path.namespaceAndTitleOfWikiResourcePath(with: language) let namespace = namespaceAndTitle.0 let title = namespaceAndTitle.1 switch namespace { case .talk: if FeatureFlags.needsNewTalkPage && project.supportsNativeUserTalkPages { return .talk(url) } else { return nil } case .userTalk: return project.supportsNativeUserTalkPages ? .userTalk(url) : nil case .special: // TODO: Fix to work across languages, not just EN. Fetch special page aliases per site and add to a set of local json files. // https://en.wikipedia.org/w/api.php?action=query&format=json&meta=siteinfo&formatversion=2&siprop=specialpagealiases if language.uppercased() == "EN" || language.uppercased() == "TEST", title == "MyTalk", let username = loggedInUsername, let newURL = url.wmf_URL(withTitle: "User_talk:\(username)") { return .userTalk(newURL) } if language.uppercased() == "EN" || language.uppercased() == "TEST", title == "MyContributions", let username = loggedInUsername, let newURL = url.wmf_URL(withPath: "/wiki/Special:Contributions/\(username)", isMobile: true) { return .inAppLink(newURL) } if language.uppercased() == "EN" || language.uppercased() == "TEST", title == "UserLogin" { return .login } if title == "ReadingLists", let components = URLComponents(url: url, resolvingAgainstBaseURL: false), let firstQueryItem = components.queryItems?.first, firstQueryItem.name == "limport", let encodedPayload = firstQueryItem.value { return .readingListsImport(encodedPayload: encodedPayload) } guard project.supportsNativeDiffPages else { return nil } if let compareDiffMatch = mobilediffRegexCompare.firstMatch(in: title), let fromRevID = Int(mobilediffRegexCompare.replacementString(for: compareDiffMatch, in: title, offset: 0, template: "$1")), let toRevID = Int(mobilediffRegexCompare.replacementString(for: compareDiffMatch, in: title, offset: 0, template: "$2")) { return .articleDiffCompare(url, fromRevID: fromRevID, toRevID: toRevID) } if let singleDiffMatch = mobilediffRegexSingle.firstReplacementString(in: title), let toRevID = Int(singleDiffMatch) { return .articleDiffSingle(url, fromRevID: nil, toRevID: toRevID) } if let articleTitle = historyRegex.firstReplacementString(in: title)?.normalizedPageTitle { return .articleHistory(url, articleTitle: articleTitle) } return nil case .main: guard project.mainNamespaceGoesToNativeArticleView else { return nil } return WikipediaURLTranslations.isMainpageTitle(title, in: language) ? nil : Destination.article(url) case .wikipedia: guard project.considersWResourcePathsForRouting else { return nil } let onThisDayURLSnippet = "On_this_day" if title.uppercased().contains(onThisDayURLSnippet.uppercased()) { // URL in form of https://en.wikipedia.org/wiki/Wikipedia:On_this_day/Today?3. Take bit past question mark. if let selected = url.query { return .onThisDay(Int(selected)) } else { return .onThisDay(nil) } } else { fallthrough } default: return nil } } internal func destinationForWResourceURL(_ url: URL, project: WikimediaProject) -> Destination? { guard project.considersWResourcePathsForRouting, let path = url.wResourcePath else { return nil } guard var components = URLComponents(string: path) else { return nil } components.query = url.query guard components.path.lowercased() == "index.php" else { return nil } guard let queryItems = components.queryItems else { return nil } var params: [String: String] = [:] params.reserveCapacity(queryItems.count) for item in queryItems { params[item.name] = item.value } if let search = params["search"] { return .search(url, term: search) } let maybeTitle = params["title"] let maybeDiff = params["diff"] let maybeOldID = params["oldid"] let maybeType = params["type"] let maybeAction = params["action"] let maybeDir = params["dir"] let maybeLimit = params["limit"] guard let title = maybeTitle else { return nil } let language = project.languageCode ?? "en" if language.uppercased() == "EN" || language.uppercased() == "TEST", title == "Special:UserLogin" { return .login } if maybeLimit != nil, maybeDir != nil, let action = maybeAction, action == "history" { // TODO: push history 'slice' return .articleHistory(url, articleTitle: title) } else if let action = maybeAction, action == "history" { return .articleHistory(url, articleTitle: title) } else if let type = maybeType, type == "revision", let diffString = maybeDiff, let oldIDString = maybeOldID, let toRevID = Int(diffString), let fromRevID = Int(oldIDString) { return .articleDiffCompare(url, fromRevID: fromRevID, toRevID: toRevID) } else if let diff = maybeDiff, diff == "prev", let oldIDString = maybeOldID, let toRevID = Int(oldIDString) { return .articleDiffCompare(url, fromRevID: nil, toRevID: toRevID) } else if let diff = maybeDiff, diff == "next", let oldIDString = maybeOldID, let fromRevID = Int(oldIDString) { return .articleDiffCompare(url, fromRevID: fromRevID, toRevID: nil) } else if let oldIDString = maybeOldID, let toRevID = Int(oldIDString) { return .articleDiffSingle(url, fromRevID: nil, toRevID: toRevID) } return nil } internal func destinationForHostURL(_ url: URL, project: WikimediaProject, loggedInUsername: String?) -> Destination { let canonicalURL = url.canonical if let wikiResourcePathInfo = destinationForWikiResourceURL(canonicalURL, project: project, loggedInUsername: loggedInUsername) { return wikiResourcePathInfo } if let wResourcePathInfo = destinationForWResourceURL(canonicalURL, project: project) { return wResourcePathInfo } return webViewDestinationForHostURL(url) } internal func webViewDestinationForHostURL(_ url: URL) -> Destination { let canonicalURL = url.canonical if configuration.hostCanRouteToInAppWebView(url.host) { return .inAppLink(canonicalURL) } else { return .externalLink(url) } } }