-
Notifications
You must be signed in to change notification settings - Fork 16
/
PostServiceRemoteXMLRPC.m
456 lines (405 loc) · 19 KB
/
PostServiceRemoteXMLRPC.m
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
#import "PostServiceRemoteXMLRPC.h"
#import "RemotePost.h"
#import "RemotePostCategory.h"
#import "NSMutableDictionary+Helpers.h"
#import "WPKit-Swift.h"
@import NSObject_SafeExpectations;
@import WordPressShared;
const NSInteger HTTP404ErrorCode = 404;
NSString * const WordPressAppErrorDomain = @"org.wordpress.iphone";
static NSString * const RemoteOptionKeyNumber = @"number";
static NSString * const RemoteOptionKeyOffset = @"offset";
static NSString * const RemoteOptionKeyOrder = @"order";
static NSString * const RemoteOptionKeyOrderBy = @"orderby";
static NSString * const RemoteOptionKeyStatus = @"post_status";
static NSString * const RemoteOptionKeySearch = @"s";
static NSString * const RemoteOptionValueOrderAscending = @"ASC";
static NSString * const RemoteOptionValueOrderDescending = @"DESC";
static NSString * const RemoteOptionValueOrderByDate = @"date";
static NSString * const RemoteOptionValueOrderByModified = @"modified";
static NSString * const RemoteOptionValueOrderByTitle = @"title";
static NSString * const RemoteOptionValueOrderByCommentCount = @"comment_count";
static NSString * const RemoteOptionValueOrderByPostID = @"ID";
@implementation PostServiceRemoteXMLRPC
- (void)getPostWithID:(NSNumber *)postID
success:(void (^)(RemotePost *post))success
failure:(void (^)(NSError *))failure
{
NSArray *parameters = [self XMLRPCArgumentsWithExtra:postID];
[self.api callMethod:@"wp.getPost"
parameters:parameters
success:^(id responseObject, NSHTTPURLResponse *httpResponse) {
if (success) {
success([self remotePostFromXMLRPCDictionary:responseObject]);
}
} failure:^(NSError *error, NSHTTPURLResponse *httpResponse) {
if (failure) {
failure(error);
}
}];
}
- (void)getPostsOfType:(NSString *)postType
success:(void (^)(NSArray <RemotePost *> *remotePosts))success
failure:(void (^)(NSError *))failure {
[self getPostsOfType:postType options:nil success:success failure:failure];
}
- (void)getPostsOfType:(NSString *)postType
options:(NSDictionary *)options
success:(void (^)(NSArray <RemotePost *> *remotePosts))success
failure:(void (^)(NSError *error))failure {
NSArray *statuses = @[PostStatusDraft, PostStatusPending, PostStatusPrivate, PostStatusPublish, PostStatusScheduled, PostStatusTrash];
NSString *postStatus = [statuses componentsJoinedByString:@","];
NSDictionary *extraParameters = @{
@"number": @40,
@"post_type": postType,
@"post_status": postStatus,
};
if (options) {
NSMutableDictionary *mutableParameters = [extraParameters mutableCopy];
[mutableParameters addEntriesFromDictionary:options];
extraParameters = [NSDictionary dictionaryWithDictionary:mutableParameters];
}
NSArray *parameters = [self XMLRPCArgumentsWithExtra:extraParameters];
[self.api callMethod:@"wp.getPosts"
parameters:parameters
success:^(id responseObject, NSHTTPURLResponse *httpResponse) {
NSAssert([responseObject isKindOfClass:[NSArray class]], @"Response should be an array.");
if (success) {
success([self remotePostsFromXMLRPCArray:responseObject]);
}
} failure:^(NSError *error, NSHTTPURLResponse *httpResponse) {
if (failure) {
failure(error);
}
}];
}
- (void)createPost:(RemotePost *)post
success:(void (^)(RemotePost *))success
failure:(void (^)(NSError *))failure
{
NSDictionary *extraParameters = [self parametersWithRemotePost:post];
NSArray *parameters = [self XMLRPCArgumentsWithExtra:extraParameters];
[self.api callMethod:@"metaWeblog.newPost"
parameters:parameters
success:^(id responseObject, NSHTTPURLResponse *httpResponse) {
if ([responseObject respondsToSelector:@selector(numericValue)]) {
post.postID = [responseObject numericValue];
if (!post.date) {
// Set the temporary date until we get it from the server so it sorts properly on the list
post.date = [NSDate date];
}
[self getPostWithID:post.postID success:^(RemotePost *fetchedPost) {
if (success) {
success(fetchedPost);
}
} failure:^(NSError *error) {
// update failed, and that sucks, but creating the post succeeded… so, let's just act like everything is ok!
if (success) {
success(post);
}
}];
} else if (failure) {
NSDictionary *userInfo = @{NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Invalid value returned for new post: %@", responseObject]};
NSError *error = [NSError errorWithDomain:WordPressAppErrorDomain code:0 userInfo:userInfo];
failure(error);
}
} failure:^(NSError *error, NSHTTPURLResponse *httpResponse) {
if (failure) {
failure(error);
}
}];
}
- (void)updatePost:(RemotePost *)post
success:(void (^)(RemotePost *))success
failure:(void (^)(NSError *))failure
{
NSParameterAssert(post.postID.integerValue > 0);
if ([post.postID integerValue] <= 0) {
if (failure) {
NSDictionary *userInfo = @{NSLocalizedDescriptionKey: @"Can't edit a post if it's not in the server"};
NSError *error = [NSError errorWithDomain:WordPressAppErrorDomain code:0 userInfo:userInfo];
dispatch_async(dispatch_get_main_queue(), ^{
failure(error);
});
}
return;
}
NSDictionary *extraParameters = [self parametersWithRemotePost:post];
NSMutableArray *parameters = [NSMutableArray arrayWithArray:[self XMLRPCArgumentsWithExtra:extraParameters]];
[parameters replaceObjectAtIndex:0 withObject:post.postID];
[self.api callMethod:@"metaWeblog.editPost"
parameters:parameters
success:^(id responseObject, NSHTTPURLResponse *httpResponse) {
[self getPostWithID:post.postID success:^(RemotePost *fetchedPost) {
if (success) {
success(fetchedPost);
}
} failure:^(NSError *error) {
//We failed to fetch the post but the update was successful
if (success) {
success(post);
}
}];
} failure:^(NSError *error, NSHTTPURLResponse *httpResponse) {
if (failure) {
failure(error);
}
}];
}
- (void)deletePost:(RemotePost *)post
success:(void (^)(void))success
failure:(void (^)(NSError *))failure
{
NSParameterAssert([post.postID longLongValue] > 0);
NSNumber *postID = post.postID;
if ([postID longLongValue] > 0) {
NSArray *parameters = [self XMLRPCArgumentsWithExtra:postID];
[self.api callMethod:@"wp.deletePost"
parameters:parameters
success:^(id responseObject, NSHTTPURLResponse *httpResponse) {
if (success) success();
} failure:^(NSError *error, NSHTTPURLResponse *httpResponse) {
if (failure) failure(error);
}];
}
}
- (void)trashPost:(RemotePost *)post
success:(void (^)(RemotePost *))success
failure:(void (^)(NSError *))failure
{
NSParameterAssert([post.postID longLongValue] > 0);
NSNumber *postID = post.postID;
if ([postID longLongValue] <= 0) {
return;
}
NSArray *parameters = [self XMLRPCArgumentsWithExtra:postID];
[self.api callMethod:@"wp.deletePost"
parameters:parameters
success:^(id responseObject, NSHTTPURLResponse *httpResponse) {
[self.api callMethod:@"wp.getPost"
parameters:parameters
success:^(id responseObject, NSHTTPURLResponse *httpResponse) {
if (success) {
// The post was trashed but not yet deleted.
RemotePost *post = [self remotePostFromXMLRPCDictionary:responseObject];
success(post);
}
} failure:^(NSError *error, NSHTTPURLResponse *httpResponse) {
if (httpResponse.statusCode == HTTP404ErrorCode) {
// The post was deleted.
if (success) {
success(post);
}
}
}];
} failure:^(NSError *error, NSHTTPURLResponse *httpResponse) {
if (failure) {
failure(error);
}
}];
}
- (void)restorePost:(RemotePost *)post
success:(void (^)(RemotePost *))success
failure:(void (^)(NSError *error))failure
{
[self updatePost:post success:success failure:failure];
}
- (NSDictionary *)dictionaryWithRemoteOptions:(id <PostServiceRemoteOptions>)options
{
NSMutableDictionary *remoteParams = [NSMutableDictionary dictionary];
if (options.number) {
[remoteParams setObject:options.number forKey:RemoteOptionKeyNumber];
}
if (options.offset) {
[remoteParams setObject:options.offset forKey:RemoteOptionKeyOffset];
}
NSString *statusesStr = nil;
if (options.statuses.count) {
statusesStr = [options.statuses componentsJoinedByString:@","];
}
if (options.order) {
NSString *orderStr = nil;
switch (options.order) {
case PostServiceResultsOrderDescending:
orderStr = RemoteOptionValueOrderDescending;
break;
case PostServiceResultsOrderAscending:
orderStr = RemoteOptionValueOrderAscending;
break;
}
[remoteParams setObject:orderStr forKey:RemoteOptionKeyOrder];
}
NSString *orderByStr = nil;
if (options.orderBy) {
switch (options.orderBy) {
case PostServiceResultsOrderingByDate:
orderByStr = RemoteOptionValueOrderByDate;
break;
case PostServiceResultsOrderingByModified:
orderByStr = RemoteOptionValueOrderByModified;
break;
case PostServiceResultsOrderingByTitle:
orderByStr = RemoteOptionValueOrderByTitle;
break;
case PostServiceResultsOrderingByCommentCount:
orderByStr = RemoteOptionValueOrderByCommentCount;
break;
case PostServiceResultsOrderingByPostID:
orderByStr = RemoteOptionValueOrderByPostID;
break;
}
}
if (statusesStr.length) {
[remoteParams setObject:statusesStr forKey:RemoteOptionKeyStatus];
}
if (orderByStr.length) {
[remoteParams setObject:orderByStr forKey:RemoteOptionKeyOrderBy];
}
NSString *search = [options search];
if (search.length) {
[remoteParams setObject:search forKey:RemoteOptionKeySearch];
}
return remoteParams.count ? [NSDictionary dictionaryWithDictionary:remoteParams] : nil;
}
#pragma mark - Private methods
- (NSArray <RemotePost *> *)remotePostsFromXMLRPCArray:(NSArray *)xmlrpcArray {
return [xmlrpcArray wp_map:^id(NSDictionary *xmlrpcPost) {
return [self remotePostFromXMLRPCDictionary:xmlrpcPost];
}];
}
- (RemotePost *)remotePostFromXMLRPCDictionary:(NSDictionary *)xmlrpcDictionary {
RemotePost *post = [RemotePost new];
post.postID = [xmlrpcDictionary numberForKey:@"post_id"];
post.date = xmlrpcDictionary[@"post_date_gmt"];
post.dateModified = xmlrpcDictionary[@"post_modified_gmt"];
if (xmlrpcDictionary[@"link"]) {
post.URL = [NSURL URLWithString:xmlrpcDictionary[@"link"]];
}
post.title = xmlrpcDictionary[@"post_title"];
post.content = xmlrpcDictionary[@"post_content"];
post.excerpt = xmlrpcDictionary[@"post_excerpt"];
post.slug = xmlrpcDictionary[@"post_name"];
post.authorID = [xmlrpcDictionary numberForKey:@"post_author"];
post.status = [self statusForPostStatus:xmlrpcDictionary[@"post_status"] andDate:post.date];
post.password = xmlrpcDictionary[@"post_password"];
if ([post.password isEmpty]) {
post.password = nil;
}
post.parentID = [xmlrpcDictionary numberForKey:@"post_parent"];
// When there is no featured image, post_thumbnail is an empty array :(
NSDictionary *thumbnailDict = [xmlrpcDictionary dictionaryForKey:@"post_thumbnail"];
post.postThumbnailID = [thumbnailDict numberForKey:@"attachment_id"];
post.postThumbnailPath = [thumbnailDict stringForKey:@"link"];
post.type = xmlrpcDictionary[@"post_type"];
post.format = xmlrpcDictionary[@"post_format"];
post.metadata = xmlrpcDictionary[@"custom_fields"];
NSArray *terms = [xmlrpcDictionary arrayForKey:@"terms"];
post.tags = [self tagsFromXMLRPCTermsArray:terms];
post.categories = [self remoteCategoriesFromXMLRPCTermsArray:terms];
post.isStickyPost = [xmlrpcDictionary numberForKeyPath:@"sticky"];
// Pick an image to use for display
if (post.postThumbnailPath) {
post.pathForDisplayImage = post.postThumbnailPath;
} else {
// parse content for a suitable image.
post.pathForDisplayImage = [DisplayableImageHelper searchPostContentForImageToDisplay:post.content];
}
return post;
}
- (NSString *)statusForPostStatus:(NSString *)status andDate:(NSDate *)date
{
// Scheduled posts are synced with a post_status of 'publish' but we want to
// work with a status of 'future' from within the app.
if ([status isEqualToString:PostStatusPublish] && date == [date laterDate:[NSDate date]]) {
return PostStatusScheduled;
}
return status;
}
- (NSArray *)tagsFromXMLRPCTermsArray:(NSArray *)terms {
NSArray *tags = [terms filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"taxonomy = 'post_tag' AND name != NIL"]];
return [tags valueForKey:@"name"];
}
- (NSArray *)remoteCategoriesFromXMLRPCTermsArray:(NSArray *)terms {
return [[terms wp_filter:^BOOL(NSDictionary *category) {
return [[category stringForKey:@"taxonomy"] isEqualToString:@"category"];
}] wp_map:^id(NSDictionary *category) {
return [self remoteCategoryFromXMLRPCDictionary:category];
}];
}
- (RemotePostCategory *)remoteCategoryFromXMLRPCDictionary:(NSDictionary *)xmlrpcCategory {
RemotePostCategory *category = [RemotePostCategory new];
category.categoryID = [xmlrpcCategory numberForKey:@"term_id"];
category.name = [xmlrpcCategory stringForKey:@"name"];
category.parentID = [xmlrpcCategory numberForKey:@"parent"];
return category;
}
- (NSDictionary *)parametersWithRemotePost:(RemotePost *)post
{
BOOL existingPost = ([post.postID longLongValue] > 0);
NSMutableDictionary *postParams = [NSMutableDictionary dictionary];
[postParams setValueIfNotNil:post.type forKey:@"post_type"];
[postParams setValueIfNotNil:post.title forKey:@"title"];
[postParams setValueIfNotNil:post.content forKey:@"description"];
[postParams setValueIfNotNil:post.date forKey:@"date_created_gmt"];
[postParams setValueIfNotNil:post.password forKey:@"wp_password"];
[postParams setValueIfNotNil:[post.URL absoluteString] forKey:@"permalink"];
[postParams setValueIfNotNil:post.excerpt forKey:@"mt_excerpt"];
[postParams setValueIfNotNil:post.slug forKey:@"wp_slug"];
[postParams setValueIfNotNil:post.authorID forKey:@"wp_author_id"];
// To remove a featured image, you have to send an empty string to the API
if (post.postThumbnailID == nil) {
// Including an empty string for wp_post_thumbnail generates
// an "Invalid attachment ID" error in the call to wp.newPage
if (existingPost) {
postParams[@"wp_post_thumbnail"] = @"";
}
} else if (!existingPost || post.isFeaturedImageChanged) {
// Do not add this param to existing posts when the featured image has not changed.
// Doing so results in a XML-RPC fault: Invalid attachment ID.
postParams[@"wp_post_thumbnail"] = post.postThumbnailID;
}
[postParams setValueIfNotNil:post.format forKey:@"wp_post_format"];
[postParams setValueIfNotNil:[post.tags componentsJoinedByString:@","] forKey:@"mt_keywords"];
if (existingPost && post.date == nil) {
// Change the date of an already published post to the current date/time. (publish immediately)
// Pass the current date so the post is updated correctly
postParams[@"date_created_gmt"] = [NSDate date];
}
if (post.categories) {
NSArray *categoryNames = [post.categories wp_map:^id(RemotePostCategory *category) {
return category.name;
}];
postParams[@"categories"] = categoryNames;
}
if ([post.metadata count] > 0) {
postParams[@"custom_fields"] = post.metadata;
}
postParams[@"wp_page_parent_id"] = post.parentID ? post.parentID.stringValue : @"0";
// Scheduled posts need to sync with a status of 'publish'.
// Passing a status of 'future' will set the post status to 'draft'
// This is an apparent inconsistency in the XML-RPC API as 'future' should
// be a valid status.
// https://codex.wordpress.org/Post_Status_Transitions
if (post.status == nil || [post.status isEqualToString:PostStatusScheduled]) {
post.status = PostStatusPublish;
}
// At least as of 5.2.2, Private and/or Password Protected posts can't be stickied.
// However, the code used on the backend doesn't check the value of the `sticky` field,
// instead doing a simple `! empty( $post_data['sticky'] )` check.
//
// This means we have to omit this field entirely for those posts from the payload we're sending
// to the XML-RPC sevices.
//
// https://github.com/WordPress/WordPress/blob/master/wp-includes/class-wp-xmlrpc-server.php
//
BOOL shouldIncludeStickyField = ![post.status isEqualToString:PostStatusPrivate] && post.password == nil;
if (post.isStickyPost != nil && shouldIncludeStickyField) {
postParams[@"sticky"] = post.isStickyPost.boolValue ? @"true" : @"false";
}
if ([post.type isEqualToString:@"page"]) {
[postParams setObject:post.status forKey:@"page_status"];
}
[postParams setObject:post.status forKey:@"post_status"];
return [NSDictionary dictionaryWithDictionary:postParams];
}
@end