@@ -174,11 +174,11 @@ public extension Blog {
174174 self . account == nil
175175 }
176176
177- @objc var supportsCoreRestApi : Bool {
178- if case . selfHosted = try ? WordPressSite ( blog: self ) {
179- return true
177+ @objc var hasDirectCoreRESTAPIAccess : Bool {
178+ guard let site = try ? WordPressSite ( blog: self ) else {
179+ return false
180180 }
181- return false
181+ return site . applicationPasswordCredentials != nil
182182 }
183183}
184184
@@ -190,58 +190,150 @@ public extension WpApiApplicationPasswordDetails {
190190 }
191191}
192192
193- public enum WordPressSite : Hashable {
194- case dotCom( siteURL: URL , siteId: Int , authToken: String )
195- case selfHosted( blogId: TaggedManagedObjectID < Blog > , siteURL: URL , apiRootURL: ParsedUrl , username: String , authToken: String )
193+ /// Describes a WordPress site's hosting type, authentication credentials,
194+ /// and API capabilities.
195+ ///
196+ /// This is a value type constructed from a `Blog` Core Data object. It captures
197+ /// a snapshot of the site's characteristics at construction time.
198+ ///
199+ /// `WordPressSite` is not a one-to-one mapping with `Blog`. It represents the
200+ /// subset of `Blog` instances that have access to the WordPress core REST API
201+ /// (wp/v2). A self-hosted site that only has XML-RPC credentials is not
202+ /// representable as a `WordPressSite`.
203+ ///
204+ /// - All WordPress.com sites qualify (wp/v2 is accessed via WP.com REST API
205+ /// with OAuth).
206+ /// - Self-hosted sites must have application password credentials.
207+ public struct WordPressSite {
208+ public let blogId : TaggedManagedObjectID < Blog >
209+ public let siteURL : URL
210+ public let flavor : ApiFlavor
211+
212+ public init ( blogId: TaggedManagedObjectID < Blog > , siteURL: URL , flavor: ApiFlavor ) {
213+ self . blogId = blogId
214+ self . siteURL = siteURL
215+ self . flavor = flavor
216+ }
217+ }
218+
219+ extension WordPressSite {
220+ public enum ApiFlavor {
221+ /// A site hosted on WordPress.com. Always has OAuth access via
222+ /// WPAccount. May also have application password credentials
223+ /// (e.g., Atomic sites).
224+ case dotCom( DotComCredentials )
225+
226+ /// A self-hosted WordPress site with application password credentials.
227+ /// Application password is required for wp/v2 API access.
228+ case selfHosted( ApplicationPasswordCredentials )
229+ }
230+ }
231+
232+ extension WordPressSite {
233+ public struct DotComCredentials : Hashable {
234+ public let siteId : Int
235+ public let oAuthToken : String
236+ /// Non-nil for Atomic sites that also have application password access.
237+ public let applicationPassword : ApplicationPasswordCredentials ?
238+
239+ public init ( siteId: Int , oAuthToken: String , applicationPassword: ApplicationPasswordCredentials ? ) {
240+ self . siteId = siteId
241+ self . oAuthToken = oAuthToken
242+ self . applicationPassword = applicationPassword
243+ }
244+ }
245+
246+ public struct ApplicationPasswordCredentials : Hashable {
247+ public let apiRootURL : ParsedUrl
248+ public let username : String
249+ public let token : String
250+
251+ public init ( apiRootURL: ParsedUrl , username: String , token: String ) {
252+ self . apiRootURL = apiRootURL
253+ self . username = username
254+ self . token = token
255+ }
256+ }
257+ }
196258
259+ extension WordPressSite : Hashable {
260+ public static func == ( lhs: WordPressSite , rhs: WordPressSite ) -> Bool {
261+ lhs. blogId == rhs. blogId
262+ }
263+
264+ public func hash( into hasher: inout Hasher ) {
265+ hasher. combine ( blogId)
266+ }
267+ }
268+
269+ extension WordPressSite {
270+ /// Constructs a `WordPressSite` from a `Blog` Core Data object.
271+ ///
272+ /// Throws if the blog lacks enough data to determine its hosting type
273+ /// and at least one valid authentication method.
274+ ///
275+ /// For self-hosted sites, application password credentials are required.
276+ /// Sites without them cannot be represented as a `WordPressSite`.
197277 public init ( blog: Blog ) throws {
198278 let siteURL = try blog. getUrl ( )
199- // Directly access the site content when available.
279+ self . blogId = TaggedManagedObjectID ( blog)
280+ self . siteURL = siteURL
281+
282+ // Build application password credentials if available.
283+ // These are shared across both hosting types — WordPress.com Atomic
284+ // sites can have them too.
285+ let applicationPassword : ApplicationPasswordCredentials ?
200286 if let restApiRootURL = blog. restApiRootURL,
201- let restApiRootURL = try ? ParsedUrl . parse ( input: restApiRootURL) ,
287+ let parsedApiRoot = try ? ParsedUrl . parse ( input: restApiRootURL) ,
202288 let username = blog. username,
203- let authToken = try ? blog. getApplicationToken ( ) {
204- self = . selfHosted ( blogId : TaggedManagedObjectID ( blog ) , siteURL : siteURL , apiRootURL : restApiRootURL , username : username , authToken : authToken )
205- } else if let account = blog . account , let siteId = blog . dotComID ? . intValue {
206- // When the site is added via a WP.com account, access the site via WP.com
207- let authToken = try account . authToken ?? WPAccount . token ( forUsername : account . username )
208- self = . dotCom ( siteURL : siteURL , siteId : siteId , authToken : authToken )
289+ let token = try ? blog. getApplicationToken ( ) {
290+ applicationPassword = ApplicationPasswordCredentials (
291+ apiRootURL : parsedApiRoot ,
292+ username : username ,
293+ token: token
294+ )
209295 } else {
210- // In theory, this branch should never run, because the two if statements above should have covered all paths.
211- // But we'll keep it here as the fallback.
212- let url = try blog. getUrl ( )
213- let apiRootURL = try ParsedUrl . parse ( input: blog. restApiRootURL ?? blog. getUrl ( ) . appending ( path: " wp-json " ) . absoluteString)
214- self = . selfHosted( blogId: TaggedManagedObjectID ( blog) , siteURL: url, apiRootURL: apiRootURL, username: try blog. getUsername ( ) , authToken: try blog. getApplicationToken ( ) )
296+ applicationPassword = nil
215297 }
216- }
217298
218- public var siteURL : URL {
219- switch self {
220- case let . dotCom( siteURL, _, _) :
221- return siteURL
222- case let . selfHosted( _, siteURL, _, _, _) :
223- return siteURL
299+ // Check for WordPress.com account first. This means Atomic sites
300+ // (which have both an account and application password credentials)
301+ // resolve to `.dotCom`.
302+ if let account = blog. account,
303+ let siteId = blog. dotComID? . intValue {
304+ let authToken = try account. authToken
305+ ?? WPAccount . token ( forUsername: account. username)
306+ self . flavor = . dotCom( DotComCredentials (
307+ siteId: siteId,
308+ oAuthToken: authToken,
309+ applicationPassword: applicationPassword
310+ ) )
311+ } else {
312+ // Self-hosted sites must have application password credentials
313+ // for wp/v2 API access.
314+ guard let applicationPassword else {
315+ throw Blog . BlogCredentialsError. blogPasswordMissing
316+ }
317+ self . flavor = . selfHosted( applicationPassword)
224318 }
225319 }
320+ }
226321
227- public func blog( in context: NSManagedObjectContext ) throws -> Blog ? {
228- switch self {
229- case let . dotCom( _, siteId, _) :
230- return try Blog . lookup ( withID: siteId, in: context)
231- case let . selfHosted( blogId, _, _, _, _) :
232- return try context. existingObject ( with: blogId)
322+ extension WordPressSite {
323+ /// The application password credentials, if available.
324+ /// Always non-nil for self-hosted sites. Optional for WordPress.com sites
325+ /// (non-nil for Atomic sites).
326+ public var applicationPasswordCredentials : ApplicationPasswordCredentials ? {
327+ switch flavor {
328+ case let . dotCom( credentials) :
329+ return credentials. applicationPassword
330+ case let . selfHosted( credentials) :
331+ return credentials
233332 }
234333 }
235334
236- public func blogId( in coreDataStack: CoreDataStack ) -> TaggedManagedObjectID < Blog > ? {
237- switch self {
238- case let . dotCom( _, siteId, _) :
239- return coreDataStack. performQuery { context in
240- guard let blog = try ? Blog . lookup ( withID: siteId, in: context) else { return nil }
241- return TaggedManagedObjectID ( blog)
242- }
243- case let . selfHosted( id, _, _, _, _) :
244- return id
245- }
335+ /// Look up the `Blog` object in a given Core Data context.
336+ public func blog( in context: NSManagedObjectContext ) throws -> Blog {
337+ try context. existingObject ( with: blogId)
246338 }
247339}
0 commit comments