Skip to content

Commit 05eb9ec

Browse files
authored
Merge pull request #5490 from woocommerce/feat/5363-jcp-image-upload-networking
Jetpack CP: add media endpoints for JCP sites
2 parents 3321fb4 + 115f43a commit 05eb9ec

File tree

7 files changed

+729
-0
lines changed

7 files changed

+729
-0
lines changed

Networking/Networking.xcodeproj/project.pbxproj

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,10 +50,14 @@
5050
02A26F1B2744F5FC008E4EDB /* wc-site-settings-partial.json in Resources */ = {isa = PBXBuildFile; fileRef = 02A26F192744F5FC008E4EDB /* wc-site-settings-partial.json */; };
5151
02A26F1C2744F5FC008E4EDB /* wp-site-settings.json in Resources */ = {isa = PBXBuildFile; fileRef = 02A26F1A2744F5FC008E4EDB /* wp-site-settings.json */; };
5252
02AAD53F250092A400BA1E26 /* product-add-or-delete.json in Resources */ = {isa = PBXBuildFile; fileRef = 02AAD53E250092A300BA1E26 /* product-add-or-delete.json */; };
53+
02AF07EA27492DBC00B2D81E /* WordPressMedia.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02AF07E927492DBC00B2D81E /* WordPressMedia.swift */; };
54+
02AF07EC27492FDD00B2D81E /* media-library-from-wordpress-site.json in Resources */ = {isa = PBXBuildFile; fileRef = 02AF07EB27492FDD00B2D81E /* media-library-from-wordpress-site.json */; };
55+
02AF07EE27493AE700B2D81E /* media-upload-to-wordpress-site.json in Resources */ = {isa = PBXBuildFile; fileRef = 02AF07ED27493AE700B2D81E /* media-upload-to-wordpress-site.json */; };
5356
02BA23C922EEF62C009539E7 /* order-stats-v4-wcadmin-deactivated.json in Resources */ = {isa = PBXBuildFile; fileRef = 02BA23C722EEF62C009539E7 /* order-stats-v4-wcadmin-deactivated.json */; };
5457
02BA23CA22EEF62C009539E7 /* order-stats-v4-wcadmin-activated.json in Resources */ = {isa = PBXBuildFile; fileRef = 02BA23C822EEF62C009539E7 /* order-stats-v4-wcadmin-activated.json */; };
5558
02BDB83523EA98C800BCC63E /* String+HTML.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02BDB83423EA98C800BCC63E /* String+HTML.swift */; };
5659
02BDB83723EA9C4D00BCC63E /* String+HTMLTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02BDB83623EA9C4D00BCC63E /* String+HTMLTests.swift */; };
60+
02BE0A7B274B695F001176D2 /* WordPressMediaMapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02BE0A7A274B695F001176D2 /* WordPressMediaMapper.swift */; };
5761
02C11276274285FF00F4F0B4 /* WooCommerceAvailabilityMapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02C11275274285FF00F4F0B4 /* WooCommerceAvailabilityMapper.swift */; };
5862
02C112782742862600F4F0B4 /* WordPressSiteSettingsMapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02C112772742862600F4F0B4 /* WordPressSiteSettingsMapper.swift */; };
5963
02C1CEF424C6A02B00703EBA /* ProductVariationMapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02C1CEF324C6A02B00703EBA /* ProductVariationMapper.swift */; };
@@ -632,10 +636,14 @@
632636
02A26F192744F5FC008E4EDB /* wc-site-settings-partial.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "wc-site-settings-partial.json"; sourceTree = "<group>"; };
633637
02A26F1A2744F5FC008E4EDB /* wp-site-settings.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "wp-site-settings.json"; sourceTree = "<group>"; };
634638
02AAD53E250092A300BA1E26 /* product-add-or-delete.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "product-add-or-delete.json"; sourceTree = "<group>"; };
639+
02AF07E927492DBC00B2D81E /* WordPressMedia.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WordPressMedia.swift; sourceTree = "<group>"; };
640+
02AF07EB27492FDD00B2D81E /* media-library-from-wordpress-site.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "media-library-from-wordpress-site.json"; sourceTree = "<group>"; };
641+
02AF07ED27493AE700B2D81E /* media-upload-to-wordpress-site.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "media-upload-to-wordpress-site.json"; sourceTree = "<group>"; };
635642
02BA23C722EEF62C009539E7 /* order-stats-v4-wcadmin-deactivated.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "order-stats-v4-wcadmin-deactivated.json"; sourceTree = "<group>"; };
636643
02BA23C822EEF62C009539E7 /* order-stats-v4-wcadmin-activated.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "order-stats-v4-wcadmin-activated.json"; sourceTree = "<group>"; };
637644
02BDB83423EA98C800BCC63E /* String+HTML.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+HTML.swift"; sourceTree = "<group>"; };
638645
02BDB83623EA9C4D00BCC63E /* String+HTMLTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+HTMLTests.swift"; sourceTree = "<group>"; };
646+
02BE0A7A274B695F001176D2 /* WordPressMediaMapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WordPressMediaMapper.swift; sourceTree = "<group>"; };
639647
02C11275274285FF00F4F0B4 /* WooCommerceAvailabilityMapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WooCommerceAvailabilityMapper.swift; sourceTree = "<group>"; };
640648
02C112772742862600F4F0B4 /* WordPressSiteSettingsMapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WordPressSiteSettingsMapper.swift; sourceTree = "<group>"; };
641649
02C1CEF324C6A02B00703EBA /* ProductVariationMapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductVariationMapper.swift; sourceTree = "<group>"; };
@@ -1173,6 +1181,7 @@
11731181
children = (
11741182
020D07B723D852BB00FD9580 /* Media.swift */,
11751183
020D07B923D8542000FD9580 /* UploadableMedia.swift */,
1184+
02AF07E927492DBC00B2D81E /* WordPressMedia.swift */,
11761185
);
11771186
path = Media;
11781187
sourceTree = "<group>";
@@ -1634,7 +1643,9 @@
16341643
B505F6D420BEE4E600BB1B69 /* me.json */,
16351644
93D8BBFE226BC1DA00AD2EB3 /* me-settings.json */,
16361645
02F096C12406691100C0C1D5 /* media-library.json */,
1646+
02AF07EB27492FDD00B2D81E /* media-library-from-wordpress-site.json */,
16371647
020D07C123D858BB00FD9580 /* media-upload.json */,
1648+
02AF07ED27493AE700B2D81E /* media-upload-to-wordpress-site.json */,
16381649
B58D10C92114D22E00107ED4 /* new-order-note.json */,
16391650
022902D322E2436400059692 /* no_stats_permission_error.json */,
16401651
B5A24178217F98F600595DEF /* notifications-load-all.json */,
@@ -1854,6 +1865,7 @@
18541865
FE28F6E326842848004465C7 /* UserMapper.swift */,
18551866
077F39D326A58DE700ABEADC /* SystemStatusMapper.swift */,
18561867
02C11275274285FF00F4F0B4 /* WooCommerceAvailabilityMapper.swift */,
1868+
02BE0A7A274B695F001176D2 /* WordPressMediaMapper.swift */,
18571869
02C112772742862600F4F0B4 /* WordPressSiteSettingsMapper.swift */,
18581870
);
18591871
path = Mapper;
@@ -2136,6 +2148,7 @@
21362148
45D685FA23D0C3CF005F87D0 /* product-search-sku.json in Resources */,
21372149
CCF48B2C2628AE160034EA83 /* shipping-label-account-settings.json in Resources */,
21382150
31054734262E36AB00C5C02B /* wcpay-payment-intent-error.json in Resources */,
2151+
02AF07EE27493AE700B2D81E /* media-upload-to-wordpress-site.json in Resources */,
21392152
45ED4F12239E8C57004F1BE3 /* taxes-classes.json in Resources */,
21402153
B5A2417B217F98FC00595DEF /* broken-notifications.json in Resources */,
21412154
3158A4A32729F42500C3CFA8 /* wcpay-account-dev-test.json in Resources */,
@@ -2145,6 +2158,7 @@
21452158
4599FC5C24A6276F0056157A /* product-tags-all.json in Resources */,
21462159
74A7B4BE217A841400E85A8B /* broken-settings-general.json in Resources */,
21472160
026CF624237D839B009563D4 /* product-variations-load-all.json in Resources */,
2161+
02AF07EC27492FDD00B2D81E /* media-library-from-wordpress-site.json in Resources */,
21482162
CC9A253C26442C71005DE56E /* shipping-label-eligibility-success.json in Resources */,
21492163
B5A24179217F98F600595DEF /* notifications-load-all.json in Resources */,
21502164
0282DD91233A120A006A5FDB /* products-search-photo.json in Resources */,
@@ -2439,6 +2453,7 @@
24392453
26B2F74524C5573F0065CCC8 /* LeaderboardListMapper.swift in Sources */,
24402454
24F98C542502E8DD00F49B68 /* Bundle+Woo.swift in Sources */,
24412455
31D27C812602889C002EDB1D /* SitePluginsRemote.swift in Sources */,
2456+
02BE0A7B274B695F001176D2 /* WordPressMediaMapper.swift in Sources */,
24422457
45150A9A268340D2006922EA /* Country.swift in Sources */,
24432458
450106852399A7CB00E24722 /* TaxClass.swift in Sources */,
24442459
CC0786612677B2DA00BA9AC1 /* ShippingLabelPurchaseMapper.swift in Sources */,
@@ -2482,6 +2497,7 @@
24822497
74A1D26F21189EA100931DFA /* SiteVisitStatsRemote.swift in Sources */,
24832498
029BA4F4255D72EC006171FD /* ShippingLabelPrintData.swift in Sources */,
24842499
24F98C562502EA4800F49B68 /* FeatureFlag.swift in Sources */,
2500+
02AF07EA27492DBC00B2D81E /* WordPressMedia.swift in Sources */,
24852501
B557DA1D20979E7D005962F4 /* Order.swift in Sources */,
24862502
74159625224D4045003C21CF /* SiteSettingGroup.swift in Sources */,
24872503
26B2F74D24C696E70065CCC8 /* LeaderboardRowContent.swift in Sources */,
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/// Mapper: WordPressMedia
2+
///
3+
struct WordPressMediaMapper: Mapper {
4+
/// (Attempts) to convert data into a WordPressMedia.
5+
func map(response: Data) throws -> WordPressMedia {
6+
let decoder = JSONDecoder()
7+
decoder.dateDecodingStrategy = .formatted(Constants.dateFormatterForDecoding)
8+
return try decoder.decode(WordPressMedia.self, from: response)
9+
}
10+
}
11+
12+
/// Mapper: WordPressMedia List
13+
///
14+
struct WordPressMediaListMapper: Mapper {
15+
/// (Attempts) to convert data into a WordPressMedia list.
16+
func map(response: Data) throws -> [WordPressMedia] {
17+
let decoder = JSONDecoder()
18+
decoder.dateDecodingStrategy = .formatted(Constants.dateFormatterForDecoding)
19+
return try decoder.decode([WordPressMedia].self, from: response)
20+
}
21+
}
22+
23+
private enum Constants {
24+
static let dateFormatterForDecoding = DateFormatter.Defaults.dateTimeFormatter
25+
}
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
/// Media from WordPress Site API
2+
public struct WordPressMedia: Equatable {
3+
public let mediaID: Int64
4+
public let date: Date
5+
public let slug: String
6+
public let mimeType: String
7+
public let src: String
8+
public let alt: String?
9+
public let details: MediaDetails?
10+
public let title: MediaTitle?
11+
12+
/// Media initializer.
13+
public init(mediaID: Int64,
14+
date: Date,
15+
slug: String,
16+
mimeType: String,
17+
src: String,
18+
alt: String?,
19+
details: MediaDetails?,
20+
title: MediaTitle?) {
21+
self.mediaID = mediaID
22+
self.date = date
23+
self.slug = slug
24+
self.mimeType = mimeType
25+
self.src = src
26+
self.alt = alt
27+
self.details = details
28+
self.title = title
29+
}
30+
}
31+
32+
extension WordPressMedia: Decodable {
33+
/// Decodable Initializer.
34+
public init(from decoder: Decoder) throws {
35+
let container = try decoder.container(keyedBy: CodingKeys.self)
36+
37+
let mediaID = try container.decode(Int64.self, forKey: .mediaID)
38+
let date = try container.decodeIfPresent(Date.self, forKey: .date) ?? Date()
39+
let slug = try container.decodeIfPresent(String.self, forKey: .slug) ?? ""
40+
let mimeType = try container.decodeIfPresent(String.self, forKey: .mimeType) ?? ""
41+
let src = try container.decodeIfPresent(String.self, forKey: .src) ?? ""
42+
let alt = try container.decodeIfPresent(String.self, forKey: .alt)
43+
let details = try container.decodeIfPresent(MediaDetails.self, forKey: .details)
44+
let title = try container.decodeIfPresent(MediaTitle.self, forKey: .title)
45+
46+
self.init(mediaID: mediaID,
47+
date: date,
48+
slug: slug,
49+
mimeType: mimeType,
50+
src: src,
51+
alt: alt,
52+
details: details,
53+
title: title)
54+
}
55+
}
56+
57+
public extension WordPressMedia {
58+
/// Details about a WordPress site media.
59+
struct MediaDetails: Decodable, Equatable {
60+
public let width: Double
61+
public let height: Double
62+
public let fileName: String
63+
public let sizes: [String: MediaSizeDetails]
64+
65+
enum CodingKeys: String, CodingKey {
66+
case width
67+
case height
68+
case fileName = "file"
69+
case sizes
70+
}
71+
}
72+
73+
/// Details about a size of WordPress site media (e.g. `thumbnail`, `medium`, `2048x2048`).
74+
struct MediaSizeDetails: Decodable, Equatable {
75+
public let fileName: String
76+
public let src: String
77+
public let width: Double
78+
public let height: Double
79+
80+
enum CodingKeys: String, CodingKey {
81+
case fileName = "file"
82+
case src = "source_url"
83+
case width
84+
case height
85+
}
86+
}
87+
88+
/// Title of the WordPress site media.
89+
struct MediaTitle: Decodable, Equatable {
90+
/// `GET` media list request's `title` field only contains `rendered`, while `POST` media request includes both `raw` and `rendered`.
91+
let rendered: String
92+
93+
enum CodingKeys: String, CodingKey {
94+
case rendered
95+
}
96+
}
97+
}
98+
99+
private extension WordPressMedia {
100+
enum CodingKeys: String, CodingKey {
101+
case mediaID = "id"
102+
case date = "date_gmt"
103+
case slug
104+
case mimeType = "mime_type"
105+
case src = "source_url"
106+
case alt = "alt_text"
107+
case details = "media_details"
108+
case title
109+
}
110+
}

