This PR contains all the work related to setting up this project as required to implement the [Assignment](https://repo.rock-n-code.com/rock-n-code/deep-linking-assignment/wiki/Assignment) on top, as intended. To summarise this work: - [x] created a new **Xcode** project; - [x] cloned the `Wikipedia` app and inserted it into the **Xcode** project; - [x] created the `Locations` app and also, its `Libraries` package; - [x] created the `Shared` package to share dependencies between the apps; - [x] added a `Makefile` file and implemented some **environment** and **help** commands. Co-authored-by: Javier Cicchelli <javier@rock-n-code.com> Reviewed-on: rock-n-code/deep-linking-assignment#1
259 lines
10 KiB
Swift
259 lines
10 KiB
Swift
@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)
|
|
}
|
|
}
|
|
}
|