Skip to content

Commit b66999d

Browse files
authored
Implement a wordpress-rs backed MediaServiceRemote (#24540)
* Update wordpress-rs * Implement a wordpress-rs backed `MediaServiceRemote` * Move `WordPressSite` to WordPressData * Use core REST API when application password is available * Support changing media alt text * Add guard statements to get rid of force unwraps
1 parent 9525c85 commit b66999d

File tree

8 files changed

+264
-33
lines changed

8 files changed

+264
-33
lines changed

Modules/Package.resolved

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Modules/Package.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ let package = Package(
5454
),
5555
.package(url: "https://github.com/zendesk/support_sdk_ios", from: "8.0.3"),
5656
// We can't use wordpress-rs branches nor commits here. Only tags work.
57-
.package(url: "https://github.com/Automattic/wordpress-rs", revision: "alpha-20250505"),
57+
.package(url: "https://github.com/Automattic/wordpress-rs", revision: "alpha-20250513"),
5858
.package(url: "https://github.com/wordpress-mobile/GutenbergKit", revision: "fdfe788530bbff864ce7147b5a68608d7025e078"),
5959
.package(
6060
url: "https://github.com/Automattic/color-studio",

Sources/WordPressData/Objective-C/Blog.m

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -614,7 +614,7 @@ - (BOOL)supports:(BlogFeature)feature
614614
// alt is not supported via XML-RPC API
615615
// https://core.trac.wordpress.org/ticket/58582
616616
// https://github.com/wordpress-mobile/WordPress-Android/issues/18514#issuecomment-1589752274
617-
return [self supportsRestApi];
617+
return [self supportsRestApi] || [self supportsCoreRestApi];
618618
case BlogFeatureMediaDeletion:
619619
return [self isAdmin];
620620
case BlogFeatureHomepageSettings:

Sources/WordPressData/Swift/Blog+SelfHosted.swift

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,13 @@ public extension Blog {
161161
@objc var isSelfHosted: Bool {
162162
self.account == nil
163163
}
164+
165+
@objc var supportsCoreRestApi: Bool {
166+
if case .selfHosted = try? WordPressSite(blog: self) {
167+
return true
168+
}
169+
return false
170+
}
164171
}
165172

166173
public extension WpApiApplicationPasswordDetails {
@@ -170,3 +177,19 @@ public extension WpApiApplicationPasswordDetails {
170177
.joined()
171178
}
172179
}
180+
181+
public enum WordPressSite {
182+
case dotCom(siteId: Int, authToken: String)
183+
case selfHosted(blogId: TaggedManagedObjectID<Blog>, apiRootURL: ParsedUrl, username: String, authToken: String)
184+
185+
public init(blog: Blog) throws {
186+
if let account = blog.account {
187+
let authToken = try account.authToken ?? WPAccount.token(forUsername: account.username)
188+
self = .dotCom(siteId: blog.dotComID?.intValue ?? 0, authToken: authToken)
189+
} else {
190+
let url = try blog.restApiRootURL ?? blog.getUrl().appending(path: "wp-json").absoluteString
191+
let apiRootURL = try ParsedUrl.parse(input: url)
192+
self = .selfHosted(blogId: TaggedManagedObjectID(blog), apiRootURL: apiRootURL, username: try blog.getUsername(), authToken: try blog.getApplicationToken())
193+
}
194+
}
195+
}

WordPress/Classes/Networking/WordPressClient.swift

Lines changed: 2 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -5,22 +5,6 @@ import WordPressAPIInternal
55
import WordPressCore
66
import WordPressShared
77

8-
enum WordPressSite {
9-
case dotCom(authToken: String)
10-
case selfHosted(blogId: TaggedManagedObjectID<Blog>, apiRootURL: ParsedUrl, username: String, authToken: String)
11-
12-
init(blog: Blog) throws {
13-
if let account = blog.account {
14-
let authToken = try account.authToken ?? WPAccount.token(forUsername: account.username)
15-
self = .dotCom(authToken: authToken)
16-
} else {
17-
let url = try blog.restApiRootURL ?? blog.getUrl().appending(path: "wp-json").absoluteString
18-
let apiRootURL = try ParsedUrl.parse(input: url)
19-
self = .selfHosted(blogId: TaggedManagedObjectID(blog), apiRootURL: apiRootURL, username: try blog.getUsername(), authToken: try blog.getApplicationToken())
20-
}
21-
}
22-
}
23-
248
extension WordPressClient {
259
static var requestedWithInvalidAuthenticationNotification: Foundation.Notification.Name {
2610
.init("WordPressClient.requestedWithInvalidAuthenticationNotification")
@@ -38,8 +22,8 @@ extension WordPressClient {
3822
let session = URLSession(configuration: .ephemeral)
3923

4024
switch site {
41-
case let .dotCom(authToken):
42-
let apiRootURL = try! ParsedUrl.parse(input: "https://public-api.wordpress.com")
25+
case let .dotCom(siteId, authToken):
26+
let apiRootURL = try! ParsedUrl.parse(input: "https://public-api.wordpress.com/wpcom/v2/site/\(siteId)")
4327
let api = WordPressAPI(urlSession: session, apiRootUrl: apiRootURL, authentication: .bearer(token: authToken))
4428
self.init(api: api, rootUrl: apiRootURL)
4529
case let .selfHosted(blogId, apiRootURL, username, authToken):

WordPress/Classes/Services/MediaRepository.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,10 @@ private extension MediaRepository {
9494
return MediaServiceRemoteREST(wordPressComRestApi: api, siteID: dotComID)
9595
}
9696

97+
if let site = try? WordPressSite(blog: blog) {
98+
return MediaServiceRemoteCoreREST(client: .init(site: site))
99+
}
100+
97101
if let username = blog.username, let password = blog.password, let api = blog.xmlrpcApi {
98102
return MediaServiceRemoteXMLRPC(api: api, username: username, password: password)
99103
}

WordPress/Classes/Services/MediaService.m

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -476,16 +476,7 @@ - (NSString *)mimeTypeForMediaType:(NSNumber *)mediaType
476476

477477
- (id<MediaServiceRemote>)remoteForBlog:(Blog *)blog
478478
{
479-
id <MediaServiceRemote> remote;
480-
if ([blog supports:BlogFeatureWPComRESTAPI]) {
481-
if (blog.wordPressComRestApi) {
482-
remote = [[MediaServiceRemoteREST alloc] initWithWordPressComRestApi:blog.wordPressComRestApi
483-
siteID:blog.dotComID];
484-
}
485-
} else if (blog.xmlrpcApi) {
486-
remote = [[MediaServiceRemoteXMLRPC alloc] initWithApi:blog.xmlrpcApi username:blog.username password:blog.password];
487-
}
488-
return remote;
479+
return [[[MediaServiceRemoteFactory alloc] init] remoteForBlog:blog error:nil];
489480
}
490481

491482
- (void)mergeMedia:(NSArray *)media
Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
import Foundation
2+
import Combine
3+
import WordPressCore
4+
import WordPressShared
5+
import WordPressAPI
6+
import WordPressAPIInternal
7+
import WordPressKit
8+
9+
/// A `MediaServiceRemote` implementation that uses the WordPress core REST API (`/wp-json/wp/v2/media`).
10+
class MediaServiceRemoteCoreREST: NSObject, MediaServiceRemote {
11+
let client: WordPressClient
12+
13+
init(client: WordPressClient) {
14+
self.client = client
15+
}
16+
17+
func getMediaWithID(_ mediaID: NSNumber, success: ((RemoteMedia?) -> Void)?, failure: (((any Error)?) -> Void)?) {
18+
Task { @MainActor in
19+
do {
20+
let media = try await client.api.media.retrieveWithEditContext(mediaId: mediaID.int64Value).data
21+
success?(RemoteMedia(media: media))
22+
} catch {
23+
failure?(error)
24+
}
25+
}
26+
}
27+
28+
func uploadMedia(
29+
_ media: RemoteMedia,
30+
progress progressPtr: AutoreleasingUnsafeMutablePointer<Progress?>?,
31+
success: ((RemoteMedia?) -> Void)?,
32+
failure: (((any Error)?) -> Void)?
33+
) {
34+
guard let localURL = media.localURL else {
35+
wpAssertionFailure("local url missing in the media")
36+
failure?(URLError(.fileDoesNotExist))
37+
return
38+
}
39+
40+
// Set up a `Progress` instance that are updated from the main thread, which is a behaviour that other parts of the app rely on.
41+
let totalUnit: Int64 = 100
42+
let mainThreadProgress = Progress(totalUnitCount: totalUnit)
43+
progressPtr?.pointee = mainThreadProgress
44+
45+
Task { @MainActor in
46+
do {
47+
let progress = Progress.discreteProgress(totalUnitCount: totalUnit)
48+
let cancellable = progress
49+
.publisher(for: \.fractionCompleted, options: .new)
50+
.map { Int64($0 * Double(totalUnit)) }
51+
.receive(on: DispatchQueue.main)
52+
.assign(to: \.completedUnitCount, on: mainThreadProgress)
53+
defer { cancellable.cancel() }
54+
55+
let media = try await client.api.uploadMedia(params: .init(media: media), fromLocalFileURL: localURL, fulfilling: progress).data
56+
success?(.init(media: media))
57+
} catch {
58+
failure?(error)
59+
}
60+
}
61+
}
62+
63+
func update(_ media: RemoteMedia, success: ((RemoteMedia?) -> Void)?, failure: (((any Error)?) -> Void)?) {
64+
guard let mediaID = media.mediaID else {
65+
wpAssertionFailure("id missing in the media")
66+
failure?(URLError(.unknown))
67+
return
68+
}
69+
70+
Task { @MainActor in
71+
do {
72+
let media = try await client.api.media.update(mediaId: mediaID.int64Value, params: .init(media: media)).data
73+
success?(.init(media: media))
74+
} catch {
75+
failure?(error)
76+
}
77+
}
78+
}
79+
80+
func delete(_ media: RemoteMedia, success: (() -> Void)?, failure: (((any Error)?) -> Void)?) {
81+
guard let mediaID = media.mediaID else {
82+
wpAssertionFailure("id missing in the media")
83+
failure?(URLError(.unknown))
84+
return
85+
}
86+
87+
Task { @MainActor in
88+
do {
89+
let _ = try await client.api.media.delete(mediaId: mediaID.int64Value)
90+
success?()
91+
} catch {
92+
failure?(error)
93+
}
94+
}
95+
}
96+
97+
func getMediaLibrary(pageLoad: (([Any]?) -> Void)!, success: (([Any]?) -> Void)?, failure: (((any Error)?) -> Void)?) {
98+
Task { @MainActor in
99+
do {
100+
var all = [RemoteMedia]()
101+
let sequence = await client.api.media.sequenceWithEditContext(params: .init())
102+
for try await element in sequence {
103+
let page = element.map { RemoteMedia(media: $0) }
104+
all.append(contentsOf: page)
105+
pageLoad(page)
106+
}
107+
success?(all)
108+
} catch {
109+
failure?(error)
110+
}
111+
}
112+
}
113+
114+
func getMediaLibraryCount(forType mediaType: String?, withSuccess success: ((Int) -> Void)?, failure: (((any Error)?) -> Void)?) {
115+
Task { @MainActor in
116+
do {
117+
let response = try await client.api.media.listWithEditContext(params: .init(mimeType: mediaType))
118+
success?(Int(response.headerMap.wpTotal() ?? 0))
119+
} catch {
120+
failure?(error)
121+
}
122+
}
123+
}
124+
125+
func getMetadataFromVideoPressID(
126+
_ videoPressID: String!,
127+
isSitePrivate: Bool,
128+
success: ((WordPressKit.RemoteVideoPressVideo?) -> Void)?,
129+
failure: (((any Error)?) -> Void)?
130+
) {
131+
// ⚠️ The endpoint is not available in WordPress core.
132+
failure?(URLError(.unsupportedURL))
133+
}
134+
135+
func getVideoPressToken(_ videoPressID: String!, success: ((String?) -> Void)?, failure: (((any Error)?) -> Void)?) {
136+
// ⚠️ The endpoint is not available in WordPress core.
137+
failure?(URLError(.unsupportedURL))
138+
}
139+
}
140+
141+
private extension RemoteMedia {
142+
convenience init(media: MediaWithEditContext) {
143+
self.init()
144+
145+
self.mediaID = NSNumber(value: media.id)
146+
self.url = URL(string: media.sourceUrl)
147+
self.guid = URL(string: media.guid.raw)
148+
self.date = media.dateGmt
149+
self.postID = media.postId.map { NSNumber(value: $0) }
150+
self.mimeType = media.mimeType
151+
self.extension = URL(string: media.sourceUrl)?.pathExtension
152+
self.title = media.title.raw
153+
self.caption = media.caption.raw
154+
self.descriptionText = media.description.raw
155+
self.alt = media.altText
156+
157+
if case let .object(mediaDetails) = media.mediaDetails {
158+
if case let .int(width) = mediaDetails["width"] {
159+
self.width = NSNumber(value: width)
160+
}
161+
if case let .int(height) = mediaDetails["height"] {
162+
self.height = NSNumber(value: height)
163+
}
164+
if case let .string(file) = mediaDetails["file"] {
165+
self.file = file
166+
}
167+
168+
if case let .object(sizes) = mediaDetails["sizes"] {
169+
if case let .object(medium) = sizes["medium"],
170+
case let .string(url) = medium["source_url"] {
171+
self.mediumURL = URL(string: url)
172+
}
173+
if case let .object(large) = sizes["large"],
174+
case let .string(url) = large["source_url"] {
175+
self.largeURL = URL(string: url)
176+
}
177+
if case let .object(thumbnail) = sizes["thumbnail"],
178+
case let .string(url) = thumbnail["source_url"] {
179+
self.remoteThumbnailURL = url
180+
}
181+
}
182+
}
183+
184+
self.localURL = nil
185+
self.videopressGUID = nil
186+
self.length = nil
187+
self.shortcode = nil
188+
}
189+
}
190+
191+
private extension MediaCreateParams {
192+
init(media: RemoteMedia) {
193+
self.init(
194+
date: nil,
195+
dateGmt: media.date,
196+
slug: nil,
197+
status: nil,
198+
title: media.title,
199+
author: nil,
200+
commentStatus: nil,
201+
pingStatus: nil,
202+
template: nil,
203+
altText: media.alt,
204+
caption: media.caption,
205+
description: media.descriptionText,
206+
postId: media.postID?.int64Value
207+
)
208+
}
209+
}
210+
211+
private extension MediaUpdateParams {
212+
init(media: RemoteMedia) {
213+
self.init(
214+
date: nil,
215+
dateGmt: media.date,
216+
slug: nil,
217+
status: nil,
218+
title: media.title,
219+
author: nil,
220+
commentStatus: nil,
221+
pingStatus: nil,
222+
template: nil,
223+
altText: media.alt,
224+
caption: media.caption,
225+
description: media.descriptionText,
226+
postId: media.postID?.int64Value
227+
)
228+
}
229+
}

0 commit comments

Comments
 (0)