Networking/Networking/Remote/MediaRemote.swift

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,35 @@ public class MediaRemote: Remote {
3535
enqueue(request, mapper: mapper, completion: completion)
3636
}
3737

38+
/// Loads an array of media from the site's WP Media Library via WordPress site API.
39+
/// API reference: https://developer.wordpress.org/rest-api/reference/media/#list-media
40+
///
41+
/// - Parameters:
42+
/// - siteID: Site for which we'll load the media from.
43+
/// - pageNumber: The index of the page of media data to load from, starting from 1.
44+
/// - pageSize: The number of media items to return.
45+
/// - completion: Closure to be executed upon completion.
46+
public func loadMediaLibraryFromWordPressSite(siteID: Int64,
47+
pageNumber: Int = Default.pageNumber,
48+
pageSize: Int = 25,
49+
completion: @escaping (Result<[WordPressMedia], Error>) -> Void) {
50+
let parameters: [String: Any] = [
51+
ParameterKey.pageSize: pageSize,
52+
ParameterKey.pageNumber: pageNumber,
53+
ParameterKey.fieldsWordPressSite: ParameterValue.wordPressMediaFields,
54+
ParameterKey.mimeType: "image"
55+
]
56+
57+
let path = "sites/\(siteID)/media"
58+
let request = DotcomRequest(wordpressApiVersion: .wpMark2,
59+
method: .get,
60+
path: path,
61+
parameters: parameters)
62+
let mapper = WordPressMediaListMapper()
63+
64+
enqueue(request, mapper: mapper, completion: completion)
65+
}
66+
3867
/// Uploads an array of media in the local file system.
3968
/// API reference: https://developer.wordpress.com/docs/api/1.1/post/sites/%24site/media/new/
4069
///
@@ -79,6 +108,40 @@ public class MediaRemote: Remote {
79108
}
80109
}, completion: completion)
81110
}
111+
112+
/// Uploads an array of media in the local file system to the WordPress site.via WordPress site API
113+
/// API reference: https://developer.wordpress.org/rest-api/reference/media/#create-a-media-item
114+
///
115+
/// - Parameters:
116+
/// - siteID: Site for which we'll upload the media to.
117+
/// - productID: Product for which the media items are first added to.
118+
/// - mediaItems: An array of uploadable media items.
119+
/// - completion: Closure to be executed upon completion.
120+
public func uploadMediaToWordPressSite(siteID: Int64,
121+
productID: Int64,
122+
mediaItems: [UploadableMedia],
123+
completion: @escaping (Result<WordPressMedia, Error>) -> Void) {
124+
let formParameters: [String: String] = [
125+
ParameterKey.wordPressMediaPostID: "\(productID)",
126+
ParameterKey.fieldsWordPressSite: ParameterValue.wordPressMediaFields,
127+
]
128+
let path = "sites/\(siteID)/media"
129+
let request = DotcomRequest(wordpressApiVersion: .wpMark2, method: .post, path: path, parameters: nil)
130+
let mapper = WordPressMediaMapper()
131+
132+
enqueueMultipartFormDataUpload(request, mapper: mapper, multipartFormData: { multipartFormData in
133+
formParameters.forEach { (key, value) in
134+
multipartFormData.append(Data(value.utf8), withName: key)
135+
}
136+
137+
mediaItems.forEach { mediaItem in
138+
multipartFormData.append(mediaItem.localURL,
139+
withName: ParameterValue.mediaUploadName,
140+
fileName: mediaItem.filename,
141+
mimeType: mediaItem.mimeType)
142+
}
143+
}, completion: completion)
144+
}
82145
}
83146

84147

@@ -93,8 +156,15 @@ public extension MediaRemote {
93156
private enum ParameterKey {
94157
static let pageNumber: String = "page"
95158
static let pageSize: String = "number"
159+
static let wordPressMediaPostID: String = "post"
96160
static let fields: String = "fields"
161+
static let fieldsWordPressSite: String = "_fields"
97162
static let mimeType: String = "mime_type"
98163
static let contextKey: String = "context"
99164
}
165+
166+
private enum ParameterValue {
167+
static let mediaUploadName: String = "file"
168+
static let wordPressMediaFields = "id,date_gmt,slug,mime_type,source_url,alt_text,media_details,title"
169+
}
100170
}

0 commit comments

Comments
 (0)