Javier Cicchelli 9bcdaa697b [Setup] Basic project structure (#1)
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
2023-04-08 18:37:13 +00:00

414 lines
20 KiB
Swift

import Foundation
/// Configuration handles the current environment - production, beta, staging, labs
/// It has the functions that build URLs for the various APIs utilized by the app.
/// It also maintains the list of relevant domains - default domain, domains that require the CentralAuth cookies to be copied, etc.
@objc(WMFConfiguration)
public class Configuration: NSObject {
public struct StagingOptions: OptionSet {
public let rawValue: Int
public static let appsLabsforPCS = StagingOptions(rawValue: 1 << 0)
public static let deploymentLabsForEventLogging = StagingOptions(rawValue: 1 << 1)
public static let betaCluster = StagingOptions(rawValue: 1 << 2) // note, this will force beta cluster for PCS (thus ignoring an appsLabsforPCS value if also set) and force deploymentLabsForEventLogging
public init(rawValue: Int) {
self.rawValue = rawValue
}
}
public struct LocalOptions: OptionSet {
public let rawValue: Int
public static let localAnnouncements = LocalOptions(rawValue: 1 << 0)
public static let localPCS = LocalOptions(rawValue: 1 << 1)
public init(rawValue: Int) {
self.rawValue = rawValue
}
}
public enum Environment {
case production
case staging(StagingOptions)
case local(LocalOptions)
}
public let environment: Environment
@objc public static let current: Configuration = {
#if WMF_LOCAL
return Configuration.local(options: [.localPCS, .localAnnouncements])
#elseif WMF_STAGING
/* NOTE: .betaCluster attempts to point to the MediaWiki beta cluster for all possible endpoints.
Change this to .appsLabsForPCS and/or .deploymentLabsForEventLogging for alternative staging environments.
Example: Configuration.staging(options: [.appsLabsForPCS, .deploymentLabsForEventLogging])
.appsLabsForPCS = Product Infrastructure team's labs instance for PCS endpoints
.deploymentLabsForEventLogging = labs instance for testing event logging endpoints
All other endpoints would point to production */
return Configuration.staging(options: [.deploymentLabsForEventLogging])
#else
return .production
#endif
}()
private let pageContentServiceAPIType: APIURLComponentsBuilder.RESTBase.BuilderType
private let feedContentAPIType: APIURLComponentsBuilder.RESTBase.BuilderType
private let announcementsAPIType: APIURLComponentsBuilder.RESTBase.BuilderType
private let eventLoggingAPIType: APIURLComponentsBuilder.EventLogging.BuilderType
private let mediaWikiRestAPIType = APIURLComponentsBuilder.MediaWiki.BuilderType.productionRest
private let mediaWikiAPIType = APIURLComponentsBuilder.MediaWiki.BuilderType.production
private let wikidataAPIType: APIURLComponentsBuilder.Wikidata.BuilderType
private let commonsAPIType: APIURLComponentsBuilder.Commons.BuilderType
private let metricsAPIType = APIURLComponentsBuilder.RESTBase.BuilderType.production
// MARK: Configurations
private static var commonProductionCentralAuthCookieTargetDomains = [
Domain.mediaWiki.withDotPrefix,
Domain.wikimedia.withDotPrefix,
Domain.wiktionary.withDotPrefix,
Domain.wikiquote.withDotPrefix,
Domain.wikibooks.withDotPrefix,
Domain.wikisource.withDotPrefix,
Domain.wikinews.withDotPrefix,
Domain.wikiversity.withDotPrefix,
Domain.wikispecies.withDotPrefix,
Domain.wikivoyage.withDotPrefix
]
public static let production: Configuration = {
let centralAuthCookieTargetDomains = commonProductionCentralAuthCookieTargetDomains + [Domain.wikidata.withDotPrefix, Domain.commons.withDotPrefix]
return Configuration(
environment: .production,
defaultSiteDomain: Domain.wikipedia,
wikipediaCookieDomain: Domain.wikipedia.withDotPrefix,
centralAuthCookieTargetDomains: centralAuthCookieTargetDomains,
pageContentServiceAPIType: .production,
feedContentAPIType: .production,
announcementsAPIType: .production,
wikidataAPIType: .production,
commonsAPIType: .production,
eventLoggingAPIType: .production)
}()
private static func staging(options: StagingOptions) -> Configuration {
let defaultSiteDomain = options.contains(.betaCluster) ? Domain.wikipediaBetaLabs : Domain.wikipedia
let wikipediaCookieDomain = options.contains(.betaCluster) ? Domain.wikipediaBetaLabs.withDotPrefix : Domain.wikipedia.withDotPrefix
let wikidataCookieDomain = options.contains(.betaCluster) ? Domain.wikidataBetaLabs.withDotPrefix : Domain.wikidata.withDotPrefix
let commonsCookieDomain = options.contains(.betaCluster) ? Domain.commonsBetaLabs.withDotPrefix : Domain.commons.withDotPrefix
let centralAuthCookieTargetDomains = commonProductionCentralAuthCookieTargetDomains + [wikidataCookieDomain, commonsCookieDomain]
let pcsApiType: APIURLComponentsBuilder.RESTBase.BuilderType = options.contains(.appsLabsforPCS) && !options.contains(.betaCluster) ? .stagingAppsLabsPCS : .production
let wikidataApiType: APIURLComponentsBuilder.Wikidata.BuilderType = options.contains(.betaCluster) ? .betaLabs : .production
let commonsApiType: APIURLComponentsBuilder.Commons.BuilderType = options.contains(.betaCluster) ? .betaLabs : .production
let eventLoggingApiType: APIURLComponentsBuilder.EventLogging
.BuilderType = options.contains(.deploymentLabsForEventLogging) || options.contains(.betaCluster) ? .staging : .production
return Configuration(
environment: .staging(options),
defaultSiteDomain: defaultSiteDomain,
wikipediaCookieDomain: wikipediaCookieDomain,
centralAuthCookieTargetDomains: centralAuthCookieTargetDomains,
pageContentServiceAPIType: pcsApiType,
feedContentAPIType: .production,
announcementsAPIType: .production,
wikidataAPIType: wikidataApiType,
commonsAPIType: commonsApiType,
eventLoggingAPIType: eventLoggingApiType
)
}
private static func local(options: LocalOptions) -> Configuration {
let pcsApiType: APIURLComponentsBuilder.RESTBase.BuilderType = options.contains(.localPCS) ? .localPCS : .production
let announcementsApiType: APIURLComponentsBuilder.RESTBase.BuilderType = options.contains(.localAnnouncements) ? .localAnnouncements : .production
let centralAuthCookieTargetDomains = commonProductionCentralAuthCookieTargetDomains + [Domain.wikidata.withDotPrefix, Domain.commons.withDotPrefix]
return Configuration(
environment: .local(options),
defaultSiteDomain: Domain.wikipedia,
wikipediaCookieDomain: Domain.wikipedia.withDotPrefix,
centralAuthCookieTargetDomains: centralAuthCookieTargetDomains,
pageContentServiceAPIType: pcsApiType,
feedContentAPIType: .production,
announcementsAPIType: announcementsApiType,
wikidataAPIType: .production,
commonsAPIType: .production,
eventLoggingAPIType: .production)
}
// MARK: Constants
struct Scheme {
static let http = "http"
static let https = "https"
}
public struct Domain {
public static let wikipedia = "wikipedia.org"
public static let wikipediaBetaLabs = "wikipedia.beta.wmflabs.org"
public static let wikidata = "wikidata.org"
public static let wikidataBetaLabs = "wikidata.beta.wmflabs.org"
public static let commons = "commons.wikimedia.org"
public static let commonsBetaLabs = "commons.wikimedia.beta.wmflabs.org"
public static let mediaWiki = "www.mediawiki.org"
public static let wikispecies = "species.wikimedia.org"
public static let appsLabs = "mobileapps.wmflabs.org" // Product Infrastructure team's labs instance
public static let localhost = "localhost"
public static let englishWikipedia = "en.wikipedia.org"
public static let testWikipedia = "test.wikipedia.org"
public static let wikimedia = "wikimedia.org"
public static let metaWiki = "meta.wikimedia.org"
public static let wikimediafoundation = "wikimediafoundation.org"
public static let uploads = "upload.wikimedia.org"
public static let wikibooks = "wikibooks.org"
public static let wiktionary = "wiktionary.org"
public static let wikiquote = "wikiquote.org"
public static let wikisource = "wikisource.org"
public static let wikinews = "wikinews.org"
public static let wikiversity = "wikiversity.org"
public static let wikivoyage = "wikivoyage.org"
}
struct Path {
static let wikiResourceComponent = ["wiki"]
static let restBaseAPIComponents = ["api", "rest_v1"]
static let mediaWikiAPIComponents = ["w", "api.php"]
static let mediaWikiRestAPIComponents = ["w", "rest.php"]
static let expandedWikiResourceComponents = ["w", "index.php"]
}
// MARK: State
@objc public let defaultSiteDomain: String
public let defaultSiteURL: URL
public let wikipediaCookieDomain: String
public let centralAuthCookieSourceDomain: String // copy cookies from
public let centralAuthCookieTargetDomains: [String] // copy cookies to
// Wikipedia Domains
public let wikipediaDomains: [String]
// Domains that can fall back to in-app web view
public let inAppWebViewRoutingDomains: [String]
@objc public lazy var router: Router = {
return Router(configuration: self)
}()
required init(environment: Environment, defaultSiteDomain: String,
wikipediaCookieDomain: String,
centralAuthCookieTargetDomains: [String] = [],
pageContentServiceAPIType: APIURLComponentsBuilder.RESTBase.BuilderType,
feedContentAPIType: APIURLComponentsBuilder.RESTBase.BuilderType,
announcementsAPIType: APIURLComponentsBuilder.RESTBase.BuilderType,
wikidataAPIType: APIURLComponentsBuilder.Wikidata.BuilderType,
commonsAPIType: APIURLComponentsBuilder.Commons.BuilderType,
eventLoggingAPIType: APIURLComponentsBuilder.EventLogging.BuilderType) {
self.environment = environment
self.defaultSiteDomain = defaultSiteDomain
var components = URLComponents()
components.scheme = "https"
components.host = defaultSiteDomain
self.defaultSiteURL = components.url!
self.wikipediaCookieDomain = wikipediaCookieDomain
self.centralAuthCookieSourceDomain = self.wikipediaCookieDomain
self.centralAuthCookieTargetDomains = centralAuthCookieTargetDomains
self.wikipediaDomains = [Domain.wikipedia, Domain.wikipediaBetaLabs, Domain.appsLabs]
self.inAppWebViewRoutingDomains = wikipediaDomains + [Domain.mediaWiki, Domain.wikidata, Domain.wikimedia, Domain.wikimediafoundation]
self.pageContentServiceAPIType = pageContentServiceAPIType
self.feedContentAPIType = feedContentAPIType
self.announcementsAPIType = announcementsAPIType
self.wikidataAPIType = wikidataAPIType
self.commonsAPIType = commonsAPIType
self.eventLoggingAPIType = eventLoggingAPIType
}
// MARK: Page Content Service
public func pageContentServiceBuilder(withWikiHost wikiHost: String? = nil) -> APIURLComponentsBuilder {
let builder = pageContentServiceAPIType.builder(withWikiHost: wikiHost)
return builder
}
/// The Page Content Service includes mobile-html and the associated endpoints. It can be run locally with this repository: https://gerrit.wikimedia.org/r/admin/projects/mediawiki/services/mobileapps
/// On production, it is run through RESTBase at https://en.wikipedia.org/api/rest_v1/ (works for all language wikis)
@objc(pageContentServiceAPIURLForURL:appendingPathComponents:)
public func pageContentServiceAPIURLForURL(_ url: URL? = nil, appending pathComponents: [String] = [""]) -> URL? {
let builder = pageContentServiceAPIType.builder(withWikiHost: url?.host)
let components = builder.components(byAppending: pathComponents)
return components.wmf_URLWithLanguageVariantCode(url?.wmf_languageVariantCode)
}
/// Returns the default request headers for Page Content Service API requests
public func pageContentServiceHeaders(for url: URL) -> [String: String] {
// If the language supports variants, only send a single code with variant for that language.
// This is a workaround for an issue with server-side Accept-Language header handling and
// can be removed when https://phabricator.wikimedia.org/T256491 is fixed.
// NOTE: In general it does not seem that most sites process multi-language Accept-Language headers.
// For variants, sending a single Accept-Language header is sufficient and seems the least error-prone.
if let languageVariantCode = url.wmf_languageVariantCode {
return ["Accept-Language": languageVariantCode]
} else {
return [:]
}
}
// MARK: Metrics
/// The metrics API lives only on wikimedia.org: https://wikimedia.org/api/rest_v1/
@objc(metricsAPIURLComponentsAppendingPathComponents:)
public func metricsAPIURLComponents(appending pathComponents: [String] = [""]) -> URLComponents {
let builder = metricsAPIType.builder(withWikiHost: Domain.wikimedia)
return builder.components(byAppending: ["metrics"] + pathComponents)
}
// MARK: Wikifeeds (Feed Content and Announcements)
/// Feed content is located in the wikifeeds repository. It can be run locally with: https://gerrit.wikimedia.org/r/admin/projects/mediawiki/services/wikifeeds
/// On production, it is run through RESTBase at https://en.wikipedia.org/api/rest_v1/ (works for all language wikis)
@objc(feedContentAPIURLForURL:appendingPathComponents:)
public func feedContentAPIURLForURL(_ url: URL?, appending pathComponents: [String] = [""]) -> URL? {
let builder = feedContentAPIType.builder(withWikiHost: url?.host)
let components = builder.components(byAppending: pathComponents)
return components.wmf_URLWithLanguageVariantCode(url?.wmf_languageVariantCode)
}
/// Announcements are located in the wikifeeds repository. It can be run locally with: https://gerrit.wikimedia.org/r/admin/projects/mediawiki/services/wikifeeds
/// On production, it is run through RESTBase at https://en.wikipedia.org/api/rest_v1/ (works for all language wikis)
@objc(announcementsAPIURLForURL:appendingPathComponents:)
public func announcementsAPIURLForURL(_ url: URL?, appending pathComponents: [String] = [""]) -> URL? {
let builder = announcementsAPIType.builder(withWikiHost: url?.host)
let components = builder.components(byAppending: pathComponents)
return components.wmf_URLWithLanguageVariantCode(url?.wmf_languageVariantCode)
}
// MARK: Event Logging
@objc(eventLoggingAPIURLWithPayload:)
public func eventLoggingAPIURL(with payload: NSObject) -> URL? {
let builder = eventLoggingAPIType.builder()
let components = try? builder.components(byAssigningPayloadToPercentEncodedQuery: payload)
return components?.url
}
// MARK: MediaWiki Rest
public func mediaWikiRestAPIURLForURL(_ url: URL? = nil, appending pathComponents: [String] = [""], queryParameters: [String: Any]? = nil) -> URL? {
let builder = mediaWikiRestAPIType.builder(withWikiHost: url?.host)
let components = builder.components(byAppending: pathComponents, queryParameters: queryParameters)
return components.wmf_URLWithLanguageVariantCode(url?.wmf_languageVariantCode)
}
// MARK: MediaWiki
@objc(mediaWikiAPIURLForURL:withQueryParameters:)
public func mediaWikiAPIURLForURL(_ url: URL?, with queryParameters: [String: Any]? = nil) -> URL? {
let components = mediaWikiAPIURLForHost(url?.host, with: queryParameters)
return components.wmf_URLWithLanguageVariantCode(url?.wmf_languageVariantCode)
}
public func mediaWikiAPIURLForHost(_ host: String? = nil, with queryParameters: [String: Any]? = nil) -> URLComponents {
let builder = mediaWikiAPIType.builder(withWikiHost: host)
guard let queryParameters = queryParameters else {
return builder.components()
}
return builder.components(queryParameters: queryParameters)
}
public func mediaWikiAPIURLForLanguageCode(_ languageCode: String, siteDomain: String? = nil, queryParameters: [String: Any]?) -> URLComponents {
let domain = siteDomain ?? defaultSiteDomain
let host = "\(languageCode).\(domain)"
return mediaWikiAPIURLForHost(host, with: queryParameters)
}
// MARK: Wikidata
public func wikidataAPIURLComponents(with queryParameters: [String: Any]?) -> URLComponents {
let builder = wikidataAPIType.builder()
return builder.components(queryParameters: queryParameters)
}
// MARK: Commons
@objc(commonsAPIURLComponentsWithQueryParameters:)
public func commonsAPIURLComponents(with queryParameters: [String: Any]?) -> URLComponents {
let builder = commonsAPIType.builder()
return builder.components(queryParameters: queryParameters)
}
// MARK: Article URLs
func articleURLComponentsBuilder(for host: String) -> APIURLComponentsBuilder {
var components = URLComponents()
components.host = host
components.scheme = Scheme.https
return APIURLComponentsBuilder(hostComponents: components, basePathComponents: Path.wikiResourceComponent)
}
func expandedArticleURLComponentsBuilder(for host: String) -> APIURLComponentsBuilder {
var components = URLComponents()
components.host = host
components.scheme = Scheme.https
return APIURLComponentsBuilder(hostComponents: components, basePathComponents: Path.expandedWikiResourceComponents)
}
public func articleURLForHost(_ host: String, languageVariantCode: String?, appending pathComponents: [String]) -> URL? {
let builder = articleURLComponentsBuilder(for: host)
let components = builder.components(byAppending: pathComponents)
return components.wmf_URLWithLanguageVariantCode(languageVariantCode)
}
// Uses format https://en.wikipedia.org/w/index.php?title=Main_Page
// As opposed to https://en.wikipedia.org/wiki/Main_Page
public func expandedArticleURLForHost(_ host: String, languageVariantCode: String?, queryParameters: [String: Any]?) -> URL? {
let builder = expandedArticleURLComponentsBuilder(for: host)
let components = builder.components(byAppending: [], queryParameters: queryParameters)
return components.wmf_URLWithLanguageVariantCode(languageVariantCode)
}
// MARK: Routing Helpers
public func isWikipediaHost(_ host: String?) -> Bool {
guard let host = host else {
return false
}
for domain in wikipediaDomains {
if host.isDomainOrSubDomainOf(domain) {
return true
}
}
return false
}
/// Indicates if a url should fall back to an in-app web view or not
/// Please inspect url namespace first and confirm url cannot display natively before using this method.
/// - Parameter host: url host that you are trying to route
/// - Returns: true = host should fall back to app web view, route to in-app web view. false = host should fall back to external Safari web browser (business logic for parental controls).
public func hostCanRouteToInAppWebView(_ host: String?) -> Bool {
guard let host = host else {
return false
}
for domain in inAppWebViewRoutingDomains {
if host.isDomainOrSubDomainOf(domain) {
return true
}
}
return false
}
}