├── .gitignore ├── .travis.yml ├── AppledocSettings.plist ├── Example ├── Podfile ├── Podfile.lock ├── Tests │ ├── Info.plist │ ├── MockData │ │ ├── fault_rpc_call.xml │ │ ├── html_page_with_link_to_rsd.html │ │ ├── html_page_with_link_to_rsd_non_standard.html │ │ ├── plugin_redirect.html │ │ ├── redirect.html │ │ ├── rsd.xml │ │ └── system_list_methods.xml │ ├── WordPressApiTests.m │ └── WordPressXMLRPCApiTests.m ├── WordPressApiExample.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ └── WordPressApiExample.xccheckout │ └── xcshareddata │ │ └── xcschemes │ │ └── WordPressApiExample.xcscheme ├── WordPressApiExample.xcworkspace │ └── contents.xcworkspacedata └── WordPressApiExample │ ├── AppDelegate.h │ ├── AppDelegate.m │ ├── ComposeViewController.h │ ├── ComposeViewController.m │ ├── Images.xcassets │ ├── AppIcon.appiconset │ │ └── Contents.json │ └── Contents.json │ ├── LoginViewController.h │ ├── LoginViewController.m │ ├── PostViewController.h │ ├── PostViewController.m │ ├── PostsViewController.h │ ├── PostsViewController.m │ ├── WordPressApiExample-Info.plist │ ├── WordPressApiExample-Prefix.pch │ ├── en.lproj │ ├── InfoPlist.strings │ ├── LaunchScreen.storyboard │ └── MainStoryboard.storyboard │ └── main.m ├── LICENSE.md ├── Pod ├── WPComOAuthController.h ├── WPComOAuthController.m ├── WPHTTPAuthenticationAlertController.h ├── WPHTTPAuthenticationAlertController.m ├── WPHTTPRequestOperation.h ├── WPHTTPRequestOperation.m ├── WPRSDParser.h ├── WPRSDParser.m ├── WPXMLRPCClient.h ├── WPXMLRPCClient.m ├── WPXMLRPCRequest.h ├── WPXMLRPCRequest.m ├── WPXMLRPCRequestOperation.h ├── WPXMLRPCRequestOperation.m ├── WordPressApi-Prefix.pch ├── WordPressApi.h ├── WordPressApi.m ├── WordPressBaseApi.h ├── WordPressRestApi.h ├── WordPressRestApi.m ├── WordPressRestApiJSONRequestOperation.h ├── WordPressRestApiJSONRequestOperation.m ├── WordPressRestApiJSONRequestOperationManager.h ├── WordPressRestApiJSONRequestOperationManager.m ├── WordPressXMLRPCApi.h └── WordPressXMLRPCApi.m ├── README.md └── WordPressApi.podspec /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | .DS_Store 3 | */build/* 4 | *.pbxuser 5 | !default.pbxuser 6 | *.mode1v3 7 | !default.mode1v3 8 | *.mode2v3 9 | !default.mode2v3 10 | *.perspectivev3 11 | !default.perspectivev3 12 | xcuserdata 13 | profile 14 | *.moved-aside 15 | DerivedData 16 | .idea/ 17 | *.hmap 18 | Pods/ 19 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | install: 2 | - gem install cocoapods -v 1.0.0 3 | osx_image: xcode7.1 4 | sudo: false 5 | language: objective-c 6 | podfile: Example/Podfile 7 | before_script: 8 | - cd Example && pod install && cd - 9 | script: 10 | - set -o pipefail && xcodebuild test -workspace Example/WordPressApiExample.xcworkspace -scheme WordPressApiExample -sdk iphonesimulator | xcpretty -c 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /AppledocSettings.plist: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | --input 7 | 8 | WordPressApi 9 | 10 | --index-desc 11 | README.md 12 | --output 13 | docs 14 | --project-name 15 | WordPress API for iOS 16 | --project-company 17 | WordPress 18 | --company-id 19 | org.wordpress 20 | --keep-undocumented-members 21 | 22 | --keep-intermediate-files 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /Example/Podfile: -------------------------------------------------------------------------------- 1 | source 'https://github.com/CocoaPods/Specs.git' 2 | 3 | platform :ios, '8.0' 4 | 5 | target 'WordPressApiExample' do 6 | pod 'WordPressApi', :path => '../' 7 | 8 | target 'Tests' do 9 | pod 'OHHTTPStubs', '4.7.1' 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /Example/Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - AFNetworking (2.6.3): 3 | - AFNetworking/NSURLConnection (= 2.6.3) 4 | - AFNetworking/NSURLSession (= 2.6.3) 5 | - AFNetworking/Reachability (= 2.6.3) 6 | - AFNetworking/Security (= 2.6.3) 7 | - AFNetworking/Serialization (= 2.6.3) 8 | - AFNetworking/UIKit (= 2.6.3) 9 | - AFNetworking/NSURLConnection (2.6.3): 10 | - AFNetworking/Reachability 11 | - AFNetworking/Security 12 | - AFNetworking/Serialization 13 | - AFNetworking/NSURLSession (2.6.3): 14 | - AFNetworking/Reachability 15 | - AFNetworking/Security 16 | - AFNetworking/Serialization 17 | - AFNetworking/Reachability (2.6.3) 18 | - AFNetworking/Security (2.6.3) 19 | - AFNetworking/Serialization (2.6.3) 20 | - AFNetworking/UIKit (2.6.3): 21 | - AFNetworking/NSURLConnection 22 | - AFNetworking/NSURLSession 23 | - OHHTTPStubs (4.7.1): 24 | - OHHTTPStubs/Default (= 4.7.1) 25 | - OHHTTPStubs/Core (4.7.1) 26 | - OHHTTPStubs/Default (4.7.1): 27 | - OHHTTPStubs/Core 28 | - OHHTTPStubs/JSON 29 | - OHHTTPStubs/NSURLSession 30 | - OHHTTPStubs/OHPathHelpers 31 | - OHHTTPStubs/JSON (4.7.1): 32 | - OHHTTPStubs/Core 33 | - OHHTTPStubs/NSURLSession (4.7.1): 34 | - OHHTTPStubs/Core 35 | - OHHTTPStubs/OHPathHelpers (4.7.1) 36 | - WordPressApi (0.4.0): 37 | - AFNetworking (~> 2.6.0) 38 | - wpxmlrpc (~> 0.8) 39 | - wpxmlrpc (0.8.2) 40 | 41 | DEPENDENCIES: 42 | - OHHTTPStubs (= 4.7.1) 43 | - WordPressApi (from `../`) 44 | 45 | EXTERNAL SOURCES: 46 | WordPressApi: 47 | :path: ../ 48 | 49 | SPEC CHECKSUMS: 50 | AFNetworking: cb8d14a848e831097108418f5d49217339d4eb60 51 | OHHTTPStubs: f7d1604d04d37d055460c0ef9af01bb249ded0fa 52 | WordPressApi: 483767025bcdab26429b51bfe5171f17d3d30466 53 | wpxmlrpc: 38623cc415117914d6ab5bf2ab8a57a4076cc469 54 | 55 | PODFILE CHECKSUM: c36e5d89b66b25ba4963c6fb344ea78a44c49654 56 | 57 | COCOAPODS: 1.0.0 58 | -------------------------------------------------------------------------------- /Example/Tests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /Example/Tests/MockData/fault_rpc_call.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /Example/Tests/MockData/html_page_with_link_to_rsd.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Page not found – iOS Test 10 | 11 | 12 | 16 | 30 | 31 | 32 | 33 | 36 | 39 | 42 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 |
56 |
57 | 58 | 59 | 69 | 70 |
71 | 72 |
73 |
74 | 75 |
76 | 79 | 80 |
81 |

It looks like nothing was found at this location. Maybe try a search?

82 | 83 | 84 | 91 |
92 |
93 | 94 |
95 | 96 | 97 |
98 | 99 | 100 | 146 | 147 |
148 | 149 | 157 |
158 |
159 | 160 | 161 | 166 | 167 | 168 | 169 | 170 | -------------------------------------------------------------------------------- /Example/Tests/MockData/html_page_with_link_to_rsd_non_standard.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Page not found – iOS Test 10 | 11 | 12 | 16 | 30 | 31 | 32 | 33 | 36 | 39 | 42 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 |
56 |
57 | 58 | 59 | 69 | 70 |
71 | 72 |
73 |
74 | 75 |
76 | 79 | 80 |
81 |

It looks like nothing was found at this location. Maybe try a search?

82 | 83 | 84 | 91 |
92 |
93 | 94 |
95 | 96 | 97 |
98 | 99 | 100 | 146 | 147 |
148 | 149 | 157 |
158 |
159 | 160 | 161 | 166 | 167 | 168 | 169 | 170 | -------------------------------------------------------------------------------- /Example/Tests/MockData/plugin_redirect.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Example/Tests/MockData/redirect.html: -------------------------------------------------------------------------------- 1 | HTTP/1.1 301 Moved Permanently 2 | Location: https://mywordpresssite.com/xmlrpc.php -------------------------------------------------------------------------------- /Example/Tests/MockData/rsd.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | WordPress 4 | https://wordpress.org/ 5 | https://iostest.wpsandbox.me 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /Example/Tests/MockData/system_list_methods.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | wp.getUsersBlogs 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /Example/Tests/WordPressApiTests.m: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | #import 4 | #import 5 | 6 | @interface WordPressApiTests : XCTestCase 7 | 8 | @end 9 | 10 | 11 | @implementation WordPressApiTests 12 | 13 | - (void)setUp { 14 | [super setUp]; 15 | // Put setup code here. This method is called before the invocation of each test method in the class. 16 | } 17 | 18 | - (void)tearDown { 19 | // Put teardown code here. This method is called after the invocation of each test method in the class. 20 | [super tearDown]; 21 | [OHHTTPStubs removeAllStubs]; 22 | } 23 | 24 | - (void)testServerSide404Response 25 | { 26 | __block NSError *errorToCheck = nil; 27 | XCTestExpectation *expectation = [self expectationWithDescription:@"Call should fail with error when server returns 404"]; 28 | NSString *originalHost = @"mywordpresssite.com"; 29 | [OHHTTPStubs stubRequestsPassingTest:^BOOL(NSURLRequest *request) { 30 | return [request.URL.host isEqualToString:originalHost]; 31 | } withStubResponse:^OHHTTPStubsResponse*(NSURLRequest *request) { 32 | return [[OHHTTPStubsResponse responseWithData:[NSData data] statusCode:404 headers:nil] 33 | responseTime:OHHTTPStubsDownloadSpeedWifi]; 34 | }]; 35 | 36 | [WordPressApi signInWithURL:@"mywordpresssite.com" 37 | username:@"username" 38 | password:@"password" 39 | success:^(NSURL *xmlrpcURL) { 40 | XCTFail(@"Call to site returning a 404 should not enter success block."); 41 | } failure:^(NSError *error) { 42 | [expectation fulfill]; 43 | errorToCheck = error; 44 | }]; 45 | 46 | [self waitForExpectationsWithTimeout:3 handler:nil]; 47 | XCTAssertNotNil(errorToCheck, @"Expected to get a error object"); 48 | XCTAssertNotNil(errorToCheck.userInfo, @"Expected to get a user info object in the error"); 49 | XCTAssertTrue([errorToCheck.userInfo[@"NSLocalizedDescription"] rangeOfString:@"404"].location != NSNotFound, @"Expected to get a 404 in the error description"); 50 | 51 | NSHTTPURLResponse *httpResponse = errorToCheck.userInfo[@"com.alamofire.serialization.response.error.response"]; 52 | XCTAssertNotNil(httpResponse, @"Expected to receive a HTTP response object in the error"); 53 | XCTAssertEqual(httpResponse.statusCode, 404, @"Expected the status code in the response to be a 404"); 54 | NSURLComponents *httpResponseURLComponents = [NSURLComponents componentsWithURL:httpResponse.URL resolvingAgainstBaseURL:YES]; 55 | XCTAssertNotNil(httpResponseURLComponents, @"Expected to receive a URL object in the response"); 56 | XCTAssertTrue([originalHost isEqualToString:httpResponseURLComponents.host], @"Expected the response hostname and original hostname to match"); 57 | 58 | NSURLComponents *errorURLComponents = errorToCheck.userInfo[@"NSErrorFailingURLKey"]; 59 | XCTAssertNotNil(errorURLComponents, @"Expected to receive a URL object in the error"); 60 | XCTAssertTrue([originalHost isEqualToString:errorURLComponents.host], @"Expected the error hostname and original hostname to match"); 61 | } 62 | 63 | - (void)testServerSide301Response 64 | { 65 | __block NSURL *urlToCheck = nil; 66 | XCTestExpectation *expectation = [self expectationWithDescription:@"Call should succeed when server returns 301 and has valid location defined in the response header"]; 67 | NSString *hostName = @"mywordpresssite.com"; 68 | NSString *originalURL = [@"http://" stringByAppendingString:hostName]; 69 | NSString *redirectedURL = [@"https://" stringByAppendingString:hostName]; 70 | 71 | [OHHTTPStubs stubRequestsPassingTest:^BOOL(NSURLRequest *request) { 72 | return [[request.URL absoluteString] isEqualToString:originalURL]; 73 | } withStubResponse:^OHHTTPStubsResponse*(NSURLRequest *request) { 74 | NSBundle *bundle = [NSBundle bundleForClass:[self class]]; 75 | NSURL *mockDataURL = [bundle URLForResource:@"redirect" withExtension:@"html"]; 76 | NSData *mockData = [NSData dataWithContentsOfURL:mockDataURL]; 77 | return [[OHHTTPStubsResponse responseWithData:mockData statusCode:301 headers:@{@"Location": redirectedURL}] 78 | responseTime:OHHTTPStubsDownloadSpeedWifi]; 79 | }]; 80 | 81 | [OHHTTPStubs stubRequestsPassingTest:^BOOL(NSURLRequest *request) { 82 | return [request.URL.absoluteString isEqualToString:redirectedURL]; 83 | } withStubResponse:^OHHTTPStubsResponse*(NSURLRequest *request) { 84 | NSURL *mockDataURL = [[NSBundle bundleForClass:[self class]] URLForResource:@"system_list_methods" withExtension:@"xml"]; 85 | NSData *mockData = [NSData dataWithContentsOfURL:mockDataURL]; 86 | return [[OHHTTPStubsResponse responseWithData:mockData statusCode:200 headers:nil] 87 | responseTime:OHHTTPStubsDownloadSpeedWifi]; 88 | }]; 89 | 90 | [WordPressApi signInWithURL:originalURL 91 | username:@"username" 92 | password:@"password" 93 | success:^(NSURL *xmlrpcURL) { 94 | [expectation fulfill]; 95 | urlToCheck = xmlrpcURL; 96 | } failure:^(NSError *error) { 97 | XCTFail(@"Call to site returning a valid 301 should not enter failure block."); 98 | }]; 99 | 100 | [self waitForExpectationsWithTimeout:3 handler:nil]; 101 | XCTAssertNotNil(urlToCheck, @"Expected to receive a URL object in the success block."); 102 | XCTAssertFalse([[urlToCheck absoluteString] isEqualToString:originalURL], @"Did not expect the success block URL and original URL to match"); 103 | XCTAssertTrue([[urlToCheck absoluteString] isEqualToString:redirectedURL], @"Expected the success block URL and redirected URL to match"); 104 | } 105 | 106 | - (void)testServerSide301ResponseWithNoLocation 107 | { 108 | __block NSError *errorToCheck = nil; 109 | XCTestExpectation *expectation = [self expectationWithDescription:@"Call should fail with error when server returns 301 and no location response header"]; 110 | NSString *hostName = @"mywordpresssite.com"; 111 | NSString *baseURL = [@"http://" stringByAppendingString:hostName]; 112 | 113 | [OHHTTPStubs stubRequestsPassingTest:^BOOL(NSURLRequest *request) { 114 | return [[request.URL absoluteString] isEqualToString:baseURL]; 115 | } withStubResponse:^OHHTTPStubsResponse*(NSURLRequest *request) { 116 | return [[OHHTTPStubsResponse responseWithData:[NSData data] statusCode:301 headers:nil] 117 | responseTime:OHHTTPStubsDownloadSpeedWifi]; 118 | }]; 119 | 120 | [WordPressApi signInWithURL:baseURL 121 | username:@"username" 122 | password:@"password" 123 | success:^(NSURL *xmlrpcURL) { 124 | XCTFail(@"Call to site returning a 301 should not enter success block."); 125 | } failure:^(NSError *error) { 126 | [expectation fulfill]; 127 | errorToCheck = error; 128 | }]; 129 | 130 | [self waitForExpectationsWithTimeout:3 handler:nil]; 131 | XCTAssertNotNil(errorToCheck, @"Expected to get a error object"); 132 | XCTAssertNotNil(errorToCheck.userInfo, @"Expected to get a user info object in the error"); 133 | XCTAssertTrue([errorToCheck.userInfo[@"NSLocalizedDescription"] rangeOfString:@"301"].location != NSNotFound, @"Expected to get a 301 in the error description"); 134 | 135 | NSHTTPURLResponse *httpResponse = errorToCheck.userInfo[@"com.alamofire.serialization.response.error.response"]; 136 | XCTAssertNotNil(httpResponse, @"Expected to receive a HTTP response object in the error"); 137 | XCTAssertEqual(httpResponse.statusCode, 301, @"Expected the status code in the response to be a 301"); 138 | NSURLComponents *httpResponseURLComponents = [NSURLComponents componentsWithURL:httpResponse.URL resolvingAgainstBaseURL:YES]; 139 | XCTAssertNotNil(httpResponseURLComponents, @"Expected to receive a URL object in the response"); 140 | XCTAssertTrue([[httpResponseURLComponents.URL absoluteString] isEqualToString:baseURL], @"Expected the response hostname and original hostname to match"); 141 | 142 | NSURLComponents *errorURLComponents = errorToCheck.userInfo[@"NSErrorFailingURLKey"]; 143 | XCTAssertNotNil(errorURLComponents, @"Expected to receive a URL object in the error"); 144 | XCTAssertTrue([errorURLComponents.host isEqualToString:hostName], @"Expected the error hostname and original hostname to match"); 145 | } 146 | 147 | @end 148 | -------------------------------------------------------------------------------- /Example/Tests/WordPressXMLRPCApiTests.m: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | #import 4 | #import 5 | 6 | @interface WordPressXMLRPCApiTests : XCTestCase 7 | 8 | @end 9 | 10 | @implementation WordPressXMLRPCApiTests 11 | 12 | - (void)setUp { 13 | [super setUp]; 14 | // Put setup code here. This method is called before the invocation of each test method in the class. 15 | } 16 | 17 | - (void)tearDown { 18 | // Put teardown code here. This method is called after the invocation of each test method in the class. 19 | [super tearDown]; 20 | [OHHTTPStubs removeAllStubs]; 21 | } 22 | 23 | - (void)testGuessXMLRPCURLForSiteForEmptyURLs { 24 | __block NSError *errorToCheck = nil; 25 | NSArray *emptyURLs = @[ 26 | @"", 27 | @" ", 28 | @"\t ", 29 | ]; 30 | for (NSString *emptyURL in emptyURLs) { 31 | XCTestExpectation *expectationEmpty = [self expectationWithDescription:@"Call should fail with error when invoking with empty string"]; 32 | [WordPressXMLRPCApi guessXMLRPCURLForSite:emptyURL success:^(NSURL *xmlrpcURL) { 33 | } failure:^(NSError *error) { 34 | NSLog(@"%@", [error localizedDescription]); 35 | [expectationEmpty fulfill]; 36 | errorToCheck = error; 37 | }]; 38 | [self waitForExpectationsWithTimeout:2 handler:nil]; 39 | XCTAssertTrue(errorToCheck.domain == WordPressXMLRPCApiErrorDomain, @"Expected to get an WordPressXMLRPCApiErrorDomain error"); 40 | XCTAssertTrue(errorToCheck.code == WordPressXMLRPCApiEmptyURL, @"Expected to get an WordPressXMLRPCApiEmptyURL error"); 41 | } 42 | 43 | 44 | XCTestExpectation *expectationNil = [self expectationWithDescription:@"Call should fail with error when invoking with nil string"]; 45 | [WordPressXMLRPCApi guessXMLRPCURLForSite:nil success:^(NSURL *xmlrpcURL) { 46 | } failure:^(NSError *error) { 47 | NSLog(@"%@", [error localizedDescription]); 48 | [expectationNil fulfill]; 49 | errorToCheck = error; 50 | }]; 51 | [self waitForExpectationsWithTimeout:2 handler:nil]; 52 | XCTAssertTrue(errorToCheck.domain == WordPressXMLRPCApiErrorDomain, @"Expected to get an WordPressXMLRPCApiErrorDomain error"); 53 | XCTAssertTrue(errorToCheck.code == WordPressXMLRPCApiEmptyURL, @"Expected to get an WordPressXMLRPCApiEmptyURL error"); 54 | } 55 | 56 | - (void)testGuessXMLRPCURLForSiteForMalformedURLs { 57 | __block NSError *errorToCheck = nil; 58 | NSArray *malformedURLs = @[ 59 | @"mywordpresssite.com\test", 60 | @"mywordpres ssite.com/test", 61 | @"http:\\mywordpresssite.com/test" 62 | ]; 63 | for (NSString *malformedURL in malformedURLs) { 64 | XCTestExpectation *expectationMalFormedURL = [self expectationWithDescription:@"Call should fail with error when invoking with malformed urls"]; 65 | [WordPressXMLRPCApi guessXMLRPCURLForSite:malformedURL success:^(NSURL *xmlrpcURL) { 66 | } failure:^(NSError *error) { 67 | [expectationMalFormedURL fulfill]; 68 | errorToCheck = error; 69 | }]; 70 | [self waitForExpectationsWithTimeout:2 handler:nil]; 71 | XCTAssertTrue(errorToCheck.domain == WordPressXMLRPCApiErrorDomain, @"Expected to get an WordPressXMLRPCApiErrorDomain error"); 72 | XCTAssertTrue(errorToCheck.code == WordPressXMLRPCApiInvalidURL, @"Expected to get an WordPressXMLRPCApiInvalidURL error"); 73 | } 74 | } 75 | 76 | - (void)testGuessXMLRPCURLForSiteForInvalidSchemes { 77 | __block NSError *errorToCheck = nil; 78 | NSArray *incorrectSchemes = @[ 79 | @"hppt://mywordpresssite.com/test", 80 | @"ftp://mywordpresssite.com/test", 81 | @"git://mywordpresssite.com/test" 82 | ]; 83 | for (NSString *incorrectScheme in incorrectSchemes) { 84 | XCTestExpectation *expectation = [self expectationWithDescription:@"Call should fail with error when invoking with urls with incorrect schemes"]; 85 | [WordPressXMLRPCApi guessXMLRPCURLForSite:incorrectScheme success:^(NSURL *xmlrpcURL) { 86 | } failure:^(NSError *error) { 87 | [expectation fulfill]; 88 | errorToCheck = error; 89 | }]; 90 | [self waitForExpectationsWithTimeout:2 handler:nil]; 91 | XCTAssertTrue(errorToCheck.domain == WordPressXMLRPCApiErrorDomain, @"Expected to get an WordPressXMLRPCApiErrorDomain error"); 92 | XCTAssertTrue(errorToCheck.code == WordPressXMLRPCApiInvalidScheme, @"Expected to get an WordPressXMLRPCApiInvalidScheme error"); 93 | } 94 | } 95 | 96 | - (void)testGuessXMLRPCURLForSiteForCorrectSchemes { 97 | 98 | [OHHTTPStubs stubRequestsPassingTest:^BOOL(NSURLRequest *request) { 99 | return [request.URL.host isEqualToString:@"mywordpresssite.com"]; 100 | } withStubResponse:^OHHTTPStubsResponse*(NSURLRequest *request) { 101 | NSURL *mockDataURL = [[NSBundle bundleForClass:[self class]] URLForResource:@"system_list_methods" withExtension:@"xml"]; 102 | NSData *mockData = [NSData dataWithContentsOfURL:mockDataURL]; 103 | return [[OHHTTPStubsResponse responseWithData:mockData statusCode:200 headers:nil] 104 | responseTime:OHHTTPStubsDownloadSpeedWifi]; 105 | }]; 106 | 107 | NSArray *validSchemes = @[ 108 | @"http://mywordpresssite.com/xmlrpc.php", 109 | @"https://mywordpresssite.com/xmlrpc.php", 110 | @"mywordpresssite.com/xmlrpc.php", 111 | ]; 112 | for (NSString *validScheme in validSchemes) { 113 | XCTestExpectation *expectation = [self expectationWithDescription:@"Call should be successful"]; 114 | [WordPressXMLRPCApi guessXMLRPCURLForSite:validScheme success:^(NSURL *xmlrpcURL) { 115 | [expectation fulfill]; 116 | XCTAssertTrue([xmlrpcURL.host isEqualToString:@"mywordpresssite.com"], @"Check if we are getting the corrent site in the answer"); 117 | XCTAssertTrue([xmlrpcURL.path isEqualToString:@"/xmlrpc.php"], @"Check if we are getting the corrent path in the answer"); 118 | } failure:^(NSError *error) { 119 | XCTFail(@"Call to valid site should not enter failure block."); 120 | }]; 121 | [self waitForExpectationsWithTimeout:2 handler:nil]; 122 | } 123 | 124 | } 125 | 126 | - (void)testGuessXMLRPCURLForSiteForAdditionOfXMLRPC { 127 | 128 | [OHHTTPStubs stubRequestsPassingTest:^BOOL(NSURLRequest *request) { 129 | return [request.URL.host isEqualToString:@"mywordpresssite.com"]; 130 | } withStubResponse:^OHHTTPStubsResponse*(NSURLRequest *request) { 131 | NSURL *mockDataURL = [[NSBundle bundleForClass:[self class]] URLForResource:@"system_list_methods" withExtension:@"xml"]; 132 | NSData *mockData = [NSData dataWithContentsOfURL:mockDataURL]; 133 | return [[OHHTTPStubsResponse responseWithData:mockData statusCode:200 headers:nil] 134 | responseTime:OHHTTPStubsDownloadSpeedWifi]; 135 | }]; 136 | 137 | NSArray *URLs = @[ 138 | @"http://mywordpresssite.com", 139 | @"https://mywordpresssite.com", 140 | @"mywordpresssite.com", 141 | @"mywordpresssite.com/blog1", 142 | @"mywordpresssite.com/xmlrpc.php", 143 | @"mywordpresssite.com/xmlrpc.php?test=test" 144 | ]; 145 | for (NSString *url in URLs) { 146 | XCTestExpectation *expectation = [self expectationWithDescription:@"Call should be successful"]; 147 | [WordPressXMLRPCApi guessXMLRPCURLForSite:url success:^(NSURL *xmlrpcURL) { 148 | [expectation fulfill]; 149 | XCTAssertTrue([xmlrpcURL.host isEqualToString:@"mywordpresssite.com"], @"Resolved host doens't match original url: %@", url); 150 | XCTAssertTrue([[xmlrpcURL lastPathComponent] isEqualToString:@"xmlrpc.php"], @"Resolved last path component doens't match original url: %@", url); 151 | XCTAssertTrue([xmlrpcURL query] == nil || [[xmlrpcURL query] isEqualToString:@"test=test"], @"Resolved query components doens't match original url: %@", url); 152 | } failure:^(NSError *error) { 153 | XCTFail(@"Call to valid site should not enter failure block."); 154 | }]; 155 | [self waitForExpectationsWithTimeout:2 handler:nil]; 156 | } 157 | 158 | } 159 | 160 | - (void)testGuessXMLRPCURLForSiteForFallbackToOriginalURL { 161 | NSString *originalURL = @"http://mywordpresssite.com/rpc"; 162 | NSString *appendedURL = [originalURL stringByAppendingString:@"/xmlrpc.php"]; 163 | 164 | [OHHTTPStubs stubRequestsPassingTest:^BOOL(NSURLRequest *request) { 165 | return [request.URL.absoluteString isEqualToString:originalURL]; 166 | } withStubResponse:^OHHTTPStubsResponse*(NSURLRequest *request) { 167 | NSURL *mockDataURL = [[NSBundle bundleForClass:[self class]] URLForResource:@"system_list_methods" withExtension:@"xml"]; 168 | NSData *mockData = [NSData dataWithContentsOfURL:mockDataURL]; 169 | return [[OHHTTPStubsResponse responseWithData:mockData statusCode:200 headers:nil] 170 | responseTime:OHHTTPStubsDownloadSpeedWifi]; 171 | }]; 172 | 173 | [OHHTTPStubs stubRequestsPassingTest:^BOOL(NSURLRequest *request) { 174 | return [[request.URL absoluteString] isEqualToString:appendedURL]; 175 | } withStubResponse:^OHHTTPStubsResponse*(NSURLRequest *request) { 176 | return [[OHHTTPStubsResponse responseWithData:[NSData data] statusCode:403 headers:nil] 177 | responseTime:OHHTTPStubsDownloadSpeedWifi]; 178 | }]; 179 | 180 | 181 | XCTestExpectation *expectation = [self expectationWithDescription:@"Call should be successful"]; 182 | [WordPressXMLRPCApi guessXMLRPCURLForSite:originalURL success:^(NSURL *xmlrpcURL) { 183 | [expectation fulfill]; 184 | XCTAssertTrue([xmlrpcURL.absoluteString isEqualToString:originalURL], @"Resolved url doens't match original url: %@", originalURL); 185 | } failure:^(NSError *error) { 186 | XCTFail(@"Call to valid site should not enter failure block."); 187 | }]; 188 | [self waitForExpectationsWithTimeout:5 handler:nil]; 189 | } 190 | 191 | - (void)testGuessXMLRPCURLForSiteForFallbackToStandardRSD { 192 | NSString *baseURL = @"http://mywordpresssite.com"; 193 | NSString *htmlURL = [baseURL stringByAppendingString:@"wp-login"]; 194 | NSString *appendedURL = [htmlURL stringByAppendingString:@"/xmlrpc.php"]; 195 | NSString *xmlRPCURL = [baseURL stringByAppendingString:@"/xmlrpc.php"]; 196 | NSString *rsdURL = [xmlRPCURL stringByAppendingString:@"?rsd"]; 197 | 198 | // Fail first request with 403 199 | [OHHTTPStubs stubRequestsPassingTest:^BOOL(NSURLRequest *request) { 200 | return [[request.URL absoluteString] isEqualToString:appendedURL]; 201 | } withStubResponse:^OHHTTPStubsResponse*(NSURLRequest *request) { 202 | return [[OHHTTPStubsResponse responseWithData:[NSData data] statusCode:403 headers:nil] 203 | responseTime:OHHTTPStubsDownloadSpeedWifi]; 204 | }]; 205 | 206 | // Return html page for original url 207 | [OHHTTPStubs stubRequestsPassingTest:^BOOL(NSURLRequest *request) { 208 | return [request.URL.absoluteString isEqualToString:htmlURL]; 209 | } withStubResponse:^OHHTTPStubsResponse*(NSURLRequest *request) { 210 | NSURL *mockDataURL = [[NSBundle bundleForClass:[self class]] URLForResource:@"html_page_with_link_to_rsd" withExtension:@"html"]; 211 | NSData *mockData = [NSData dataWithContentsOfURL:mockDataURL]; 212 | return [[OHHTTPStubsResponse responseWithData:mockData statusCode:200 headers:nil] 213 | responseTime:OHHTTPStubsDownloadSpeedWifi]; 214 | }]; 215 | 216 | // Return rsd xml 217 | [OHHTTPStubs stubRequestsPassingTest:^BOOL(NSURLRequest *request) { 218 | return [request.URL.absoluteString isEqualToString:rsdURL]; 219 | } withStubResponse:^OHHTTPStubsResponse*(NSURLRequest *request) { 220 | NSURL *mockDataURL = [[NSBundle bundleForClass:[self class]] URLForResource:@"rsd" withExtension:@"xml"]; 221 | NSData *mockData = [NSData dataWithContentsOfURL:mockDataURL]; 222 | return [[OHHTTPStubsResponse responseWithData:mockData statusCode:200 headers:nil] 223 | responseTime:OHHTTPStubsDownloadSpeedWifi]; 224 | }]; 225 | 226 | [OHHTTPStubs stubRequestsPassingTest:^BOOL(NSURLRequest *request) { 227 | return [request.URL.absoluteString isEqualToString:xmlRPCURL]; 228 | } withStubResponse:^OHHTTPStubsResponse*(NSURLRequest *request) { 229 | NSURL *mockDataURL = [[NSBundle bundleForClass:[self class]] URLForResource:@"system_list_methods" withExtension:@"xml"]; 230 | NSData *mockData = [NSData dataWithContentsOfURL:mockDataURL]; 231 | return [[OHHTTPStubsResponse responseWithData:mockData statusCode:200 headers:nil] 232 | responseTime:OHHTTPStubsDownloadSpeedWifi]; 233 | }]; 234 | 235 | XCTestExpectation *expectation = [self expectationWithDescription:@"Call should be successful"]; 236 | [WordPressXMLRPCApi guessXMLRPCURLForSite:htmlURL success:^(NSURL *xmlrpcURL) { 237 | [expectation fulfill]; 238 | XCTAssertTrue([xmlrpcURL.absoluteString isEqualToString:xmlRPCURL], @"Resolved url doens't match original url: %@", xmlRPCURL); 239 | } failure:^(NSError *error) { 240 | XCTFail(@"Call to valid site should not enter failure block."); 241 | }]; 242 | [self waitForExpectationsWithTimeout:5 handler:nil]; 243 | } 244 | 245 | - (void)testGuessXMLRPCURLForSiteForFallbackToNonStandardRSD { 246 | NSString *baseURL = @"http://mywordpresssite.com"; 247 | NSString *htmlURL = [baseURL stringByAppendingString:@"wp-login"]; 248 | NSString *appendedURL = [htmlURL stringByAppendingString:@"/xmlrpc.php"]; 249 | NSString *xmlRPCURL = [baseURL stringByAppendingString:@"/xmlrpc.php"]; 250 | NSString *rsdURL = [baseURL stringByAppendingString:@"/rsd.php"]; 251 | 252 | // Fail first request with 403 253 | [OHHTTPStubs stubRequestsPassingTest:^BOOL(NSURLRequest *request) { 254 | return [[request.URL absoluteString] isEqualToString:appendedURL]; 255 | } withStubResponse:^OHHTTPStubsResponse*(NSURLRequest *request) { 256 | return [[OHHTTPStubsResponse responseWithData:[NSData data] statusCode:403 headers:nil] 257 | responseTime:OHHTTPStubsDownloadSpeedWifi]; 258 | }]; 259 | 260 | // Return html page for original url 261 | [OHHTTPStubs stubRequestsPassingTest:^BOOL(NSURLRequest *request) { 262 | return [request.URL.absoluteString isEqualToString:htmlURL]; 263 | } withStubResponse:^OHHTTPStubsResponse*(NSURLRequest *request) { 264 | NSURL *mockDataURL = [[NSBundle bundleForClass:[self class]] URLForResource:@"html_page_with_link_to_rsd_non_standard" withExtension:@"html"]; 265 | NSData *mockData = [NSData dataWithContentsOfURL:mockDataURL]; 266 | return [[OHHTTPStubsResponse responseWithData:mockData statusCode:200 headers:nil] 267 | responseTime:OHHTTPStubsDownloadSpeedWifi]; 268 | }]; 269 | 270 | // Return rsd xml 271 | [OHHTTPStubs stubRequestsPassingTest:^BOOL(NSURLRequest *request) { 272 | return [request.URL.absoluteString isEqualToString:rsdURL]; 273 | } withStubResponse:^OHHTTPStubsResponse*(NSURLRequest *request) { 274 | NSURL *mockDataURL = [[NSBundle bundleForClass:[self class]] URLForResource:@"rsd" withExtension:@"xml"]; 275 | NSData *mockData = [NSData dataWithContentsOfURL:mockDataURL]; 276 | return [[OHHTTPStubsResponse responseWithData:mockData statusCode:200 headers:nil] 277 | responseTime:OHHTTPStubsDownloadSpeedWifi]; 278 | }]; 279 | 280 | [OHHTTPStubs stubRequestsPassingTest:^BOOL(NSURLRequest *request) { 281 | return [request.URL.absoluteString isEqualToString:xmlRPCURL]; 282 | } withStubResponse:^OHHTTPStubsResponse*(NSURLRequest *request) { 283 | NSURL *mockDataURL = [[NSBundle bundleForClass:[self class]] URLForResource:@"system_list_methods" withExtension:@"xml"]; 284 | NSData *mockData = [NSData dataWithContentsOfURL:mockDataURL]; 285 | return [[OHHTTPStubsResponse responseWithData:mockData statusCode:200 headers:nil] 286 | responseTime:OHHTTPStubsDownloadSpeedWifi]; 287 | }]; 288 | 289 | XCTestExpectation *expectation = [self expectationWithDescription:@"Call should be successful"]; 290 | [WordPressXMLRPCApi guessXMLRPCURLForSite:htmlURL success:^(NSURL *xmlrpcURL) { 291 | [expectation fulfill]; 292 | XCTAssertTrue([xmlrpcURL.absoluteString isEqualToString:xmlRPCURL], @"Resolved url doens't match original url: %@", xmlRPCURL); 293 | } failure:^(NSError *error) { 294 | XCTFail(@"Call to valid site should not enter failure block."); 295 | }]; 296 | [self waitForExpectationsWithTimeout:5 handler:nil]; 297 | } 298 | 299 | - (void)testGuessXMLRPCURLForSiteForSucessfulRedirects { 300 | NSString *originalURL = @"http://mywordpresssite.com/xmlrpc.php"; 301 | NSString *redirectedURL = @"https://mywordpresssite.com/xmlrpc.php"; 302 | 303 | // Fail first request with 301 304 | [OHHTTPStubs stubRequestsPassingTest:^BOOL(NSURLRequest *request) { 305 | return [[request.URL absoluteString] isEqualToString:originalURL]; 306 | } withStubResponse:^OHHTTPStubsResponse*(NSURLRequest *request) { 307 | NSBundle *bundle = [NSBundle bundleForClass:[self class]]; 308 | NSURL *mockDataURL = [bundle URLForResource:@"redirect" withExtension:@"html"]; 309 | NSData *mockData = [NSData dataWithContentsOfURL:mockDataURL]; 310 | return [[OHHTTPStubsResponse responseWithData:mockData statusCode:301 headers:@{@"Location": redirectedURL}] 311 | responseTime:OHHTTPStubsDownloadSpeedWifi]; 312 | }]; 313 | 314 | [OHHTTPStubs stubRequestsPassingTest:^BOOL(NSURLRequest *request) { 315 | return [request.URL.absoluteString isEqualToString:redirectedURL]; 316 | } withStubResponse:^OHHTTPStubsResponse*(NSURLRequest *request) { 317 | NSURL *mockDataURL = [[NSBundle bundleForClass:[self class]] URLForResource:@"system_list_methods" withExtension:@"xml"]; 318 | NSData *mockData = [NSData dataWithContentsOfURL:mockDataURL]; 319 | return [[OHHTTPStubsResponse responseWithData:mockData statusCode:200 headers:nil] 320 | responseTime:OHHTTPStubsDownloadSpeedWifi]; 321 | }]; 322 | 323 | XCTestExpectation *expectation = [self expectationWithDescription:@"Call should be successful"]; 324 | [WordPressXMLRPCApi guessXMLRPCURLForSite:originalURL success:^(NSURL *xmlrpcURL) { 325 | [expectation fulfill]; 326 | XCTAssertTrue([xmlrpcURL.absoluteString isEqualToString:redirectedURL], @"Resolved url doens't match original url: %@", redirectedURL); 327 | } failure:^(NSError *error) { 328 | XCTFail(@"Call to valid site should not enter failure block."); 329 | }]; 330 | [self waitForExpectationsWithTimeout:5 handler:nil]; 331 | } 332 | 333 | - (void)testGuessXMLRPCURLForSiteForFailedPluginRedirects { 334 | NSString *originalURL = @"http://mywordpresssite.com/xmlrpc.php"; 335 | NSString *redirectedURL = @"https://mywordpresssite.com/xmlrpc.php"; 336 | 337 | // Fail first request with 301 338 | [OHHTTPStubs stubRequestsPassingTest:^BOOL(NSURLRequest *request) { 339 | return [[request.URL absoluteString] isEqualToString:originalURL]; 340 | } withStubResponse:^OHHTTPStubsResponse*(NSURLRequest *request) { 341 | NSBundle *bundle = [NSBundle bundleForClass:[self class]]; 342 | NSURL *mockDataURL = [bundle URLForResource:@"redirect" withExtension:@"html"]; 343 | NSData *mockData = [NSData dataWithContentsOfURL:mockDataURL]; 344 | return [[OHHTTPStubsResponse responseWithData:mockData statusCode:301 headers:@{@"Location": redirectedURL}] 345 | responseTime:OHHTTPStubsDownloadSpeedWifi]; 346 | }]; 347 | 348 | [OHHTTPStubs stubRequestsPassingTest:^BOOL(NSURLRequest *request) { 349 | return [request.URL.absoluteString isEqualToString:redirectedURL]; 350 | } withStubResponse:^OHHTTPStubsResponse*(NSURLRequest *request) { 351 | NSURL *mockDataURL = [[NSBundle bundleForClass:[self class]] URLForResource:@"plugin_redirect" withExtension:@"html"]; 352 | NSData *mockData = [NSData dataWithContentsOfURL:mockDataURL]; 353 | return [[OHHTTPStubsResponse responseWithData:mockData statusCode:200 headers:nil] 354 | responseTime:OHHTTPStubsDownloadSpeedWifi]; 355 | }]; 356 | 357 | XCTestExpectation *expectation = [self expectationWithDescription:@"Call should fail"]; 358 | [WordPressXMLRPCApi guessXMLRPCURLForSite:originalURL success:^(NSURL *xmlrpcURL) { 359 | XCTFail(@"Call to valid site should not enter success block."); 360 | } failure:^(NSError *error) { 361 | [expectation fulfill]; 362 | XCTAssertTrue(error.domain == WordPressXMLRPCApiErrorDomain, @"Expected to get an WordPressXMLRPCApiErrorDomain error"); 363 | XCTAssertTrue(error.code == WordPressXMLRPCApiMobilePluginRedirectedError, @"Expected to get an WordPressXMLRPCApiMobilePluginRedirectedError error"); 364 | }]; 365 | [self waitForExpectationsWithTimeout:5 handler:nil]; 366 | } 367 | 368 | - (void)testGuessXMLRPCURLForSiteForFaultAnswers { 369 | NSString *originalURL = @"http://mywordpresssite.com/xmlrpc.php"; 370 | [OHHTTPStubs stubRequestsPassingTest:^BOOL(NSURLRequest *request) { 371 | return [request.URL.absoluteString isEqualToString:originalURL]; 372 | } withStubResponse:^OHHTTPStubsResponse*(NSURLRequest *request) { 373 | NSURL *mockDataURL = [[NSBundle bundleForClass:[self class]] URLForResource:@"fault_rpc_call" withExtension:@"xml"]; 374 | NSData *mockData = [NSData dataWithContentsOfURL:mockDataURL]; 375 | return [[OHHTTPStubsResponse responseWithData:mockData statusCode:200 headers:nil] 376 | responseTime:OHHTTPStubsDownloadSpeedWifi]; 377 | }]; 378 | XCTestExpectation *expectation = [self expectationWithDescription:@"Call should be successful"]; 379 | [WordPressXMLRPCApi guessXMLRPCURLForSite:originalURL success:^(NSURL *xmlrpcURL) { 380 | [expectation fulfill]; 381 | XCTFail(@"Call to valid site should not enter success block."); 382 | } failure:^(NSError *error) { 383 | [expectation fulfill]; 384 | XCTAssertTrue(error != nil, @"Check if we are getting an error message"); 385 | }]; 386 | [self waitForExpectationsWithTimeout:2 handler:nil]; 387 | } 388 | 389 | @end 390 | -------------------------------------------------------------------------------- /Example/WordPressApiExample.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Example/WordPressApiExample.xcodeproj/project.xcworkspace/xcshareddata/WordPressApiExample.xccheckout: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDESourceControlProjectFavoriteDictionaryKey 6 | 7 | IDESourceControlProjectIdentifier 8 | 491E1032-4298-4BEC-985B-04496BC583A7 9 | IDESourceControlProjectName 10 | WordPressApiExample 11 | IDESourceControlProjectOriginsDictionary 12 | 13 | 13C3A14B-A5C0-4AC2-BB0D-EA5CB3201A07 14 | ssh://github.com/diegoreymendez/WordPressApi.git 15 | 16 | IDESourceControlProjectPath 17 | WordPressApiExample/WordPressApiExample.xcodeproj/project.xcworkspace 18 | IDESourceControlProjectRelativeInstallPathDictionary 19 | 20 | 13C3A14B-A5C0-4AC2-BB0D-EA5CB3201A07 21 | ../../.. 22 | 23 | IDESourceControlProjectURL 24 | ssh://github.com/diegoreymendez/WordPressApi.git 25 | IDESourceControlProjectVersion 26 | 110 27 | IDESourceControlProjectWCCIdentifier 28 | 13C3A14B-A5C0-4AC2-BB0D-EA5CB3201A07 29 | IDESourceControlProjectWCConfigurations 30 | 31 | 32 | IDESourceControlRepositoryExtensionIdentifierKey 33 | public.vcs.git 34 | IDESourceControlWCCIdentifierKey 35 | 13C3A14B-A5C0-4AC2-BB0D-EA5CB3201A07 36 | IDESourceControlWCCName 37 | WordPressApi 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /Example/WordPressApiExample.xcodeproj/xcshareddata/xcschemes/WordPressApiExample.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 35 | 36 | 37 | 38 | 39 | 45 | 46 | 48 | 54 | 55 | 56 | 57 | 58 | 64 | 65 | 66 | 67 | 68 | 69 | 79 | 81 | 87 | 88 | 89 | 90 | 91 | 92 | 98 | 100 | 106 | 107 | 108 | 109 | 111 | 112 | 115 | 116 | 117 | -------------------------------------------------------------------------------- /Example/WordPressApiExample.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Example/WordPressApiExample/AppDelegate.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | @interface AppDelegate : UIResponder 4 | 5 | @property (strong, nonatomic) UIWindow *window; 6 | 7 | @end 8 | -------------------------------------------------------------------------------- /Example/WordPressApiExample/AppDelegate.m: -------------------------------------------------------------------------------- 1 | #import "AppDelegate.h" 2 | #import "WordPressApi.h" 3 | 4 | @implementation AppDelegate 5 | 6 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions 7 | { 8 | // Override point for customization after application launch. 9 | [WordPressApi setWordPressComClient:OAUTH_CLIENT_ID]; 10 | [WordPressApi setWordPressComSecret:OAUTH_SECRET]; 11 | [WordPressApi setWordPressComRedirectUrl:OAUTH_REDIRECT_URI]; 12 | return YES; 13 | } 14 | 15 | // Pre 4.2 support 16 | - (BOOL)application:(UIApplication *)application handleOpenURL:(NSURL *)url { 17 | return [WordPressApi handleOpenURL:url]; 18 | } 19 | 20 | // For 4.2+ support 21 | - (BOOL)application:(UIApplication *)application openURL:(NSURL *)url 22 | sourceApplication:(NSString *)sourceApplication annotation:(id)annotation { 23 | return [self application:application handleOpenURL:url]; 24 | } 25 | 26 | - (void)applicationWillResignActive:(UIApplication *)application 27 | { 28 | /* 29 | Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 30 | Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game. 31 | */ 32 | } 33 | 34 | - (void)applicationDidEnterBackground:(UIApplication *)application 35 | { 36 | /* 37 | Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 38 | If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 39 | */ 40 | } 41 | 42 | - (void)applicationWillEnterForeground:(UIApplication *)application 43 | { 44 | /* 45 | Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background. 46 | */ 47 | } 48 | 49 | - (void)applicationDidBecomeActive:(UIApplication *)application 50 | { 51 | /* 52 | Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 53 | */ 54 | } 55 | 56 | - (void)applicationWillTerminate:(UIApplication *)application 57 | { 58 | /* 59 | Called when the application is about to terminate. 60 | Save data if appropriate. 61 | See also applicationDidEnterBackground:. 62 | */ 63 | } 64 | 65 | @end 66 | -------------------------------------------------------------------------------- /Example/WordPressApiExample/ComposeViewController.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | @interface ComposeViewController : UIViewController 4 | @property (nonatomic, retain) IBOutlet UITextField *titleField; 5 | @property (nonatomic, retain) IBOutlet UITextView *content; 6 | @property (nonatomic, retain) IBOutlet UIImageView *imageView; 7 | @property (nonatomic, retain) UIImage *image; 8 | - (IBAction)save:(id)sender; 9 | - (IBAction)cancel:(id)sender; 10 | - (IBAction)addPicture:(id)sender; 11 | @end 12 | -------------------------------------------------------------------------------- /Example/WordPressApiExample/ComposeViewController.m: -------------------------------------------------------------------------------- 1 | #import "ComposeViewController.h" 2 | #import "PostsViewController.h" 3 | 4 | @interface ComposeViewController () 5 | 6 | @end 7 | 8 | @implementation ComposeViewController 9 | @synthesize titleField, content; 10 | 11 | - (IBAction)cancel:(id)sender { 12 | [self dismissViewControllerAnimated:YES completion:nil]; 13 | } 14 | 15 | - (IBAction)save:(id)sender { 16 | PostsViewController *postsVC = (PostsViewController *)[(UINavigationController *)[self presentingViewController] topViewController]; 17 | [postsVC publishPostWithTitle:titleField.text content:content.text image:self.image]; 18 | [self dismissViewControllerAnimated:YES completion:nil]; 19 | } 20 | 21 | - (IBAction)addPicture:(id)sender { 22 | UIImagePickerController *picker = [[UIImagePickerController alloc] init]; 23 | picker.delegate = self; 24 | [self presentViewController:picker animated:YES completion:nil]; 25 | } 26 | 27 | - (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary *)info { 28 | self.image = info[UIImagePickerControllerOriginalImage]; 29 | self.imageView.image = self.image; 30 | [self dismissViewControllerAnimated:YES completion:nil]; 31 | } 32 | 33 | - (void)imagePickerControllerDidCancel:(UIImagePickerController *)picker { 34 | [self dismissViewControllerAnimated:YES completion:nil]; 35 | } 36 | 37 | @end 38 | -------------------------------------------------------------------------------- /Example/WordPressApiExample/Images.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "29x29", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "29x29", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "40x40", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "40x40", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "60x60", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "60x60", 31 | "scale" : "3x" 32 | } 33 | ], 34 | "info" : { 35 | "version" : 1, 36 | "author" : "xcode" 37 | } 38 | } -------------------------------------------------------------------------------- /Example/WordPressApiExample/Images.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /Example/WordPressApiExample/LoginViewController.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | @interface LoginViewController : UITableViewController 4 | @property (nonatomic, retain) IBOutlet UITextField *urlField; 5 | @property (nonatomic, retain) IBOutlet UITextField *usernameField; 6 | @property (nonatomic, retain) IBOutlet UITextField *passwordField; 7 | @end 8 | -------------------------------------------------------------------------------- /Example/WordPressApiExample/LoginViewController.m: -------------------------------------------------------------------------------- 1 | #import "LoginViewController.h" 2 | #import "WordPressApi.h" 3 | 4 | @implementation LoginViewController 5 | 6 | #pragma mark - Table view delegate 7 | 8 | - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath 9 | { 10 | if (indexPath.section == 1) { 11 | // Sign in 12 | [WordPressApi signInWithURL:self.urlField.text 13 | username:self.usernameField.text 14 | password:self.passwordField.text 15 | success:^(NSURL *xmlrpcURL) { 16 | NSUserDefaults *def = [NSUserDefaults standardUserDefaults]; 17 | [def setObject:[xmlrpcURL absoluteString] forKey:@"wp_xmlrpc"]; 18 | [def setObject:self.usernameField.text forKey:@"wp_username"]; 19 | [def setObject:self.passwordField.text forKey:@"wp_password"]; 20 | [def synchronize]; 21 | [self dismissViewControllerAnimated:YES completion:nil]; 22 | } failure:^(NSError *error) { 23 | UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Login error" message:[error localizedDescription] delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil]; 24 | [alert show]; 25 | }]; 26 | } else if (indexPath.section == 2) { 27 | // Sign in with WordPress.com 28 | [WordPressApi signInWithOauthWithSuccess:^(NSString *authToken, NSString *siteId) { 29 | NSUserDefaults *def = [NSUserDefaults standardUserDefaults]; 30 | [def setObject:siteId forKey:@"wp_site_id"]; 31 | [def setObject:authToken forKey:@"wp_token"]; 32 | [def synchronize]; 33 | [self dismissViewControllerAnimated:YES completion:nil]; 34 | } failure:^(NSError *error) { 35 | UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Login error" message:[error localizedDescription] delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil]; 36 | [alert show]; 37 | }]; 38 | } 39 | [tableView deselectRowAtIndexPath:indexPath animated:YES]; 40 | } 41 | 42 | #pragma mark - UITextFieldDelegate 43 | 44 | - (BOOL)textFieldShouldReturn:(UITextField *)textField { 45 | [textField resignFirstResponder]; 46 | return NO; 47 | } 48 | 49 | @end 50 | -------------------------------------------------------------------------------- /Example/WordPressApiExample/PostViewController.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | @interface PostViewController : UIViewController 4 | 5 | @property (strong, nonatomic) NSDictionary *post; 6 | 7 | @property (strong, nonatomic) IBOutlet UIWebView *postContentView; 8 | 9 | @end 10 | -------------------------------------------------------------------------------- /Example/WordPressApiExample/PostViewController.m: -------------------------------------------------------------------------------- 1 | #import "PostViewController.h" 2 | 3 | @interface PostViewController () 4 | 5 | - (void)configureView; 6 | 7 | @end 8 | 9 | @implementation PostViewController 10 | 11 | #pragma mark - Managing the detail item 12 | 13 | - (void)setPost:(NSDictionary *)newPost 14 | { 15 | if (_post != newPost) { 16 | _post = newPost; 17 | 18 | // Update the view. 19 | [self configureView]; 20 | } 21 | } 22 | 23 | - (void)configureView 24 | { 25 | // Update the user interface for the detail item. 26 | 27 | if (self.post) { 28 | NSString *html = [NSString stringWithFormat:@"

%@

%@", 29 | [self.post objectForKey:@"title"], 30 | [self.post objectForKey:@"description"]]; 31 | self.title = [self.post objectForKey:@"title"]; 32 | [self.postContentView loadHTMLString:html baseURL:nil]; 33 | } 34 | } 35 | 36 | - (void)didReceiveMemoryWarning 37 | { 38 | [super didReceiveMemoryWarning]; 39 | // Release any cached data, images, etc that aren't in use. 40 | } 41 | 42 | #pragma mark - View lifecycle 43 | 44 | - (void)viewDidLoad 45 | { 46 | [super viewDidLoad]; 47 | // Do any additional setup after loading the view, typically from a nib. 48 | [self configureView]; 49 | } 50 | 51 | - (void)viewDidUnload 52 | { 53 | [super viewDidUnload]; 54 | // Release any retained subviews of the main view. 55 | // e.g. self.myOutlet = nil; 56 | } 57 | 58 | - (void)viewWillAppear:(BOOL)animated 59 | { 60 | [super viewWillAppear:animated]; 61 | } 62 | 63 | - (void)viewDidAppear:(BOOL)animated 64 | { 65 | [super viewDidAppear:animated]; 66 | } 67 | 68 | - (void)viewWillDisappear:(BOOL)animated 69 | { 70 | [super viewWillDisappear:animated]; 71 | } 72 | 73 | - (void)viewDidDisappear:(BOOL)animated 74 | { 75 | [super viewDidDisappear:animated]; 76 | } 77 | 78 | - (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation 79 | { 80 | // Return YES for supported orientations 81 | return (interfaceOrientation != UIInterfaceOrientationPortraitUpsideDown); 82 | } 83 | 84 | @end 85 | -------------------------------------------------------------------------------- /Example/WordPressApiExample/PostsViewController.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import "WordPressBaseApi.h" 3 | 4 | @interface PostsViewController : UITableViewController 5 | @property (readonly, nonatomic, retain) id api; 6 | 7 | - (IBAction)refreshPosts:(id)sender; 8 | - (void)publishPostWithTitle:(NSString *)title content:(NSString *)content image:(UIImage *)image; 9 | @end 10 | -------------------------------------------------------------------------------- /Example/WordPressApiExample/PostsViewController.m: -------------------------------------------------------------------------------- 1 | #import "PostsViewController.h" 2 | #import "PostViewController.h" 3 | #import "WordPressApi.h" 4 | 5 | @interface PostsViewController () 6 | 7 | @property (readwrite, nonatomic, retain) id api; 8 | @property (readwrite, nonatomic, retain) NSArray *posts; 9 | 10 | @end 11 | 12 | @implementation PostsViewController 13 | 14 | - (void)awakeFromNib 15 | { 16 | [self setupApi]; 17 | self.posts = [NSArray array]; 18 | [super awakeFromNib]; 19 | } 20 | 21 | - (void)didReceiveMemoryWarning 22 | { 23 | [super didReceiveMemoryWarning]; 24 | // Release any cached data, images, etc that aren't in use. 25 | } 26 | 27 | #pragma mark - View lifecycle 28 | 29 | - (void)viewDidLoad 30 | { 31 | [super viewDidLoad]; 32 | if (self.navigationItem.rightBarButtonItems && [self.navigationItem.rightBarButtonItems count] == 1) { 33 | UIBarButtonItem *logout = [[UIBarButtonItem alloc] initWithTitle:@"Logout" style:UIBarButtonItemStylePlain target:self action:@selector(logout:)]; 34 | self.navigationItem.rightBarButtonItems = [NSArray arrayWithObjects:self.navigationItem.rightBarButtonItem, logout, nil]; 35 | } 36 | // Do any additional setup after loading the view, typically from a nib. 37 | [self refreshPosts:nil]; 38 | [[NSUserDefaults standardUserDefaults] addObserver:self forKeyPath:@"wp_xmlrpc" options:0 context:nil]; 39 | } 40 | 41 | - (void)viewDidUnload 42 | { 43 | [super viewDidUnload]; 44 | // Release any retained subviews of the main view. 45 | // e.g. self.myOutlet = nil; 46 | } 47 | 48 | - (void)viewWillAppear:(BOOL)animated 49 | { 50 | [self setupApi]; 51 | [super viewWillAppear:animated]; 52 | } 53 | 54 | - (void)viewDidAppear:(BOOL)animated 55 | { 56 | [super viewDidAppear:animated]; 57 | if (self.api == nil) { 58 | [self.navigationController performSegueWithIdentifier:@"login" sender:self]; 59 | } 60 | } 61 | 62 | - (void)viewWillDisappear:(BOOL)animated 63 | { 64 | [super viewWillDisappear:animated]; 65 | } 66 | 67 | - (void)viewDidDisappear:(BOOL)animated 68 | { 69 | [super viewDidDisappear:animated]; 70 | } 71 | 72 | - (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation 73 | { 74 | // Return YES for supported orientations 75 | return (interfaceOrientation != UIInterfaceOrientationPortraitUpsideDown); 76 | } 77 | 78 | #pragma mark - Table data source 79 | 80 | - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { 81 | return 1; 82 | } 83 | 84 | - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { 85 | return [self.posts count]; 86 | } 87 | 88 | - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { 89 | UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"Cell"]; 90 | NSDictionary *post = [self.posts objectAtIndex:indexPath.row]; 91 | 92 | cell.textLabel.text = [post objectForKey:@"title"]; 93 | cell.detailTextLabel.text = [post objectForKey:@"description"]; 94 | return cell; 95 | } 96 | 97 | #pragma mark - Table delegate 98 | 99 | - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath { 100 | return 80.0f; 101 | } 102 | 103 | - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { 104 | [self performSegueWithIdentifier:@"showPost" sender:self]; 105 | } 106 | 107 | #pragma mark - Storyboards 108 | 109 | - (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender { 110 | if ([segue.identifier isEqualToString:@"showPost"]) { 111 | NSDictionary *post = [self.posts objectAtIndex:[self.tableView indexPathForSelectedRow].row]; 112 | PostViewController *postViewController = (PostViewController *)segue.destinationViewController; 113 | postViewController.post = post; 114 | } 115 | } 116 | 117 | #pragma mark - Custom methods 118 | 119 | - (IBAction)refreshPosts:(id)sender { 120 | [self.api getPosts:10 success:^(NSArray *posts) { 121 | self.posts = posts; 122 | NSLog(@"We have %lu posts", (unsigned long)[self.posts count]); 123 | [self.tableView reloadData]; 124 | } failure:^(NSError *error) { 125 | NSLog(@"Error fetching posts: %@", [error localizedDescription]); 126 | }]; 127 | } 128 | 129 | - (void)publishPostWithTitle:(NSString *)title content:(NSString *)content image:(UIImage *)image { 130 | [self.api publishPostWithImage:image description:content title:title success:^(NSUInteger postId, NSURL *permalink) { 131 | [self refreshPosts:self]; 132 | } failure:^(NSError *error) { 133 | UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Error posting" 134 | message:[error localizedDescription] 135 | delegate:nil 136 | cancelButtonTitle:@"OK" 137 | otherButtonTitles:nil]; 138 | [alert show]; 139 | }]; 140 | } 141 | 142 | #pragma mark - Private 143 | 144 | - (void)setupApi { 145 | if (self.api == nil) { 146 | NSUserDefaults *def = [NSUserDefaults standardUserDefaults]; 147 | NSString *token = [def objectForKey:@"wp_token"]; 148 | NSString *siteId = [def objectForKey:@"wp_site_id"]; 149 | if (token && siteId) { 150 | self.api = [WordPressApi apiWithOauthToken:token siteId:siteId]; 151 | } else { 152 | NSString *xmlrpc = [def objectForKey:@"wp_xmlrpc"]; 153 | if (xmlrpc) { 154 | NSString *username = [def objectForKey:@"wp_username"]; 155 | NSString *password = [def objectForKey:@"wp_password"]; 156 | if (username && password) { 157 | self.api = [WordPressApi apiWithXMLRPCURL:[NSURL URLWithString:xmlrpc] username:username password:password]; 158 | } 159 | } 160 | } 161 | } 162 | if (self.api) { 163 | [self refreshPosts:self]; 164 | } 165 | } 166 | 167 | - (IBAction)logout:(id)sender { 168 | self.api = nil; 169 | NSUserDefaults *def = [NSUserDefaults standardUserDefaults]; 170 | [def removeObjectForKey:@"wp_xmlrpc"]; 171 | [def synchronize]; 172 | [self.navigationController performSegueWithIdentifier:@"login" sender:self]; 173 | } 174 | 175 | - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { 176 | if ([keyPath isEqualToString:@"wp_xmlrpc"]) { 177 | [self setupApi]; 178 | } 179 | } 180 | 181 | @end 182 | -------------------------------------------------------------------------------- /Example/WordPressApiExample/WordPressApiExample-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleDisplayName 8 | ${PRODUCT_NAME} 9 | CFBundleExecutable 10 | ${EXECUTABLE_NAME} 11 | CFBundleIcons~ipad 12 | 13 | CFBundleIdentifier 14 | $(PRODUCT_BUNDLE_IDENTIFIER) 15 | CFBundleInfoDictionaryVersion 16 | 6.0 17 | CFBundleName 18 | ${PRODUCT_NAME} 19 | CFBundlePackageType 20 | APPL 21 | CFBundleShortVersionString 22 | 1.0 23 | CFBundleSignature 24 | ???? 25 | CFBundleURLTypes 26 | 27 | 28 | CFBundleTypeRole 29 | Editor 30 | CFBundleURLName 31 | org.wordpress.wordpress-api-example 32 | CFBundleURLSchemes 33 | 34 | wordpress-2174 35 | 36 | 37 | 38 | CFBundleVersion 39 | 1.0 40 | LSRequiresIPhoneOS 41 | 42 | NSAppTransportSecurity 43 | 44 | NSAllowsArbitraryLoads 45 | 46 | 47 | UILaunchStoryboardName 48 | LaunchScreen 49 | UIMainStoryboardFile 50 | MainStoryboard 51 | UIRequiredDeviceCapabilities 52 | 53 | armv7 54 | 55 | UISupportedInterfaceOrientations 56 | 57 | UIInterfaceOrientationPortrait 58 | UIInterfaceOrientationLandscapeLeft 59 | UIInterfaceOrientationLandscapeRight 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /Example/WordPressApiExample/WordPressApiExample-Prefix.pch: -------------------------------------------------------------------------------- 1 | // 2 | // Prefix header for all source files of the 'WordPressApiExample' target in the 'WordPressApiExample' project 3 | // 4 | 5 | #import 6 | 7 | #ifndef __IPHONE_8_0 8 | #warning "This project uses features only available in iOS SDK 8.0 and later." 9 | #endif 10 | 11 | #ifdef __OBJC__ 12 | #import 13 | #import 14 | #endif 15 | 16 | //#error Please fill in your OAuth details 17 | #define OAUTH_CLIENT_ID @"2174" 18 | #define OAUTH_REDIRECT_URI @"https://github.com/koke/WordPressApi" 19 | #define OAUTH_SECRET @"kPS9VGJ1kVmVnRwD5UBNkaSSCQb70zyAV2amIjgMvI6TGj2f7Zl4ch0hvAsVE6nX" 20 | -------------------------------------------------------------------------------- /Example/WordPressApiExample/en.lproj/InfoPlist.strings: -------------------------------------------------------------------------------- 1 | /* Localized versions of Info.plist keys */ 2 | 3 | -------------------------------------------------------------------------------- /Example/WordPressApiExample/en.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 27 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /Example/WordPressApiExample/main.m: -------------------------------------------------------------------------------- 1 | // 2 | // main.m 3 | // WordPressApiExample 4 | // 5 | // Created by Jorge Bernal on 12/20/11. 6 | // Copyright (c) 2011 Automattic. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | #import "AppDelegate.h" 12 | 13 | int main(int argc, char *argv[]) 14 | { 15 | @autoreleasepool { 16 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS -------------------------------------------------------------------------------- /Pod/WPComOAuthController.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | extern NSString *const WPComOAuthErrorDomain; 4 | typedef NS_ENUM(NSUInteger, WPComOAuthErrorCode) { 5 | WPComOAuthErrorCodeUnknown 6 | }; 7 | 8 | @interface WPComOAuthController : UIViewController 9 | 10 | + (WPComOAuthController *)sharedController; 11 | 12 | - (void)setWordPressComUsername:(NSString *)username; 13 | - (void)setWordPressComAuthToken:(NSString *)authToken; 14 | 15 | - (void)setClient:(NSString *)client; 16 | - (void)setRedirectUrl:(NSString *)redirectUrl; 17 | - (void)setSecret:(NSString *)secret; 18 | - (void)setCompletionBlock:(void (^)(NSString *token, NSString *blogId, NSString *blogUrl, NSString *scope, NSError *error))completionBlock; 19 | 20 | - (void)present; 21 | - (void)presentWithScope:(NSString *)scope blogId:(NSString *)blogId; 22 | - (void)getTokenWithCode:(NSString *)code secret:(NSString *)secret; 23 | - (BOOL)handleOpenURL:(NSURL *)URL; 24 | 25 | @end 26 | -------------------------------------------------------------------------------- /Pod/WPComOAuthController.m: -------------------------------------------------------------------------------- 1 | #import 2 | #import "WPComOAuthController.h" 3 | 4 | NSString *const WPComOAuthBaseUrl = @"https://public-api.wordpress.com/oauth2"; 5 | NSString *const WPComOAuthLoginUrl = @"https://wordpress.com/wp-login.php"; 6 | NSString *const WPComOAuthErrorDomain = @"WPComOAuthError"; 7 | 8 | @interface WPComOAuthController () 9 | @property IBOutlet UIWebView *webView; 10 | @end 11 | 12 | @implementation WPComOAuthController { 13 | NSString *_clientId; 14 | NSString *_redirectUrl; 15 | NSString *_scope; 16 | NSString *_blogId; 17 | NSString *_secret; 18 | NSString *_username; 19 | NSString *_password; 20 | NSString *_authToken; 21 | BOOL _isSSO; 22 | void (^_completionBlock)(NSString *token, NSString *blogId, NSString *blogUrl, NSString *scope, NSError *error); 23 | } 24 | 25 | + (WPComOAuthController *)sharedController { 26 | static WPComOAuthController *_sharedController = nil; 27 | static dispatch_once_t oncePredicate; 28 | dispatch_once(&oncePredicate, ^{ 29 | _sharedController = [[self alloc] init]; 30 | }); 31 | 32 | return _sharedController; 33 | } 34 | 35 | - (void)present { 36 | [self presentWithScope:nil blogId:nil]; 37 | } 38 | 39 | - (void)presentWithScope:(NSString *)scope blogId:(NSString *)blogId { 40 | NSAssert(_clientId != nil, @"WordPress.com OAuth can't be presented without the client id. Use setClient: before presenting"); 41 | NSAssert(_redirectUrl != nil, @"WordPress.com OAuth can't be presented without the redirect URL. Use setRedirectUrl: before presenting"); 42 | _scope = scope; 43 | _blogId = blogId; 44 | 45 | if (![[self class] isThisTheWordPressApp] && [[self class] isWordPressAppAvailable] && !_username && !_password) { 46 | NSString *url = [NSString stringWithFormat:@"%@://authorize?client_id=%@&redirect_uri=%@", [[self class] wordpressAppURLScheme], _clientId, _redirectUrl]; 47 | [[UIApplication sharedApplication] openURL:[NSURL URLWithString:url]]; 48 | } else { 49 | [self presentMe]; 50 | } 51 | } 52 | 53 | - (void)setWordPressComUsername:(NSString *)username { 54 | _username = username; 55 | } 56 | 57 | - (void)setWordPressComAuthToken:(NSString *)authToken { 58 | _authToken = authToken; 59 | } 60 | 61 | - (void)setClient:(NSString *)client { 62 | _clientId = client; 63 | } 64 | 65 | - (void)setRedirectUrl:(NSString *)redirectUrl { 66 | _redirectUrl = redirectUrl; 67 | } 68 | 69 | - (void)setSecret:(NSString *)secret { 70 | _secret = secret; 71 | } 72 | 73 | - (void)setCompletionBlock:(void (^)(NSString *token, NSString *blogId, NSString *blogUrl, NSString *scope, NSError *error))completionBlock { 74 | _completionBlock = [completionBlock copy]; 75 | } 76 | 77 | #pragma mark - View lifecycle 78 | 79 | - (NSString *)nibName { 80 | return nil; 81 | } 82 | 83 | - (void)loadView { 84 | UIView *view = [[UIView alloc] initWithFrame:CGRectZero]; 85 | view.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; 86 | UIWebView* webView = [[UIWebView alloc] initWithFrame:CGRectZero]; 87 | webView.delegate = self; 88 | webView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; 89 | [view addSubview:webView]; 90 | self.webView = webView; 91 | self.view = view; 92 | } 93 | 94 | - (void)viewDidLoad { 95 | [super viewDidLoad]; 96 | self.navigationItem.leftBarButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemCancel target:self action:@selector(cancel:)]; 97 | } 98 | 99 | - (void)viewWillAppear:(BOOL)animated { 100 | [super viewWillAppear:animated]; 101 | [self.webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"about:blank"]]]; 102 | } 103 | 104 | - (void)viewDidAppear:(BOOL)animated { 105 | [super viewDidAppear:animated]; 106 | 107 | NSString *queryUrl = [NSString stringWithFormat:@"%@/authorize?client_id=%@&redirect_uri=%@&response_type=code", WPComOAuthBaseUrl, _clientId, _redirectUrl]; 108 | if (_scope) { 109 | queryUrl = [queryUrl stringByAppendingFormat:@"&scope=%@", _scope]; 110 | } 111 | if (_blogId) { 112 | queryUrl = [queryUrl stringByAppendingFormat:@"&blog_id=%@", _blogId]; 113 | } 114 | NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:queryUrl]]; 115 | NSString *userAgent = [self userAgent]; 116 | if (userAgent) { 117 | [request setValue:userAgent forHTTPHeaderField:@"User-Agent"]; 118 | } 119 | if (_username && _authToken) { 120 | NSString *requestBody = [NSString stringWithFormat:@"%@=%@&%@=%@&%@=%@", 121 | @"log", [self stringByUrlEncodingString:_username], 122 | @"pwd", [NSString string], 123 | @"redirect_to", [self stringByUrlEncodingString:queryUrl]]; 124 | 125 | [request setURL:[NSURL URLWithString:WPComOAuthLoginUrl]]; 126 | [request setHTTPBody:[requestBody dataUsingEncoding:NSUTF8StringEncoding]]; 127 | [request setValue:[NSString stringWithFormat:@"%lu", (unsigned long)requestBody.length] forHTTPHeaderField:@"Content-Length"]; 128 | [request addValue:@"*/*" forHTTPHeaderField:@"Accept"]; 129 | [request addValue:[NSString stringWithFormat:@"Bearer %@", _authToken] forHTTPHeaderField:@"Authorization"]; 130 | [request setHTTPMethod:@"POST"]; 131 | } 132 | [self.webView loadRequest:request]; 133 | } 134 | 135 | - (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation 136 | { 137 | return [super shouldAutorotateToInterfaceOrientation:interfaceOrientation]; 138 | } 139 | 140 | #pragma mark - 141 | 142 | - (IBAction)cancel:(id)sender { 143 | [self dismissMe]; 144 | 145 | if (_isSSO) { 146 | [self openCallbackWithQueryString:@"error=canceled"]; 147 | } else { 148 | if (_completionBlock) { 149 | _completionBlock(nil, nil, nil, nil, nil); 150 | } 151 | } 152 | } 153 | 154 | - (void)presentMe { 155 | UINavigationController *navigation = [[UINavigationController alloc] initWithRootViewController:self]; 156 | UIWindow *window = [[UIApplication sharedApplication] keyWindow]; 157 | navigation.modalPresentationStyle = UIModalPresentationFormSheet; 158 | UIViewController *presenter = window.rootViewController; 159 | while (presenter.presentedViewController != nil) { 160 | presenter = presenter.presentedViewController; 161 | } 162 | [presenter presentViewController:navigation animated:YES completion:nil]; 163 | } 164 | 165 | - (void)dismissMe { 166 | [self.presentingViewController dismissViewControllerAnimated:YES completion:nil]; 167 | } 168 | 169 | - (NSString *)userAgent { 170 | return [[NSUserDefaults standardUserDefaults] objectForKey:@"UserAgent"]; 171 | } 172 | 173 | - (NSMutableDictionary *)dictionaryFromQueryString:(NSString *)string { 174 | if (!self) 175 | return nil; 176 | 177 | NSMutableDictionary *result = [NSMutableDictionary dictionary]; 178 | 179 | NSArray *pairs = [string componentsSeparatedByString:@"&"]; 180 | for (NSString *pair in pairs) { 181 | NSRange separator = [pair rangeOfString:@"="]; 182 | NSString *key, *value; 183 | if (separator.location != NSNotFound) { 184 | key = [pair substringToIndex:separator.location]; 185 | value = [[pair substringFromIndex:separator.location + 1] stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding]; 186 | } else { 187 | key = pair; 188 | value = @""; 189 | } 190 | 191 | key = [key stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding]; 192 | [result setObject:value forKey:key]; 193 | } 194 | 195 | return result; 196 | } 197 | 198 | - (NSString *)stringByUrlEncodingString:(NSString *)string { 199 | return (NSString *)CFBridgingRelease(CFURLCreateStringByAddingPercentEscapes(NULL, (CFStringRef)string, NULL, (CFStringRef)@"!*'();:@&=+$,/?%#[]", kCFStringEncodingUTF8)); 200 | } 201 | 202 | 203 | + (NSString *)wordpressAppURLScheme{ 204 | return @"wordpress-oauth-v2"; 205 | } 206 | 207 | + (BOOL)isWordPressAppAvailable { 208 | return [[UIApplication sharedApplication] canOpenURL:[NSURL URLWithString:[[self wordpressAppURLScheme] stringByAppendingString:@":"]]]; 209 | } 210 | 211 | + (BOOL)isThisTheWordPressApp { 212 | NSString *appIdentifier = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleIdentifier"]; 213 | return [appIdentifier isEqualToString:@"org.wordpress"]; 214 | } 215 | 216 | #pragma mark - 217 | 218 | - (id)initForSSO { 219 | self = [super init]; 220 | if (self) { 221 | _isSSO = YES; 222 | } 223 | return self; 224 | } 225 | 226 | - (void)getTokenWithCode:(NSString *)code secret:(NSString *)secret { 227 | NSAssert(secret != nil, @"WordPress.com OAuth can't be presented without the secret. Use setSecret: before presenting"); 228 | NSString *tokenUrl = [NSString stringWithFormat:@"%@/token", WPComOAuthBaseUrl]; 229 | __block NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:tokenUrl]]; 230 | NSString *userAgent = [self userAgent]; 231 | if (userAgent) { 232 | [request setValue:userAgent forHTTPHeaderField:@"User-Agent"]; 233 | } 234 | NSString *request_body = [NSString stringWithFormat:@"client_id=%@&redirect_uri=%@&client_secret=%@&code=%@&grant_type=authorization_code", 235 | _clientId, 236 | _redirectUrl, 237 | secret, 238 | code]; 239 | [request setHTTPMethod:@"POST"]; 240 | [request setHTTPBody:[request_body dataUsingEncoding:NSUTF8StringEncoding]]; 241 | AFHTTPRequestOperation* operation = [[AFHTTPRequestOperation alloc] initWithRequest:request]; 242 | operation.responseSerializer = [[AFJSONResponseSerializer alloc] init]; 243 | [operation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) { 244 | NSDictionary *response = (NSDictionary *)responseObject; 245 | NSString *token = [response objectForKey:@"access_token"]; 246 | NSString *blogUrl = [response objectForKey:@"blog_url"]; 247 | NSString *blogId = [response objectForKey:@"blog_url"]; 248 | NSString *scope = [response objectForKey:@"scope"]; 249 | if (token && blogUrl) { 250 | [self dismissMe]; 251 | 252 | if (_completionBlock) { 253 | _completionBlock(token, blogId, blogUrl, scope, nil); 254 | } 255 | } else if ([response objectForKey:@"error_description"]) { 256 | if (_completionBlock) { 257 | _completionBlock(nil, nil, nil, nil, [NSError errorWithDomain:WPComOAuthErrorDomain code:WPComOAuthErrorCodeUnknown userInfo:@{NSLocalizedDescriptionKey: [response objectForKey:@"error_description"]}]); 258 | } 259 | } else { 260 | if (_completionBlock) { 261 | _completionBlock(nil, nil, nil, nil, nil); 262 | } 263 | } 264 | } failure:^(AFHTTPRequestOperation *operation, NSError *error) { 265 | [self cancel:nil]; 266 | }]; 267 | [self.webView loadHTMLString:@"Access granted, getting permament access..." baseURL:[NSURL URLWithString:WPComOAuthBaseUrl]]; 268 | [operation start]; 269 | } 270 | 271 | - (BOOL)handleOpenURL:(NSURL *)URL { 272 | if ([[URL scheme] isEqualToString:[[self class] wordpressAppURLScheme]] && [[URL host] isEqualToString:@"authorize"]) { 273 | NSDictionary *params = [self dictionaryFromQueryString:[URL query]]; 274 | NSString *clientId = [params objectForKey:@"client_id"]; 275 | NSString *redirectUrl = [params objectForKey:@"redirect_uri"]; 276 | if (clientId && redirectUrl) { 277 | WPComOAuthController *ssoController = [[WPComOAuthController alloc] initForSSO]; 278 | [ssoController setWordPressComUsername:_username]; 279 | [ssoController setWordPressComAuthToken:_authToken]; 280 | [ssoController setClient:clientId]; 281 | [ssoController setRedirectUrl:redirectUrl]; 282 | [ssoController present]; 283 | return YES; 284 | } 285 | } 286 | 287 | if (![[self class] isThisTheWordPressApp] && [[URL scheme] hasPrefix:@"wordpress-"] && [[URL host] isEqualToString:@"wordpress-sso"]) { 288 | NSDictionary *params = [self dictionaryFromQueryString:[URL query]]; 289 | NSString *code = [params objectForKey:@"code"]; 290 | if (code) { 291 | [self getTokenWithCode:code secret:_secret]; 292 | } else if ([params objectForKey:@"error_description"]) { 293 | if (_completionBlock) { 294 | _completionBlock(nil, nil, nil, nil, [NSError errorWithDomain:WPComOAuthErrorDomain code:WPComOAuthErrorCodeUnknown userInfo:@{NSLocalizedDescriptionKey: [params objectForKey:@"error_description"]}]); 295 | } 296 | } else { 297 | if (_completionBlock) { 298 | _completionBlock(nil, nil, nil, nil, nil); 299 | } 300 | } 301 | return YES; 302 | } 303 | return NO; 304 | } 305 | 306 | - (void)openCallbackWithQueryString:(NSString *)query { 307 | _isSSO = NO; 308 | NSString *callbackUrl = [NSString stringWithFormat:@"wordpress-%@://wordpress-sso?%@", _clientId, query]; 309 | [[UIApplication sharedApplication] openURL:[NSURL URLWithString:callbackUrl]]; 310 | } 311 | 312 | #pragma mark - UIWebViewDelegate 313 | 314 | - (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType { 315 | NSLog(@"webView should load %@", [request URL]); 316 | NSURL *url = [request URL]; 317 | if ([[url absoluteString] isEqualToString:@"about:blank"]) { 318 | return YES; 319 | } 320 | if ([[url absoluteString] hasPrefix:WPComOAuthBaseUrl] || [[url absoluteString] hasPrefix:WPComOAuthLoginUrl]) { 321 | NSLog(@"loading %@", url); 322 | return YES; 323 | } else if ([[url absoluteString] hasPrefix:_redirectUrl]) { 324 | NSLog(@"found redirect URL"); 325 | NSString *query = [url query]; 326 | NSArray *parameters = [query componentsSeparatedByString:@"&"]; 327 | NSString *code = nil; 328 | for (NSString *parameter in parameters) { 329 | if ([parameter hasPrefix:@"code="]) { 330 | code = [[parameter componentsSeparatedByString:@"="] lastObject]; 331 | NSLog(@"found code: %@", code); 332 | break; 333 | } 334 | } 335 | if (code) { 336 | if (_isSSO) { 337 | [self openCallbackWithQueryString:query]; 338 | } else { 339 | [self getTokenWithCode:code secret:_secret]; 340 | } 341 | [self dismissMe]; 342 | return NO; 343 | } else { 344 | [self cancel:nil]; 345 | } 346 | } 347 | return YES; 348 | } 349 | 350 | - (void)webView:(UIWebView *)aWebView didFailLoadWithError:(NSError *)error { 351 | // 102 is the error code when we refuse to load a request 352 | if (error.code != 102) { 353 | NSLog(@"webView failed loading %@: %@", aWebView.request.URL, [error localizedDescription]); 354 | [self cancel:nil]; 355 | } 356 | } 357 | 358 | @end 359 | -------------------------------------------------------------------------------- /Pod/WPHTTPAuthenticationAlertController.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | @interface WPHTTPAuthenticationAlertController : UIAlertController 4 | + (void)presentWithChallenge:(NSURLAuthenticationChallenge *)challenge; 5 | @end 6 | -------------------------------------------------------------------------------- /Pod/WPHTTPAuthenticationAlertController.m: -------------------------------------------------------------------------------- 1 | #import "WPHTTPAuthenticationAlertController.h" 2 | 3 | @interface WPHTTPAuthenticationAlertController () 4 | @property (nonatomic, strong) NSURLAuthenticationChallenge *challenge; 5 | @end 6 | 7 | @implementation WPHTTPAuthenticationAlertController 8 | 9 | + (void)presentWithChallenge:(NSURLAuthenticationChallenge *)challenge { 10 | UIAlertController *controller; 11 | if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) { 12 | controller = [self controllerForServerTrustChallenge:challenge]; 13 | } else { 14 | controller = [self controllerForUserAuthenticationChallenge:challenge]; 15 | } 16 | UIViewController *presentingController = [[[UIApplication sharedApplication] keyWindow] rootViewController]; 17 | if (presentingController.presentedViewController) { 18 | presentingController = presentingController.presentedViewController; 19 | } 20 | 21 | [presentingController presentViewController:controller animated:YES completion:nil]; 22 | } 23 | 24 | + (UIAlertController *)controllerForServerTrustChallenge:(NSURLAuthenticationChallenge *)challenge { 25 | NSString *title = NSLocalizedString(@"Certificate error", @"Popup title for wrong SSL certificate."); 26 | NSString *message = [NSString stringWithFormat:NSLocalizedString(@"The certificate for this server is invalid. You might be connecting to a server that is pretending to be “%@” which could put your confidential information at risk.\n\nWould you like to trust the certificate anyway?", @""), challenge.protectionSpace.host]; 27 | UIAlertController *controller = [UIAlertController alertControllerWithTitle:title 28 | message:message 29 | preferredStyle:UIAlertControllerStyleAlert]; 30 | 31 | UIAlertAction *cancelAction = [UIAlertAction actionWithTitle:NSLocalizedString(@"Cancel", @"Cancel button label") 32 | style:UIAlertActionStyleDefault 33 | handler:^(UIAlertAction * _Nonnull action) { 34 | [challenge.sender cancelAuthenticationChallenge:challenge]; 35 | }]; 36 | [controller addAction:cancelAction]; 37 | 38 | UIAlertAction *trustAction = [UIAlertAction actionWithTitle:NSLocalizedString(@"Trust", @"Connect when the SSL certificate is invalid") 39 | style:UIAlertActionStyleDefault 40 | handler:^(UIAlertAction * _Nonnull action) { 41 | NSURLCredential *credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust]; 42 | 43 | [[NSURLCredentialStorage sharedCredentialStorage] setDefaultCredential:credential forProtectionSpace:challenge.protectionSpace]; 44 | [challenge.sender useCredential:credential forAuthenticationChallenge:challenge]; 45 | }]; 46 | [controller addAction:trustAction]; 47 | return controller; 48 | } 49 | 50 | + (UIAlertController *)controllerForUserAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge { 51 | NSString *title = NSLocalizedString(@"Authentication required", @"Popup title to ask for user credentials."); 52 | NSString *message = NSLocalizedString(@"Please enter your credentials", @"Popup message to ask for user credentials (fields shown below)."); 53 | UIAlertController *controller = [UIAlertController alertControllerWithTitle:title 54 | message:message 55 | preferredStyle:UIAlertControllerStyleAlert]; 56 | 57 | [controller addTextFieldWithConfigurationHandler:^(UITextField * _Nonnull textField) { 58 | textField.placeholder = NSLocalizedString(@"Username", @"Login dialog username placeholder"); 59 | }]; 60 | 61 | [controller addTextFieldWithConfigurationHandler:^(UITextField * _Nonnull textField) { 62 | textField.placeholder = NSLocalizedString(@"Password", @"Login dialog password placeholder"); 63 | textField.secureTextEntry = YES; 64 | }]; 65 | 66 | UIAlertAction *cancelAction = [UIAlertAction actionWithTitle:NSLocalizedString(@"Cancel", @"Cancel button label") 67 | style:UIAlertActionStyleDefault 68 | handler:^(UIAlertAction * _Nonnull action) { 69 | [challenge.sender cancelAuthenticationChallenge:challenge]; 70 | }]; 71 | [controller addAction:cancelAction]; 72 | 73 | UIAlertAction *loginAction = [UIAlertAction actionWithTitle:NSLocalizedString(@"Log In", @"Log In button label.") 74 | style:UIAlertActionStyleDefault 75 | handler:^(UIAlertAction * _Nonnull action) { 76 | NSString *username = controller.textFields.firstObject.text; 77 | NSString *password = controller.textFields.lastObject.text; 78 | NSURLCredential *credential = [NSURLCredential credentialWithUser:username password:password persistence:NSURLCredentialPersistencePermanent]; 79 | 80 | [[NSURLCredentialStorage sharedCredentialStorage] setDefaultCredential:credential forProtectionSpace:challenge.protectionSpace]; 81 | [challenge.sender useCredential:credential forAuthenticationChallenge:challenge]; 82 | }]; 83 | [controller addAction:loginAction]; 84 | return controller; 85 | } 86 | 87 | @end 88 | -------------------------------------------------------------------------------- /Pod/WPHTTPRequestOperation.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | @interface WPHTTPRequestOperation : AFHTTPRequestOperation 4 | 5 | @end 6 | -------------------------------------------------------------------------------- /Pod/WPHTTPRequestOperation.m: -------------------------------------------------------------------------------- 1 | #import "WPHTTPRequestOperation.h" 2 | 3 | #import 4 | #import "WPHTTPAuthenticationAlertController.h" 5 | 6 | @implementation WPHTTPRequestOperation 7 | 8 | #pragma mark - NSURLConnectionDelegate 9 | 10 | - (void)connection:(NSURLConnection *)connection willSendRequestForAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge 11 | { 12 | if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) { 13 | // Handle invalid certificates 14 | SecTrustResultType result; 15 | OSStatus certificateStatus = SecTrustEvaluate(challenge.protectionSpace.serverTrust, &result); 16 | if (certificateStatus == 0 && result == kSecTrustResultRecoverableTrustFailure) { 17 | dispatch_async(dispatch_get_main_queue(), ^(void) { 18 | [WPHTTPAuthenticationAlertController presentWithChallenge:challenge]; 19 | }); 20 | } else { 21 | [challenge.sender continueWithoutCredentialForAuthenticationChallenge:challenge]; 22 | } 23 | } else if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodClientCertificate]) { 24 | [challenge.sender continueWithoutCredentialForAuthenticationChallenge:challenge]; 25 | } else { 26 | NSURLCredential *credential = [[NSURLCredentialStorage sharedCredentialStorage] defaultCredentialForProtectionSpace:[challenge protectionSpace]]; 27 | 28 | if ([challenge previousFailureCount] == 0 && credential) { 29 | [[challenge sender] useCredential:credential forAuthenticationChallenge:challenge]; 30 | } else { 31 | dispatch_async(dispatch_get_main_queue(), ^(void) { 32 | [WPHTTPAuthenticationAlertController presentWithChallenge:challenge]; 33 | }); 34 | } 35 | } 36 | } 37 | 38 | @end 39 | -------------------------------------------------------------------------------- /Pod/WPRSDParser.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | @interface WPRSDParser : NSObject 4 | - (id)initWithXmlString:(NSString *)string; 5 | - (NSString *)parsedEndpointWithError:(NSError **)error; 6 | @end 7 | -------------------------------------------------------------------------------- /Pod/WPRSDParser.m: -------------------------------------------------------------------------------- 1 | #import "WPRSDParser.h" 2 | 3 | @implementation WPRSDParser { 4 | NSXMLParser *_parser; 5 | NSString *_endpoint; 6 | NSError *_error; 7 | } 8 | 9 | - (id)initWithXmlString:(NSString *)string { 10 | self = [super init]; 11 | if (self) { 12 | _parser = [[NSXMLParser alloc] initWithData:[string dataUsingEncoding:NSUTF8StringEncoding]]; 13 | [_parser setDelegate:self]; 14 | } 15 | return self; 16 | } 17 | 18 | - (NSString *)parsedEndpointWithError:(NSError **)error { 19 | [_parser parse]; 20 | if (error) *error = _error; 21 | return _endpoint; 22 | } 23 | 24 | #pragma mark - NSXMLParserDelegate 25 | 26 | - (void)parser:(NSXMLParser *)parser didStartElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName attributes:(NSDictionary *)attributeDict { 27 | if ([elementName isEqualToString:@"api"]) { 28 | NSString *apiName = attributeDict[@"name"]; 29 | if (apiName && [apiName isEqualToString:@"WordPress"]) { 30 | _endpoint = attributeDict[@"apiLink"]; 31 | if (_endpoint) { 32 | [_parser abortParsing]; 33 | } 34 | } 35 | } 36 | } 37 | 38 | - (void)parser:(NSXMLParser *)parser parseErrorOccurred:(NSError *)parseError { 39 | _error = parseError; 40 | } 41 | 42 | @end 43 | -------------------------------------------------------------------------------- /Pod/WPXMLRPCClient.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | 4 | @class WPXMLRPCRequestOperation, WPXMLRPCRequest; 5 | 6 | extern NSString *const WPXMLRPCClientErrorDomain; 7 | 8 | /** 9 | `AFXMLRPCClient` binds together AFNetworking and eczarny's XML-RPC library to interact with XML-RPC based APIs 10 | */ 11 | @interface WPXMLRPCClient : NSObject 12 | 13 | ///--------------------------------------- 14 | /// @name Accessing HTTP Client Properties 15 | ///--------------------------------------- 16 | 17 | /** 18 | The url used as the XML-RPC endpoint 19 | */ 20 | @property (readonly, nonatomic, strong) NSURL *xmlrpcEndpoint; 21 | 22 | /** 23 | The operation queue which manages operations enqueued by the HTTP client. 24 | */ 25 | @property (readonly, nonatomic, strong) NSOperationQueue *operationQueue; 26 | 27 | ///------------------------------------------------ 28 | /// @name Creating and Initializing XML-RPC Clients 29 | ///------------------------------------------------ 30 | 31 | /** 32 | Creates and initializes an `AFXMLRPCClient` object with the specified base URL. 33 | 34 | @param xmlrpcEndpoint The XML-RPC endpoint URL for the XML-RPC client. This argument must not be nil. 35 | 36 | @return The newly-initialized XML-RPC client 37 | */ 38 | + (WPXMLRPCClient *)clientWithXMLRPCEndpoint:(NSURL *)xmlrpcEndpoint; 39 | 40 | /** 41 | Initializes an `AFXMLRPCClient` object with the specified base URL. 42 | 43 | @param xmlrpcEndpoint The XML-RPC endpoint URL for the XML-RPC client. This argument must not be nil. 44 | 45 | @return The newly-initialized XML-RPC client 46 | */ 47 | - (id)initWithXMLRPCEndpoint:(NSURL *)xmlrpcEndpoint; 48 | 49 | ///---------------------------------- 50 | /// @name Managing HTTP Header Values 51 | ///---------------------------------- 52 | 53 | /** 54 | Returns the value for the HTTP headers set in request objects created by the HTTP client. 55 | 56 | @param header The HTTP header to return the default value for 57 | 58 | @return The default value for the HTTP header, or `nil` if unspecified 59 | */ 60 | - (NSString *)defaultValueForHeader:(NSString *)header; 61 | 62 | /** 63 | Sets the value for the HTTP headers set in request objects made by the HTTP client. If `nil`, removes the existing value for that header. 64 | 65 | @param header The HTTP header to set a default value for 66 | @param value The value set as default for the specified header, or `nil 67 | */ 68 | - (void)setDefaultHeader:(NSString *)header value:(NSString *)value; 69 | 70 | /** 71 | Sets the "Authorization" HTTP header set in request objects made by the HTTP client to a token-based authentication value, such as an OAuth access token. This overwrites any existing value for this header. 72 | 73 | @param token The authentication token 74 | */ 75 | - (void)setAuthorizationHeaderWithToken:(NSString *)token; 76 | 77 | /** 78 | Clears any existing value for the "Authorization" HTTP header. 79 | */ 80 | - (void)clearAuthorizationHeader; 81 | 82 | ///------------------------------- 83 | /// @name Creating Request Objects 84 | ///------------------------------- 85 | 86 | /** 87 | Creates a `NSMutableURLRequest` object with the specified XML-RPC method and parameters. 88 | 89 | @param method The XML-RPC method for the request. 90 | @param parameters The XML-RPC parameters to be set as the request body. 91 | 92 | @return A `NSMutableURLRequest` object 93 | */ 94 | - (NSMutableURLRequest *)requestWithMethod:(NSString *)method 95 | parameters:(NSArray *)parameters; 96 | 97 | /** 98 | Creates a `NSMutableURLRequest` object with the specified XML-RPC method and parameters, but uses streaming to encode and send the XML-RPC request. 99 | 100 | @param method The XML-RPC method for the request. 101 | @param parameters The XML-RPC parameters to be set as the request body. 102 | @param filePathForCache The path wehere the streaming request will be cached. This file can only be delete after 103 | @return A `NSMutableURLRequest` object 104 | */ 105 | - (NSMutableURLRequest *)streamingRequestWithMethod:(NSString *)method 106 | parameters:(NSArray *)parameters 107 | usingFilePathForCache:(NSString *)filePath; 108 | 109 | /** 110 | Creates an `AFXMLRPCRequest` object with the specified XML-RPC method and parameters. 111 | 112 | @param method The XML-RPC method for the request. 113 | @param parameters The XML-RPC parameters to be set as the request body. 114 | 115 | @return An `AFXMLRPCRequest` object 116 | */ 117 | - (WPXMLRPCRequest *)XMLRPCRequestWithMethod:(NSString *)method 118 | parameters:(NSArray *)parameters; 119 | 120 | ///------------------------------- 121 | /// @name Creating HTTP Operations 122 | ///------------------------------- 123 | 124 | /** 125 | Creates an `AFHTTPRequestOperation` 126 | 127 | @param request The request object to be loaded asynchronously during execution of the operation. 128 | @param success A block object to be executed when the request operation finishes successfully. This block has no return value and takes two arguments: the created request operation and the object created from the response data of request. 129 | @param failure A block object to be executed when the request operation finishes unsuccessfully, or that finishes successfully, but encountered an error while parsing the resonse data. This block has no return value and takes two arguments:, the created request operation and the `NSError` object describing the network or parsing error that occurred. 130 | */ 131 | - (AFHTTPRequestOperation *)HTTPRequestOperationWithRequest:(NSURLRequest *)request 132 | success:(void (^)(AFHTTPRequestOperation *operation, id responseObject))success 133 | failure:(void (^)(AFHTTPRequestOperation *operation, NSError *error))failure; 134 | 135 | /** 136 | Creates an `AFXMLRPCRequestOperation` 137 | 138 | @param request The request object to be loaded asynchronously during execution of the operation. 139 | @param success A block object to be executed when the request operation finishes successfully. This block has no return value and takes two arguments: the created request operation and the object created from the response data of request. 140 | @param failure A block object to be executed when the request operation finishes unsuccessfully, or that finishes successfully, but encountered an error while parsing the resonse data. This block has no return value and takes two arguments:, the created request operation and the `NSError` object describing the network or parsing error that occurred. 141 | */ 142 | - (WPXMLRPCRequestOperation *)XMLRPCRequestOperationWithRequest:(WPXMLRPCRequest *)request 143 | success:(void (^)(AFHTTPRequestOperation *operation, id responseObject))success 144 | failure:(void (^)(AFHTTPRequestOperation *operation, NSError *error))failure; 145 | 146 | /** 147 | Creates an `AFHTTPRequestOperation` combining multiple XML-RPC calls in a single request using `system.multicall` 148 | 149 | @param operations An array of `AFXMLRPCRequestOperation` objects 150 | @param success A block object to be executed when the request operation finishes successfully. This block has no return value and takes two arguments: the created request operation and the object created from the response data of request. 151 | @param failure A block object to be executed when the request operation finishes unsuccessfully, or that finishes successfully, but encountered an error while parsing the resonse data. This block has no return value and takes two arguments:, the created request operation and the `NSError` object describing the network or parsing error that occurred. 152 | */ 153 | - (AFHTTPRequestOperation *)combinedHTTPRequestOperationWithOperations:(NSArray *)operations 154 | success:(void (^)(AFHTTPRequestOperation *operation, id responseObject))success 155 | failure:(void (^)(AFHTTPRequestOperation *operation, NSError *error))failure; 156 | 157 | ///---------------------------------------- 158 | /// @name Managing Enqueued HTTP Operations 159 | ///---------------------------------------- 160 | 161 | /** 162 | Enqueues an `AFHTTPRequestOperation` to the XML-RPC client's operation queue. 163 | 164 | @param operation The XML-RPC request operation to be enqueued. 165 | */ 166 | - (void)enqueueHTTPRequestOperation:(AFHTTPRequestOperation *)operation; 167 | 168 | /** 169 | Enqueues an `AFXMLRPCRequestOperation` to the XML-RPC client's operation queue. 170 | 171 | @param operation The XML-RPC request operation to be enqueued. 172 | */ 173 | - (void)enqueueXMLRPCRequestOperation:(WPXMLRPCRequestOperation *)operation; 174 | 175 | /** 176 | Cancels all operations in the HTTP client's operation queue. 177 | */ 178 | - (void)cancelAllHTTPOperations; 179 | 180 | 181 | ///------------------------------ 182 | /// @name Making XML-RPC requests 183 | ///------------------------------ 184 | 185 | /** 186 | Creates an `AFHTTPRequestOperation` with a `XML-RPC` request, and enqueues it to the HTTP client's operation queue. 187 | 188 | @param method The XML-RPC method. 189 | @param parameters The XML-RPC parameters to be set as the request body. 190 | @param success A block object to be executed when the request operation finishes successfully. This block has no return value and takes two arguments: the created request operation and the object created from the response data of request. 191 | @param failure A block object to be executed when the request operation finishes unsuccessfully, or that finishes successfully, but encountered an error while parsing the resonse data. This block has no return value and takes two arguments:, the created request operation and the `NSError` object describing the network or parsing error that occurred. 192 | 193 | @see HTTPRequestOperationWithRequest:success:failure 194 | */ 195 | - (void)callMethod:(NSString *)method 196 | parameters:(NSArray *)parameters 197 | success:(void (^)(AFHTTPRequestOperation *operation, id responseObject))success 198 | failure:(void (^)(AFHTTPRequestOperation *operation, NSError *error))failure; 199 | 200 | @end 201 | -------------------------------------------------------------------------------- /Pod/WPXMLRPCClient.m: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | #import 4 | 5 | #import "WPXMLRPCClient.h" 6 | 7 | #import "WPXMLRPCRequest.h" 8 | #import "WPXMLRPCRequestOperation.h" 9 | #import "WPHTTPRequestOperation.h" 10 | 11 | #ifndef WPFLog 12 | #define WPFLog(...) NSLog(__VA_ARGS__) 13 | #endif 14 | 15 | NSString *const WPXMLRPCClientErrorDomain = @"XMLRPC"; 16 | static NSUInteger const WPXMLRPCClientDefaultMaxConcurrentOperationCount = 4; 17 | 18 | @interface WPXMLRPCClient () 19 | @property (readwrite, nonatomic, strong) NSURL *xmlrpcEndpoint; 20 | @property (readwrite, nonatomic, strong) NSMutableDictionary *defaultHeaders; 21 | @property (readwrite, nonatomic, strong) NSOperationQueue *operationQueue; 22 | @end 23 | 24 | @implementation WPXMLRPCClient 25 | 26 | #pragma mark - Creating and Initializing XML-RPC Clients 27 | 28 | + (WPXMLRPCClient *)clientWithXMLRPCEndpoint:(NSURL *)xmlrpcEndpoint { 29 | return [[self alloc] initWithXMLRPCEndpoint:xmlrpcEndpoint]; 30 | } 31 | 32 | - (id)initWithXMLRPCEndpoint:(NSURL *)xmlrpcEndpoint { 33 | self = [super init]; 34 | if (!self) { 35 | return nil; 36 | } 37 | 38 | self.xmlrpcEndpoint = xmlrpcEndpoint; 39 | 40 | self.defaultHeaders = [NSMutableDictionary dictionary]; 41 | 42 | // Accept-Encoding HTTP Header; see http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.3 43 | if (([[[UIDevice currentDevice] systemVersion] compare:@"7.1" options:NSNumericSearch] != NSOrderedAscending)) { 44 | // if iOS 7.1 or later 45 | [self setDefaultHeader:@"Accept-Encoding" value:@"gzip, deflate"]; 46 | } else { 47 | // Disable compression by default, since it causes connection problems with some hosts 48 | // Fixed in iOS SDK 7.1 see: https://developer.apple.com/library/ios/releasenotes/General/RN-iOSSDK-7.1/ 49 | [self setDefaultHeader:@"Accept-Encoding" value:@"identity"]; 50 | } 51 | [self setDefaultHeader:@"Content-Type" value:@"text/xml"]; 52 | 53 | NSString *applicationUserAgent = [[NSUserDefaults standardUserDefaults] objectForKey:@"UserAgent"]; 54 | if (applicationUserAgent) { 55 | [self setDefaultHeader:@"User-Agent" value:applicationUserAgent]; 56 | } else { 57 | [self setDefaultHeader:@"User-Agent" value:[NSString stringWithFormat:@"%@/%@ (%@, %@ %@, %@, Scale/%f)", [[[NSBundle mainBundle] infoDictionary] objectForKey:(NSString *)kCFBundleIdentifierKey], [[[NSBundle mainBundle] infoDictionary] objectForKey:(NSString *)kCFBundleVersionKey], @"unknown", [[UIDevice currentDevice] systemName], [[UIDevice currentDevice] systemVersion], [[UIDevice currentDevice] model], ([[UIScreen mainScreen] respondsToSelector:@selector(scale)] ? [[UIScreen mainScreen] scale] : 1.0)]]; 58 | } 59 | 60 | self.operationQueue = [[NSOperationQueue alloc] init]; 61 | [self.operationQueue setMaxConcurrentOperationCount:WPXMLRPCClientDefaultMaxConcurrentOperationCount]; 62 | 63 | return self; 64 | } 65 | 66 | 67 | #pragma mark - Managing HTTP Header Values 68 | 69 | - (NSString *)defaultValueForHeader:(NSString *)header { 70 | return [self.defaultHeaders valueForKey:header]; 71 | } 72 | 73 | - (void)setDefaultHeader:(NSString *)header value:(NSString *)value { 74 | [self.defaultHeaders setValue:value forKey:header]; 75 | } 76 | 77 | - (void)setAuthorizationHeaderWithToken:(NSString *)token { 78 | [self setDefaultHeader:@"Authorization" value:[NSString stringWithFormat:@"Bearer %@", token]]; 79 | } 80 | 81 | - (void)clearAuthorizationHeader { 82 | [self.defaultHeaders removeObjectForKey:@"Authorization"]; 83 | } 84 | 85 | #pragma mark - Creating Request Objects 86 | 87 | - (NSMutableURLRequest *)requestWithMethod:(NSString *)method 88 | parameters:(NSArray *)parameters { 89 | NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:self.xmlrpcEndpoint]; 90 | [request setHTTPMethod:@"POST"]; 91 | [request setAllHTTPHeaderFields:self.defaultHeaders]; 92 | 93 | WPXMLRPCEncoder *encoder = [[WPXMLRPCEncoder alloc] initWithMethod:method andParameters:parameters]; 94 | [request setHTTPBody:[encoder dataEncodedWithError:nil]]; 95 | 96 | return request; 97 | } 98 | 99 | - (NSMutableURLRequest *)streamingRequestWithMethod:(NSString *)method 100 | parameters:(NSArray *)parameters 101 | usingFilePathForCache:(NSString *)filePath 102 | { 103 | NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:self.xmlrpcEndpoint]; 104 | [request setHTTPMethod:@"POST"]; 105 | [request setAllHTTPHeaderFields:self.defaultHeaders]; 106 | 107 | WPXMLRPCEncoder *encoder = [[WPXMLRPCEncoder alloc] initWithMethod:method andParameters:parameters]; 108 | NSError *error = nil; 109 | if (![encoder encodeToFile:filePath error:&error]){ 110 | WPFLog(@"Error encoding request to stream: %@",[error localizedDescription]); 111 | return nil; 112 | } 113 | 114 | NSDictionary *attributes = [[NSFileManager defaultManager] attributesOfItemAtPath:filePath error:&error]; 115 | if (!attributes){ 116 | WPFLog(@"Error getting length of the request stream: %@",[error localizedDescription]); 117 | return nil; 118 | } 119 | unsigned long long contentLength = [attributes[NSFileSize] unsignedLongLongValue]; 120 | 121 | NSInputStream * inputStream = [NSInputStream inputStreamWithFileAtPath:filePath]; 122 | [request setHTTPBodyStream:inputStream]; 123 | [request setValue:[NSString stringWithFormat:@"%llu", contentLength] forHTTPHeaderField:@"Content-Length"]; 124 | 125 | return request; 126 | } 127 | 128 | - (WPXMLRPCRequest *)XMLRPCRequestWithMethod:(NSString *)method 129 | parameters:(NSArray *)parameters { 130 | return [[WPXMLRPCRequest alloc] initWithMethod:method andParameters:parameters]; 131 | } 132 | 133 | #pragma mark - Creating HTTP Operations 134 | 135 | - (AFHTTPRequestOperation *)HTTPRequestOperationWithRequest:(NSURLRequest *)request 136 | success:(void (^)(AFHTTPRequestOperation *operation, id responseObject))success 137 | failure:(void (^)(AFHTTPRequestOperation *operation, NSError *error))failure { 138 | WPHTTPRequestOperation *operation = [[WPHTTPRequestOperation alloc] initWithRequest:request]; 139 | 140 | BOOL extra_debug_on = getenv("WPDebugXMLRPC") ? YES : NO; 141 | #ifndef DEBUG 142 | NSNumber *extra_debug = [[NSUserDefaults standardUserDefaults] objectForKey:@"extra_debug"]; 143 | if ([extra_debug boolValue]) extra_debug_on = YES; 144 | #endif 145 | 146 | void (^xmlrpcSuccess)(AFHTTPRequestOperation *, id) = ^(AFHTTPRequestOperation *operation, id responseObject) { 147 | dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(void) { 148 | WPXMLRPCDecoder *decoder = [[WPXMLRPCDecoder alloc] initWithData:responseObject]; 149 | NSError *err = nil; 150 | if ( extra_debug_on == YES ) { 151 | WPFLog(@"[XML-RPC] < %@", operation.responseString); 152 | } 153 | 154 | if ([decoder isFault] || [decoder object] == nil) { 155 | err = [decoder error]; 156 | } 157 | 158 | if ([decoder object] == nil && extra_debug_on) { 159 | WPFLog(@"Blog returned invalid data (URL: %@)\n%@", request.URL.absoluteString, operation.responseString); 160 | } 161 | 162 | id object = [[decoder object] copy]; 163 | 164 | dispatch_async(dispatch_get_main_queue(), ^(void) { 165 | if (err) { 166 | if (failure) { 167 | failure(operation, err); 168 | } 169 | } else { 170 | if (success) { 171 | success(operation, object); 172 | } 173 | } 174 | }); 175 | }); 176 | }; 177 | void (^xmlrpcFailure)(AFHTTPRequestOperation *, NSError *) = ^(AFHTTPRequestOperation *operation, NSError *error) { 178 | if ( extra_debug_on == YES ) { 179 | WPFLog(@"[XML-RPC] ! %@", [error localizedDescription]); 180 | } 181 | 182 | if (failure) { 183 | failure(operation, error); 184 | } 185 | }; 186 | [operation setCompletionBlockWithSuccess:xmlrpcSuccess failure:xmlrpcFailure]; 187 | 188 | if ( extra_debug_on == YES ) { 189 | NSString *requestString = [[NSString alloc] initWithData:[request HTTPBody] encoding:NSUTF8StringEncoding]; 190 | if (getenv("WPDebugXMLRPC")) { 191 | WPFLog(@"[XML-RPC] > %@", requestString); 192 | } else { 193 | NSError *error = NULL; 194 | NSRegularExpression *method = [NSRegularExpression regularExpressionWithPattern:@"(.*)" options:NSRegularExpressionCaseInsensitive error:&error]; 195 | NSArray *matches = [method matchesInString:requestString options:0 range:NSMakeRange(0, [requestString length])]; 196 | NSString *methodName = nil; 197 | if (matches.count > 0) { 198 | NSRange methodRange = [[matches objectAtIndex:0] rangeAtIndex:1]; 199 | if(methodRange.location != NSNotFound) 200 | methodName = [requestString substringWithRange:methodRange]; 201 | } else if ([request HTTPBodyStream] != nil) { 202 | methodName = @"streaming request, unknown method"; 203 | } 204 | WPFLog(@"[XML-RPC] > %@", methodName); 205 | } 206 | } 207 | 208 | return operation; 209 | } 210 | 211 | - (WPXMLRPCRequestOperation *)XMLRPCRequestOperationWithRequest:(WPXMLRPCRequest *)request 212 | success:(void (^)(AFHTTPRequestOperation *operation, id responseObject))success 213 | failure:(void (^)(AFHTTPRequestOperation *operation, NSError *error))failure { 214 | WPXMLRPCRequestOperation *operation = [[WPXMLRPCRequestOperation alloc] init]; 215 | operation.XMLRPCRequest = request; 216 | operation.success = success; 217 | operation.failure = failure; 218 | 219 | return operation; 220 | } 221 | 222 | - (AFHTTPRequestOperation *)combinedHTTPRequestOperationWithOperations:(NSArray *)operations success:(void (^)(AFHTTPRequestOperation *operation, id responseObject))success failure:(void (^)(AFHTTPRequestOperation *operation, NSError *error))failure { 223 | NSMutableArray *parameters = [NSMutableArray array]; 224 | 225 | for (WPXMLRPCRequestOperation *operation in operations) { 226 | NSDictionary *param = [NSDictionary dictionaryWithObjectsAndKeys: 227 | operation.XMLRPCRequest.method, @"methodName", 228 | operation.XMLRPCRequest.parameters, @"params", 229 | nil]; 230 | [parameters addObject:param]; 231 | } 232 | 233 | NSURLRequest *request = [self requestWithMethod:@"system.multicall" parameters:parameters]; 234 | void (^_success)(AFHTTPRequestOperation *operation, id responseObject) = ^(AFHTTPRequestOperation *multicallOperation, id responseObject) { 235 | NSArray *responses = (NSArray *)responseObject; 236 | for (int i = 0; i < [responses count]; i++) { 237 | WPXMLRPCRequestOperation *operation = [operations objectAtIndex:i]; 238 | id object = [responses objectAtIndex:i]; 239 | 240 | NSError *error = nil; 241 | if ([object isKindOfClass:[NSDictionary class]] && [object objectForKey:@"faultCode"] && [object objectForKey:@"faultString"]) { 242 | NSDictionary *usrInfo = [NSDictionary dictionaryWithObjectsAndKeys:[object objectForKey:@"faultString"], NSLocalizedDescriptionKey, nil]; 243 | error = [NSError errorWithDomain:WPXMLRPCClientErrorDomain code:[[object objectForKey:@"faultCode"] intValue] userInfo:usrInfo]; 244 | } else if ([object isKindOfClass:[NSArray class]] && [object count] == 1) { 245 | object = [object objectAtIndex:0]; 246 | } 247 | 248 | 249 | if (error) { 250 | if (operation.failure) { 251 | operation.failure(multicallOperation, error); 252 | } 253 | } else { 254 | if (operation.success) { 255 | operation.success(multicallOperation, object); 256 | } 257 | } 258 | } 259 | if (success) { 260 | success(multicallOperation, responseObject); 261 | } 262 | }; 263 | void (^_failure)(AFHTTPRequestOperation *operation, NSError *error) = ^(AFHTTPRequestOperation *multicallOperation, NSError *error) { 264 | for (WPXMLRPCRequestOperation *operation in operations) { 265 | if (operation.failure) { 266 | operation.failure(multicallOperation, error); 267 | } 268 | } 269 | if (failure) { 270 | failure(multicallOperation, error); 271 | } 272 | }; 273 | AFHTTPRequestOperation *operation = [self HTTPRequestOperationWithRequest:request 274 | success:_success 275 | failure:_failure]; 276 | return operation; 277 | } 278 | 279 | #pragma mark - Managing Enqueued HTTP Operations 280 | 281 | - (void)enqueueHTTPRequestOperation:(AFHTTPRequestOperation *)operation { 282 | [self.operationQueue addOperation:operation]; 283 | } 284 | 285 | - (void)enqueueXMLRPCRequestOperation:(WPXMLRPCRequestOperation *)operation { 286 | NSURLRequest *request = [self requestWithMethod:operation.XMLRPCRequest.method parameters:operation.XMLRPCRequest.parameters]; 287 | AFHTTPRequestOperation *op = [self HTTPRequestOperationWithRequest:request success:operation.success failure:operation.failure]; 288 | [self enqueueHTTPRequestOperation:op]; 289 | } 290 | 291 | - (void)cancelAllHTTPOperations { 292 | for (AFHTTPRequestOperation *operation in [self.operationQueue operations]) { 293 | [operation cancel]; 294 | } 295 | } 296 | 297 | #pragma mark - Making XML-RPC Requests 298 | 299 | - (void)callMethod:(NSString *)method 300 | parameters:(NSArray *)parameters 301 | success:(void (^)(AFHTTPRequestOperation *operation, id responseObject))success 302 | failure:(void (^)(AFHTTPRequestOperation *operation, NSError *error))failure { 303 | NSURLRequest *request = [self requestWithMethod:method parameters:parameters]; 304 | AFHTTPRequestOperation *operation = [self HTTPRequestOperationWithRequest:request success:success failure:failure]; 305 | 306 | [self enqueueHTTPRequestOperation:operation]; 307 | } 308 | 309 | 310 | @end 311 | -------------------------------------------------------------------------------- /Pod/WPXMLRPCRequest.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | /** 4 | `WPXMLRPCRequest` represents a XML-RPC request. 5 | 6 | It is designed to combine multiple requests using `system.multicall` but can also be used for single requests 7 | */ 8 | @interface WPXMLRPCRequest : NSObject 9 | 10 | /** 11 | Initializes a `WPXMLRPCRequest` object with the specified method and parameters. 12 | 13 | @param method the XML-RPC method for this request 14 | @param parameters an array containing the parameters for the request. If you want to support streaming, you can use either `NSInputStream` or `NSFileHandle` to encode binary data 15 | 16 | @return The newly-initialized XML-RPC request 17 | */ 18 | - (id)initWithMethod:(NSString *)method andParameters:(NSArray *)parameters; 19 | 20 | /** 21 | The XML-RPC method for this request. 22 | 23 | This is a *read-only* property, as requests can't be reused. 24 | */ 25 | @property (nonatomic, readonly) NSString *method; 26 | 27 | /** 28 | The XML-RPC parameters for this request. 29 | 30 | This is a *read-only* property, as requests can't be reused. 31 | */ 32 | @property (nonatomic, readonly) NSArray *parameters; 33 | 34 | @end -------------------------------------------------------------------------------- /Pod/WPXMLRPCRequest.m: -------------------------------------------------------------------------------- 1 | #import "WPXMLRPCRequest.h" 2 | 3 | @implementation WPXMLRPCRequest 4 | 5 | - (id)initWithMethod:(NSString *)method andParameters:(NSArray *)parameters { 6 | self = [super init]; 7 | if (self) { 8 | _method = method; 9 | _parameters = parameters; 10 | } 11 | return self; 12 | } 13 | 14 | @end 15 | -------------------------------------------------------------------------------- /Pod/WPXMLRPCRequestOperation.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | @class WPXMLRPCRequest; 4 | 5 | @interface WPXMLRPCRequestOperation : NSObject 6 | @property (nonatomic, strong) WPXMLRPCRequest *XMLRPCRequest; 7 | @property (nonatomic, copy) void (^success)(AFHTTPRequestOperation *operation, id responseObject); 8 | @property (nonatomic, copy) void (^failure)(AFHTTPRequestOperation *operation, NSError *error); 9 | @end 10 | -------------------------------------------------------------------------------- /Pod/WPXMLRPCRequestOperation.m: -------------------------------------------------------------------------------- 1 | #import "WPXMLRPCRequestOperation.h" 2 | 3 | @implementation WPXMLRPCRequestOperation 4 | 5 | @end 6 | -------------------------------------------------------------------------------- /Pod/WordPressApi-Prefix.pch: -------------------------------------------------------------------------------- 1 | // 2 | // Prefix header 3 | // 4 | // The contents of this file are implicitly included at the beginning of every source file. 5 | // 6 | 7 | #ifdef __OBJC__ 8 | #endif 9 | -------------------------------------------------------------------------------- /Pod/WordPressApi.h: -------------------------------------------------------------------------------- 1 | #import 2 | #ifndef _WORDPRESSAPI 3 | #define _WORDPRESSAPI 4 | #import "WPXMLRPCClient.h" 5 | #import "WPXMLRPCRequest.h" 6 | #import "WPXMLRPCRequestOperation.h" 7 | #import "WordPressBaseApi.h" 8 | #import "WordPressRestApi.h" 9 | #import "WordPressXMLRPCApi.h" 10 | #import "WPComOAuthController.h" 11 | #endif /* _WORDPRESSAPI */ 12 | 13 | @interface WordPressApi : NSObject 14 | + (void)signInWithURL:(NSString *)url username:(NSString *)username password:(NSString *)password success:(void (^)(NSURL *xmlrpcURL))success failure:(void (^)(NSError *error))failure; 15 | 16 | + (void)signInWithXMLRPCURL:(NSURL *)xmlrpcURL username:(NSString *)username password:(NSString *)password success:(void (^)())success failure:(void (^)(NSError *error))failure; 17 | + (id)apiWithXMLRPCURL:(NSURL *)xmlrpcURL username:(NSString *)username password:(NSString *)password; 18 | 19 | + (void)signInWithOauthWithSuccess:(void (^)(NSString *authToken, NSString *siteId))success failure:(void (^)(NSError *error))failure; 20 | + (id)apiWithOauthToken:(NSString *)authToken siteId:(NSString *)siteId; 21 | 22 | + (void)signInWithJetpackUsername:(NSString *)username password:(NSString *)password success:(void (^)(NSString *authToken))success failure:(void (^)(NSError *error))failure; 23 | 24 | + (void)setWordPressComClient:(NSString *)clientId; 25 | + (void)setWordPressComSecret:(NSString *)secret; 26 | + (void)setWordPressComRedirectUrl:(NSString *)redirectUrl; 27 | + (BOOL)handleOpenURL:(NSURL *)URL; 28 | 29 | @end 30 | -------------------------------------------------------------------------------- /Pod/WordPressApi.m: -------------------------------------------------------------------------------- 1 | #import "WordPressApi.h" 2 | #import "WordPressXMLRPCApi.h" 3 | #import "WordPressRestApi.h" 4 | 5 | @implementation WordPressApi 6 | 7 | + (void)signInWithURL:(NSString *)url username:(NSString *)username password:(NSString *)password success:(void (^)(NSURL *xmlrpcURL))success failure:(void (^)(NSError *error))failure { 8 | [WordPressXMLRPCApi guessXMLRPCURLForSite:url success:^(NSURL *xmlrpcURL) { 9 | [self signInWithXMLRPCURL:xmlrpcURL username:username password:password success:^{ 10 | if (success) { 11 | success(xmlrpcURL); 12 | } 13 | } failure:failure]; 14 | } failure:failure]; 15 | } 16 | 17 | + (void)signInWithXMLRPCURL:(NSURL *)xmlrpcURL username:(NSString *)username password:(NSString *)password success:(void (^)())success failure:(void (^)(NSError *error))failure { 18 | WordPressXMLRPCApi *api = [self apiWithXMLRPCURL:xmlrpcURL username:username password:password]; 19 | [api authenticateWithSuccess:success failure:failure]; 20 | } 21 | 22 | + (id)apiWithXMLRPCURL:(NSURL *)xmlrpcURL username:(NSString *)username password:(NSString *)password { 23 | return [WordPressXMLRPCApi apiWithXMLRPCEndpoint:xmlrpcURL username:username password:password]; 24 | } 25 | 26 | + (void)signInWithOauthWithSuccess:(void (^)(NSString *authToken, NSString *siteId))success failure:(void (^)(NSError *error))failure { 27 | [WordPressRestApi signInWithOauthWithSuccess:success failure:failure]; 28 | } 29 | 30 | + (id)apiWithOauthToken:(NSString *)authToken siteId:(NSString *)siteId { 31 | return [[WordPressRestApi alloc] initWithOauthToken:authToken siteId:siteId]; 32 | } 33 | 34 | + (void)signInWithJetpackUsername:(NSString *)username password:(NSString *)password success:(void (^)(NSString *authToken))success failure:(void (^)(NSError *error))failure { 35 | [WordPressRestApi signInWithJetpackUsername:username password:password success:success failure:failure]; 36 | } 37 | 38 | + (void)setWordPressComClient:(NSString *)clientId { 39 | [WordPressRestApi setWordPressComClient:clientId]; 40 | } 41 | 42 | + (void)setWordPressComSecret:(NSString *)secret { 43 | [WordPressRestApi setWordPressComSecret:secret]; 44 | } 45 | 46 | + (void)setWordPressComRedirectUrl:(NSString *)redirectUrl { 47 | [WordPressRestApi setWordPressComRedirectUrl:redirectUrl]; 48 | } 49 | 50 | + (BOOL)handleOpenURL:(NSURL *)URL { 51 | return [WordPressRestApi handleOpenURL:URL]; 52 | } 53 | 54 | @end 55 | -------------------------------------------------------------------------------- /Pod/WordPressBaseApi.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | 4 | @protocol WordPressBaseApi 5 | 6 | ///----------------------- 7 | /// @name Quick Publishing 8 | ///----------------------- 9 | 10 | /** 11 | Publishes a post asynchronously with text/HTML only 12 | 13 | All the parameters are optional, and can be set to `nil` 14 | 15 | @param content The post content/body. It can be text only or HTML, but be aware that some HTML might be stripped in WordPress. [What's allowed in WordPress.com?](http://en.support.wordpress.com/code/) 16 | @param title The post title. 17 | @param success A block object to execute when the method successfully publishes the post. This block has no return value and takes two arguments: the resulting post ID, and the permalink (or [shortlink](http://en.support.wordpress.com/shortlinks/) if available). 18 | @param failure A block object to execute when the method can't publish the post. This block has no return value and takes one argument: a NSError object with details on the error. 19 | */ 20 | - (void)publishPostWithText:(NSString *)content 21 | title:(NSString *)title 22 | success:(void (^)(NSUInteger postId, NSURL *permalink))success 23 | failure:(void (^)(NSError *error))failure; 24 | 25 | /** 26 | Publishes a post asynchronously with an image 27 | 28 | All the parameters are optional, and can be set to `nil` 29 | 30 | @warning **Not implemented yet**. It just calls publishPostWIthText:title:success:failure: ignoring the image 31 | @param image An image to add to the post. The image will be embedded **before** the content. 32 | @param content The post content/body. It can be text only or HTML, but be aware that some HTML might be stripped in WordPress. [What's allowed in WordPress.com?](http://en.support.wordpress.com/code/) 33 | @param title The post title. 34 | @param success A block object to execute when the method successfully publishes the post. This block has no return value and takes two arguments: the resulting post ID, and the permalink (or [shortlink](http://en.support.wordpress.com/shortlinks/) if available). 35 | @param failure A block object to execute when the method can't publish the post. This block has no return value and takes one argument: a NSError object with details on the error. 36 | */ 37 | - (void)publishPostWithImage:(UIImage *)image 38 | description:(NSString *)content 39 | title:(NSString *)title 40 | success:(void (^)(NSUInteger postId, NSURL *permalink))success 41 | failure:(void (^)(NSError *error))failure; 42 | 43 | /** 44 | Publishes a post asynchronously with an image gallery 45 | 46 | All the parameters are optional, and can be set to `nil` 47 | 48 | @warning **Not implemented yet**. It just calls publishPostWIthText:title:success:failure: ignoring the images 49 | @param images An array containing images (as UIImage) to add to the post. The gallery will be embedded **before** the content using the [[gallery]](http://en.support.wordpress.com/images/gallery/) shortcode. 50 | @param content The post content/body. It can be text only or HTML, but be aware that some HTML might be stripped in WordPress. [What's allowed in WordPress.com?](http://en.support.wordpress.com/code/) 51 | @param title The post title. 52 | @param success A block object to execute when the method successfully publishes the post. This block has no return value and takes two arguments: the resulting post ID, and the permalink (or [shortlink](http://en.support.wordpress.com/shortlinks/) if available). 53 | @param failure A block object to execute when the method can't publish the post. This block has no return value and takes one argument: a NSError object with details on the error. 54 | */ 55 | - (void)publishPostWithGallery:(NSArray *)images 56 | description:(NSString *)content 57 | title:(NSString *)title 58 | success:(void (^)(NSUInteger postId, NSURL *permalink))success 59 | failure:(void (^)(NSError *error))failure; 60 | 61 | 62 | ///--------------------- 63 | /// @name Managing posts 64 | ///--------------------- 65 | 66 | /** 67 | Get a list of the recent posts 68 | 69 | @param count Number of recent posts to get 70 | @param success A block object to execute when the method successfully publishes the post. This block has no return value and takes one argument: an array with the latest posts. 71 | @param failure A block object to execute when the method can't publish the post. This block has no return value and takes one argument: a NSError object with details on the error. 72 | */ 73 | - (void)getPosts:(NSUInteger)count 74 | success:(void (^)(NSArray *posts))success 75 | failure:(void (^)(NSError *error))failure; 76 | 77 | @end 78 | -------------------------------------------------------------------------------- /Pod/WordPressRestApi.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | #import "WordPressBaseApi.h" 4 | 5 | typedef NS_ENUM(NSUInteger, WordPressRestApiError) { 6 | WordPressRestApiErrorJSON, 7 | WordPressRestApiErrorNoAccessToken, 8 | WordPressRestApiErrorLoginFailed, 9 | WordPressRestApiErrorInvalidToken, 10 | WordPressRestApiErrorAuthorizationRequired, 11 | }; 12 | 13 | extern NSString *const WordPressRestApiEndpointURL; 14 | extern NSString *const WordPressRestApiErrorDomain; 15 | extern NSString *const WordPressRestApiErrorCodeKey; 16 | 17 | @interface WordPressRestApi : NSObject 18 | 19 | + (void)signInWithOauthWithSuccess:(void (^)(NSString *authToken, NSString *siteId))success failure:(void (^)(NSError *error))failure; 20 | + (void)signInWithJetpackUsername:(NSString *)username password:(NSString *)password success:(void (^)(NSString *authToken))success failure:(void (^)(NSError *error))failure; 21 | 22 | - (id)initWithOauthToken:(NSString *)authToken siteId:(NSString *)siteId; 23 | 24 | /** 25 | Helper function for [UIApplicationDelegate application:handleOpenURL:] to process the authentication callback from the WordPress app 26 | 27 | @param url The url passed to [UIApplicationDelegate application:handleOpenURL:] 28 | @param success A block called if the url could be processed. The block has no return value and takes two arguments: the XML-RPC endpoint for the blog and the OAuth token. We highly recommend you store these in a secure place like the keychain. 29 | @returns YES if the url passed was a valid callback from authentication and it could be processed. Otherwise it returns NO. 30 | */ 31 | + (BOOL)handleOpenURL:(NSURL *)url; 32 | 33 | + (void)setWordPressComClient:(NSString *)clientId; 34 | + (void)setWordPressComSecret:(NSString *)secret; 35 | + (void)setWordPressComRedirectUrl:(NSString *)redirectUrl; 36 | 37 | @end 38 | -------------------------------------------------------------------------------- /Pod/WordPressRestApi.m: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | 4 | #import "WordPressRestApi.h" 5 | #import "WordPressRestApiJSONRequestOperation.h" 6 | #import "WordPressRestApiJSONRequestOperationManager.h" 7 | #import "WPComOAuthController.h" 8 | 9 | NSString *const WordPressRestApiEndpointURL = @"https://public-api.wordpress.com/rest/v1.1/"; 10 | NSString *const WordPressRestApiErrorDomain = @"WordPressRestApiError"; 11 | NSString *const WordPressRestApiErrorCodeKey = @"WordPressRestApiErrorCodeKey"; 12 | 13 | @implementation WordPressRestApi { 14 | NSString *_token; 15 | NSString *_siteId; 16 | AFHTTPRequestOperationManager *_operationManager; 17 | } 18 | 19 | static NSString *WordPressRestApiClient = nil; 20 | static NSString *WordPressRestApiSecret = nil; 21 | static NSString *WordPressRestApiRedirectUrl = nil; 22 | 23 | + (void)signInWithOauthWithSuccess:(void (^)(NSString *authToken, NSString *siteId))success failure:(void (^)(NSError *error))failure { 24 | [[WPComOAuthController sharedController] setCompletionBlock:^(NSString *token, NSString *blogId, NSString *blogUrl, NSString *scope, NSError *error) { 25 | if (error) { 26 | failure(error); 27 | } else { 28 | success(token, blogId); 29 | } 30 | }]; 31 | [[WPComOAuthController sharedController] present]; 32 | } 33 | 34 | + (void)signInWithJetpackUsername:(NSString *)username password:(NSString *)password success:(void (^)(NSString *authToken))success failure:(void (^)(NSError *error))failure { 35 | NSAssert(NO, @"Not implemented yet"); 36 | } 37 | 38 | - (id)initWithOauthToken:(NSString *)authToken siteId:(NSString *)siteId { 39 | self = [super init]; 40 | 41 | if (self) { 42 | NSURL* baseURL = [NSURL URLWithString:WordPressRestApiEndpointURL]; 43 | 44 | _token = authToken; 45 | _siteId = siteId; 46 | 47 | _operationManager = [[WordPressRestApiJSONRequestOperationManager alloc] initWithBaseURL:baseURL 48 | token:_token]; 49 | } 50 | 51 | return self; 52 | } 53 | 54 | + (BOOL)handleOpenURL:(NSURL *)url { 55 | return [[WPComOAuthController sharedController] handleOpenURL:url]; 56 | } 57 | 58 | + (void)setWordPressComClient:(NSString *)clientId { 59 | WordPressRestApiClient = clientId; 60 | [[WPComOAuthController sharedController] setClient:clientId]; 61 | } 62 | 63 | + (void)setWordPressComSecret:(NSString *)secret { 64 | WordPressRestApiSecret = secret; 65 | [[WPComOAuthController sharedController] setSecret:secret]; 66 | } 67 | 68 | + (void)setWordPressComRedirectUrl:(NSString *)redirectUrl { 69 | WordPressRestApiRedirectUrl = redirectUrl; 70 | [[WPComOAuthController sharedController] setRedirectUrl:redirectUrl]; 71 | } 72 | 73 | #pragma mark - WordPressBaseApi methods 74 | 75 | - (void)publishPostWithText:(NSString *)content title:(NSString *)title success:(void (^)(NSUInteger postId, NSURL *permalink))success failure:(void (^)(NSError *error))failure { 76 | NSDictionary *parameters = @{ 77 | @"title": title, 78 | @"content": content 79 | }; 80 | [_operationManager POST:[self sitePath:@"posts/new"] 81 | parameters:parameters 82 | success:^(AFHTTPRequestOperation *operation, id responseObject) 83 | { 84 | NSUInteger postId = [[responseObject objectForKey:@"ID"] unsignedIntegerValue]; 85 | NSURL *permalink = [NSURL URLWithString:[responseObject objectForKey:@"URL"]]; 86 | success(postId, permalink); 87 | } failure:^(AFHTTPRequestOperation *operation, NSError *error) { 88 | failure(error); 89 | }]; 90 | } 91 | 92 | - (void)publishPostWithImage:(UIImage *)image description:(NSString *)content title:(NSString *)title success:(void (^)(NSUInteger postId, NSURL *permalink))success failure:(void (^)(NSError *error))failure { 93 | if (image) { 94 | [self publishPostWithGallery:@[image] description:content title:title success:success failure:failure]; 95 | } else { 96 | [self publishPostWithText:content title:title success:success failure:failure]; 97 | } 98 | } 99 | 100 | - (void)publishPostWithGallery:(NSArray *)images description:(NSString *)content title:(NSString *)title success:(void (^)(NSUInteger postId, NSURL *permalink))success failure:(void (^)(NSError *error))failure { 101 | if (![images count]) { 102 | [self publishPostWithText:content title:title success:success failure:failure]; 103 | } 104 | 105 | void(^contructionBlock)(id) = ^(id formData) 106 | { 107 | [images enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) { 108 | NSData *imageData = UIImageJPEGRepresentation(obj, 1.f); 109 | [formData appendPartWithFileData:imageData name:@"media[]" 110 | fileName:[NSString stringWithFormat:@"image-%lu.jpg", (unsigned long)idx] 111 | mimeType:@"image/jpeg"]; 112 | }]; 113 | }; 114 | 115 | void(^successBlock)(AFHTTPRequestOperation* operation, id responseObject) = ^(AFHTTPRequestOperation *operation, 116 | id responseObject) 117 | { 118 | NSUInteger postId = [[responseObject objectForKey:@"ID"] unsignedIntegerValue]; 119 | NSURL *permalink = [NSURL URLWithString:[responseObject objectForKey:@"URL"]]; 120 | success(postId, permalink); 121 | }; 122 | 123 | void(^failureBlock)(AFHTTPRequestOperation *operation, NSError *error) = ^(AFHTTPRequestOperation *operation, 124 | NSError *error) 125 | { 126 | failure(error); 127 | }; 128 | 129 | NSDictionary *parameters = @{ 130 | @"title": title, 131 | @"content": content 132 | }; 133 | [_operationManager POST:[self sitePath:@"posts/new"] 134 | parameters:parameters 135 | constructingBodyWithBlock:contructionBlock 136 | success:successBlock 137 | failure:failureBlock]; 138 | } 139 | 140 | - (void)getPosts:(NSUInteger)count success:(void (^)(NSArray *posts))success failure:(void (^)(NSError *error))failure { 141 | [_operationManager GET:[self sitePath:@"posts"] 142 | parameters:nil 143 | success:^(AFHTTPRequestOperation *operation, id responseObject) 144 | { 145 | success([responseObject objectForKey:@"posts"]); 146 | } failure:^(AFHTTPRequestOperation *operation, NSError *error) { 147 | failure(error); 148 | }]; 149 | } 150 | 151 | #pragma mark - API Helpers 152 | 153 | - (NSString *)sitePath:(NSString *)path { 154 | return [NSString stringWithFormat:@"sites/%@/%@", _siteId, path]; 155 | } 156 | 157 | @end 158 | -------------------------------------------------------------------------------- /Pod/WordPressRestApiJSONRequestOperation.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | @interface WordPressRestApiJSONRequestOperation : AFHTTPRequestOperation 4 | 5 | @end 6 | -------------------------------------------------------------------------------- /Pod/WordPressRestApiJSONRequestOperation.m: -------------------------------------------------------------------------------- 1 | #import "WordPressRestApiJSONRequestOperation.h" 2 | #import "WordPressRestApi.h" 3 | 4 | @implementation WordPressRestApiJSONRequestOperation 5 | 6 | +(BOOL)canProcessRequest:(NSURLRequest *)urlRequest { 7 | NSURL *testURL = [NSURL URLWithString:WordPressRestApiEndpointURL]; 8 | if ([urlRequest.URL.host isEqualToString:testURL.host] && [urlRequest.URL.path rangeOfString:testURL.path].location == 0) 9 | return YES; 10 | 11 | return NO; 12 | } 13 | 14 | - (NSError *)error { 15 | if (self.response.statusCode >= 400) { 16 | NSString *errorMessage = [self.responseObject objectForKey:@"message"]; 17 | NSUInteger errorCode = WordPressRestApiErrorJSON; 18 | if ([self.responseObject objectForKey:@"error"] && errorMessage) { 19 | NSString *error = [self.responseObject objectForKey:@"error"]; 20 | if ([error isEqualToString:@"invalid_token"]) { 21 | errorCode = WordPressRestApiErrorInvalidToken; 22 | } else if ([error isEqualToString:@"authorization_required"]) { 23 | errorCode = WordPressRestApiErrorAuthorizationRequired; 24 | } 25 | return [NSError errorWithDomain:WordPressRestApiErrorDomain code:errorCode userInfo:@{NSLocalizedDescriptionKey: errorMessage, WordPressRestApiErrorCodeKey: error}]; 26 | } 27 | } 28 | return [super error]; 29 | } 30 | 31 | @end -------------------------------------------------------------------------------- /Pod/WordPressRestApiJSONRequestOperationManager.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | @interface WordPressRestApiJSONRequestOperationManager : AFHTTPRequestOperationManager 4 | 5 | /** 6 | * @brief Default initializer. 7 | */ 8 | - (id)initWithBaseURL:(NSURL *)url 9 | token:(NSString*)token; 10 | 11 | @end 12 | -------------------------------------------------------------------------------- /Pod/WordPressRestApiJSONRequestOperationManager.m: -------------------------------------------------------------------------------- 1 | #import "WordPressRestApiJSONRequestOperationManager.h" 2 | #import "WordPressRestApiJSONRequestOperation.h" 3 | 4 | @implementation WordPressRestApiJSONRequestOperationManager 5 | 6 | /** 7 | * @brief This method is not supported by this class. Use initWithBaseUrl:token: instead. 8 | */ 9 | - (id)initWithBaseURL:(NSURL *)url 10 | { 11 | [self doesNotRecognizeSelector:_cmd]; 12 | self = nil; 13 | return self; 14 | } 15 | 16 | - (id)initWithBaseURL:(NSURL *)url 17 | token:(NSString*)token 18 | { 19 | NSParameterAssert([url isKindOfClass:[NSURL class]]); 20 | NSParameterAssert([token isKindOfClass:[NSString class]]); 21 | 22 | self = [super initWithBaseURL:url]; 23 | 24 | if (self) 25 | { 26 | NSString* bearerString = [NSString stringWithFormat:@"Bearer %@", token]; 27 | 28 | [self.requestSerializer setValue:bearerString forHTTPHeaderField:@"Authorization"]; 29 | } 30 | 31 | return self; 32 | } 33 | 34 | - (AFHTTPRequestOperation *)HTTPRequestOperationWithRequest:(NSURLRequest *)request 35 | success:(void (^)(AFHTTPRequestOperation *operation, id responseObject))success 36 | failure:(void (^)(AFHTTPRequestOperation *operation, NSError *error))failure 37 | { 38 | WordPressRestApiJSONRequestOperation *operation = [[WordPressRestApiJSONRequestOperation alloc] initWithRequest:request]; 39 | 40 | operation.responseSerializer = [[AFJSONResponseSerializer alloc] init]; 41 | operation.shouldUseCredentialStorage = self.shouldUseCredentialStorage; 42 | operation.credential = self.credential; 43 | operation.securityPolicy = self.securityPolicy; 44 | 45 | [operation setCompletionBlockWithSuccess:success failure:failure]; 46 | 47 | return operation; 48 | } 49 | 50 | @end 51 | -------------------------------------------------------------------------------- /Pod/WordPressXMLRPCApi.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | #import "WordPressBaseApi.h" 4 | 5 | extern NSString *const WordPressXMLRPCApiErrorDomain; 6 | 7 | typedef NS_ENUM(NSInteger, WordPressXMLRPCApiError) { 8 | WordPressXMLRPCApiEmptyURL, // The URL provided was nil, empty or just whitespaces 9 | WordPressXMLRPCApiInvalidURL, // The URL provided was an invalid URL 10 | WordPressXMLRPCApiInvalidScheme, // The URL provided was an invalid scheme, only HTTP and HTTPS supported 11 | WordPressXMLRPCApiNotWordPressError, // That's a XML-RPC endpoint but doesn't look like WordPress 12 | WordPressXMLRPCApiMobilePluginRedirectedError, // There's some "mobile" plugin redirecting everything to their site 13 | WordPressXMLRPCApiInvalid, // Doesn't look to be valid XMLRPC Endpoint. 14 | }; 15 | 16 | /** 17 | WordPress API for iOS 18 | */ 19 | @interface WordPressXMLRPCApi : NSObject 20 | 21 | ///----------------------------------------- 22 | /// @name Accessing WordPress API properties 23 | ///----------------------------------------- 24 | 25 | @property (readonly, nonatomic, retain) NSURL *xmlrpc; 26 | @property (readonly, nonatomic, strong) NSOperationQueue *operationQueue; 27 | 28 | 29 | ///------------------------------------------------------- 30 | /// @name Creating and Initializing a WordPress API Client 31 | ///------------------------------------------------------- 32 | 33 | /** 34 | Creates and initializes a `WordPressAPI` client using password authentication. 35 | 36 | @param xmlrpc The XML-RPC endpoint URL, e.g.: https://en.blog.wordpress.com/xmlrpc.php 37 | @param username The user name 38 | @param password The password 39 | */ 40 | + (WordPressXMLRPCApi *)apiWithXMLRPCEndpoint:(NSURL *)xmlrpc username:(NSString *)username password:(NSString *)password; 41 | 42 | /** 43 | Initializes a `WordPressAPI` client using password authentication. 44 | 45 | @param xmlrpc The XML-RPC endpoint URL, e.g.: https://en.blog.wordpress.com/xmlrpc.php 46 | @param username The user name 47 | @param password The password 48 | */ 49 | - (id)initWithXMLRPCEndpoint:(NSURL *)xmlrpc username:(NSString *)username password:(NSString *)password; 50 | 51 | ///------------------- 52 | /// @name Authenticate 53 | ///------------------- 54 | 55 | /** 56 | Performs a XML-RPC test call just to verify that the credentials are correct. 57 | 58 | @param success A block object to execute when the credentials are valid. This block has no return value. 59 | @param failure A block object to execute when the credentials can't be verified. This block has no return value and takes one argument: a NSError object with details on the error. 60 | */ 61 | - (void)authenticateWithSuccess:(void (^)())success 62 | failure:(void (^)(NSError *error))failure; 63 | 64 | /** 65 | Authenticates and returns a list of the blogs which the user can access 66 | 67 | @param success A block object to execute when the login is successful. This block has no return value and takes one argument: an array with the blogs list. 68 | @param failure A block object to execute when the login failed. This block has no return value and takes one argument: a NSError object with details on the error. 69 | */ 70 | - (void)getBlogsWithSuccess:(void (^)(NSArray *blogs))success failure:(void (^)(NSError *error))failure; 71 | 72 | /** 73 | Authenticates and returns a dictionary of the blog's options. 74 | 75 | @param success A block object to execute when the login is successful. This block has no return value and takes one argument: a dictionary with the blog options. 76 | @param failure A block object to execute when the login failed. This block has no return value and takes one argument: a NSError object with details on the error. 77 | */ 78 | - (void)getBlogOptionsWithSuccess:(void (^)(id options))success failure:(void (^)(NSError *error))failure; 79 | 80 | ///-------------- 81 | /// @name Helpers 82 | ///-------------- 83 | 84 | /** 85 | Given a site URL, tries to guess the URL for the XML-RPC endpoint 86 | 87 | When asked for a site URL, sometimes users type the XML-RPC url, or the xmlrpc.php has been moved/renamed. This method would try a few methods to find the proper XML-RPC endpoint: 88 | 89 | * Try to load the given URL adding `/xmlrpc.php` at the end. This is the most common use case for proper site URLs 90 | * If that fails, try a test XML-RPC request given URL, maybe it was the XML-RPC URL already 91 | * If that fails, fetch the given URL and search for an `EditURI` link pointing to the XML-RPC endpoint 92 | 93 | For additional URL typo fixing, see [NSURL-Guess](https://github.com/koke/NSURL-Guess) 94 | 95 | @param url what the user entered as the URL, e.g.: myblog.com 96 | @param success A block object to execute when the method finds a suitable XML-RPC endpoint on the site provided. This block has no return value and takes two arguments: the original site URL, and the found XML-RPC endpoint URL. 97 | @param failure A block object to execute when the method doesn't find a suitable XML-RPC endpoint on the site. This block has no return value and takes one argument: a NSError object with details on the error. 98 | */ 99 | + (void)guessXMLRPCURLForSite:(NSString *)url 100 | success:(void (^)(NSURL *xmlrpcURL))success 101 | failure:(void (^)(NSError *error))failure; 102 | 103 | 104 | @end 105 | -------------------------------------------------------------------------------- /Pod/WordPressXMLRPCApi.m: -------------------------------------------------------------------------------- 1 | #import "WordPressXMLRPCApi.h" 2 | #import "WPXMLRPCClient.h" 3 | #import "WPRSDParser.h" 4 | 5 | NSString *const WordPressXMLRPCApiErrorDomain = @"WordPressXMLRPCApiError"; 6 | 7 | @interface WordPressXMLRPCApi () 8 | 9 | @property (readwrite, nonatomic, retain) NSURL *xmlrpc; 10 | @property (readwrite, nonatomic, retain) NSString *username; 11 | @property (readwrite, nonatomic, retain) NSString *password; 12 | @property (readwrite, nonatomic, retain) WPXMLRPCClient *client; 13 | 14 | @end 15 | 16 | @implementation WordPressXMLRPCApi 17 | 18 | + (WordPressXMLRPCApi *)apiWithXMLRPCEndpoint:(NSURL *)xmlrpc username:(NSString *)username password:(NSString *)password { 19 | return [[self alloc] initWithXMLRPCEndpoint:xmlrpc username:username password:password]; 20 | } 21 | 22 | - (id)initWithXMLRPCEndpoint:(NSURL *)xmlrpc username:(NSString *)username password:(NSString *)password 23 | { 24 | self = [super init]; 25 | if (!self) { 26 | return nil; 27 | } 28 | 29 | self.xmlrpc = xmlrpc; 30 | self.username = username; 31 | self.password = password; 32 | 33 | self.client = [WPXMLRPCClient clientWithXMLRPCEndpoint:xmlrpc]; 34 | 35 | return self; 36 | } 37 | 38 | - (NSOperationQueue *)operationQueue 39 | { 40 | return self.client.operationQueue; 41 | } 42 | 43 | 44 | #pragma mark - Authentication 45 | 46 | - (void)authenticateWithSuccess:(void (^)())success 47 | failure:(void (^)(NSError *error))failure { 48 | NSArray *parameters = [NSArray arrayWithObjects:self.username, self.password, nil]; 49 | [self.client callMethod:@"wp.getUsersBlogs" 50 | parameters:parameters 51 | success:^(AFHTTPRequestOperation *operation, id responseObject) { 52 | if (success) { 53 | success(); 54 | } 55 | } failure:^(AFHTTPRequestOperation *operation, NSError *error) { 56 | if (failure) { 57 | failure(error); 58 | } 59 | }]; 60 | } 61 | 62 | - (void)getBlogsWithSuccess:(void (^)(NSArray *blogs))success failure:(void (^)(NSError *error))failure { 63 | [self.client callMethod:@"wp.getUsersBlogs" 64 | parameters:[NSArray arrayWithObjects:self.username, self.password, nil] 65 | success:^(AFHTTPRequestOperation *operation, id responseObject) { 66 | if (success) { 67 | success(responseObject); 68 | } 69 | } failure:^(AFHTTPRequestOperation *operation, NSError *error) { 70 | if (failure) { 71 | failure(error); 72 | } 73 | }]; 74 | } 75 | 76 | - (void)getBlogOptionsWithSuccess:(void (^)(id options))success failure:(void (^)(NSError *error))failure 77 | { 78 | [self.client callMethod:@"wp.getOptions" 79 | parameters:[NSArray arrayWithObjects:@(1), self.username, self.password, nil] 80 | success:^(AFHTTPRequestOperation *operation, id responseObject) { 81 | if (success) { 82 | success(responseObject); 83 | } 84 | } failure:^(AFHTTPRequestOperation *operation, NSError *error) { 85 | if (failure) { 86 | failure(error); 87 | } 88 | }]; 89 | } 90 | 91 | #pragma mark - Publishing a post 92 | 93 | - (void)publishPostWithText:(NSString *)content title:(NSString *)title success:(void (^)(NSUInteger, NSURL *))success failure:(void (^)(NSError *))failure { 94 | NSDictionary *postParameters = [NSDictionary dictionaryWithObjectsAndKeys: 95 | title, @"post_title", 96 | content, @"post_content", 97 | @"publish", @"post_status", 98 | nil]; 99 | NSArray *parameters = [self buildParametersWithExtra:postParameters]; 100 | [self.client callMethod:@"wp.newPost" 101 | parameters:parameters 102 | success:^(AFHTTPRequestOperation *operation, id responseObject) { 103 | if (success) { 104 | success([responseObject intValue], nil); 105 | } 106 | } 107 | failure:^(AFHTTPRequestOperation *operation, NSError *error) { 108 | if (failure) { 109 | failure(error); 110 | } 111 | }]; 112 | } 113 | 114 | - (void)publishPostWithImage:(UIImage *)image 115 | description:(NSString *)content 116 | title:(NSString *)title 117 | success:(void (^)(NSUInteger postId, NSURL *permalink))success 118 | failure:(void (^)(NSError *error))failure { 119 | [self publishPostWithText:content title:title success:success failure:failure]; 120 | } 121 | 122 | - (void)publishPostWithGallery:(NSArray *)images 123 | description:(NSString *)content 124 | title:(NSString *)title 125 | success:(void (^)(NSUInteger postId, NSURL *permalink))success 126 | failure:(void (^)(NSError *error))failure { 127 | [self publishPostWithText:content title:title success:success failure:failure]; 128 | } 129 | 130 | - (void)publishPostWithVideo:(NSString *)videoPath 131 | description:(NSString *)content 132 | title:(NSString *)title 133 | success:(void (^)(NSUInteger postId, NSURL *permalink))success 134 | failure:(void (^)(NSError *error))failure { 135 | [self publishPostWithText:content title:title success:success failure:failure]; 136 | } 137 | 138 | #pragma mark - Managing posts 139 | 140 | - (void)getPosts:(NSUInteger)count 141 | success:(void (^)(NSArray *posts))success 142 | failure:(void (^)(NSError *error))failure { 143 | NSArray *parameters = [self buildParametersWithExtra:nil]; 144 | [self.client callMethod:@"metaWeblog.getRecentPosts" 145 | parameters:parameters 146 | success:^(AFHTTPRequestOperation *operation, id responseObject) { 147 | if (success) { 148 | success((NSArray *)responseObject); 149 | } 150 | } 151 | failure:^(AFHTTPRequestOperation *operation, NSError *error) { 152 | if (failure) { 153 | failure(error); 154 | } 155 | }]; 156 | } 157 | 158 | #pragma mark - Helpers 159 | 160 | + (NSURL *)urlForXMLRPCFromUrl:(NSString *)url addXMLRPC:(BOOL) addXMLRPC error:(NSError **)error 161 | { 162 | NSString *xmlrpc; 163 | // ------------------------------------------------ 164 | // Is an empty url? Sorry, no psychic powers yet 165 | // ------------------------------------------------ 166 | if (url == nil || [url stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]].length == 0) { 167 | *error = [NSError errorWithDomain:WordPressXMLRPCApiErrorDomain 168 | code:WordPressXMLRPCApiEmptyURL 169 | userInfo:@{NSLocalizedDescriptionKey:NSLocalizedString(@"Empty URL", @"")}]; 170 | return nil; 171 | } 172 | 173 | // ------------------------------------------------------------------------ 174 | // Check if it's a valid URL 175 | // Not a valid URL. Could be a bad protocol (htpp://), syntax error (http//), ... 176 | // See https://github.com/koke/NSURL-Guess for extra help cleaning user typed URLs 177 | // ------------------------------------------------------------------------ 178 | NSURL *baseURL = [NSURL URLWithString:url]; 179 | if (baseURL == nil) { 180 | *error = [NSError errorWithDomain:WordPressXMLRPCApiErrorDomain 181 | code:WordPressXMLRPCApiInvalidURL 182 | userInfo:@{NSLocalizedDescriptionKey: NSLocalizedString(@"Invalid URL", @"")}]; 183 | return nil; 184 | } 185 | // ------------------------------------------------------------------------ 186 | // Let's see if a scheme is provided and it's HTTP or HTTPS 187 | // ------------------------------------------------------------------------ 188 | NSString *scheme = [baseURL.scheme lowercaseString]; 189 | if (!scheme) { 190 | url = [NSString stringWithFormat:@"http://%@", url]; 191 | scheme = @"http"; 192 | } 193 | if (!([scheme isEqualToString:@"http"] || [scheme isEqualToString:@"https"])) { 194 | *error = [NSError errorWithDomain:WordPressXMLRPCApiErrorDomain 195 | code:WordPressXMLRPCApiInvalidScheme 196 | userInfo:@{NSLocalizedDescriptionKey:NSLocalizedString(@"Invalid URL scheme inserted, only HTTP and HTTPS are supported.", @"Message to explay to the user he should only use HTTP or HTTPS for is self-hosted WordPress sites")}]; 197 | return nil; 198 | } 199 | 200 | // ------------------------------------------------------------------------ 201 | // Assume the given url is the home page and XML-RPC sits at /xmlrpc.php 202 | // ------------------------------------------------------------------------ 203 | [self logExtraInfo: @"Assume the given url is the home page and XML-RPC sits at /xmlrpc.php" ]; 204 | if ([[baseURL lastPathComponent] isEqualToString:@"xmlrpc.php"] || !addXMLRPC) { 205 | xmlrpc = url; 206 | } else { 207 | xmlrpc = [NSString stringWithFormat:@"%@/xmlrpc.php", url]; 208 | } 209 | return [NSURL URLWithString:xmlrpc];; 210 | } 211 | 212 | + (void)guessXMLRPCURLForSite:(NSString *)url 213 | success:(void (^)(NSURL *xmlrpcURL))success 214 | failure:(void (^)(NSError *error))failure { 215 | NSError *error = nil; 216 | NSURL *xmlrpcURL = [self urlForXMLRPCFromUrl:url addXMLRPC:YES error:&error]; 217 | if (xmlrpcURL == nil) { 218 | [self logExtraInfo: [error localizedDescription]]; 219 | if (failure) { 220 | failure(error); 221 | } 222 | return; 223 | } 224 | [self logExtraInfo: @"Trying the following URL: %@", xmlrpcURL ]; 225 | [self validateXMLRPCUrl:xmlrpcURL success:^(NSURL *validatedXmlrpcURL){ 226 | if (success) { 227 | success(validatedXmlrpcURL); 228 | } 229 | } failure:^(NSError *error){ 230 | [self logError:error]; 231 | if (([error.domain isEqual:NSURLErrorDomain] && error.code == NSURLErrorUserCancelledAuthentication) 232 | || ([error.domain isEqual:WordPressXMLRPCApiErrorDomain] && error.code == WordPressXMLRPCApiMobilePluginRedirectedError)) { 233 | if (failure) { 234 | failure(error); 235 | } 236 | return; 237 | } 238 | // ------------------------------------------- 239 | // Try the original given url as an XML-RPC endpoint 240 | // ------------------------------------------- 241 | NSURL *originalXmlrpcURL = [self urlForXMLRPCFromUrl:url addXMLRPC:NO error:nil]; 242 | [self logExtraInfo: @"Try the given url as an XML-RPC endpoint: %@", originalXmlrpcURL]; 243 | [self validateXMLRPCUrl:originalXmlrpcURL success:^(NSURL *validatedXmlrpcURL){ 244 | if (success) { 245 | success(validatedXmlrpcURL); 246 | } 247 | } failure:^(NSError *error){ 248 | [self logError:error]; 249 | if ([error.domain isEqual:WordPressXMLRPCApiErrorDomain] && error.code == WordPressXMLRPCApiMobilePluginRedirectedError) { 250 | if (failure) { 251 | failure(error); 252 | } 253 | return; 254 | } 255 | // Fetch the original url and look for the RSD link 256 | [self guessXMLRPCURLFromHTMLURL:originalXmlrpcURL success:success failure:failure]; 257 | }]; 258 | }];} 259 | 260 | + (void)guessXMLRPCURLFromHTMLURL:(NSURL *)htmlURL 261 | success:(void (^)(NSURL *xmlrpcURL))success 262 | failure:(void (^)(NSError *error))failure { 263 | [self logExtraInfo:@"Fetch the original url and look for the RSD link by using RegExp"]; 264 | NSURLRequest *request = [NSURLRequest requestWithURL:htmlURL]; 265 | AFHTTPRequestOperation *operation = [[AFHTTPRequestOperation alloc] initWithRequest:request]; 266 | [operation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) { 267 | NSError *error = nil; 268 | NSString *responseString = operation.responseString; 269 | NSArray *matches = nil; 270 | if (responseString) { 271 | NSRegularExpression *rsdURLRegExp = [NSRegularExpression regularExpressionWithPattern:@"" options:NSRegularExpressionCaseInsensitive error:&error]; 272 | matches = [rsdURLRegExp matchesInString:responseString options:0 range:NSMakeRange(0, [responseString length])]; 273 | } 274 | NSString *rsdURL = nil; 275 | if ([matches count]) { 276 | NSRange rsdURLRange = [[matches objectAtIndex:0] rangeAtIndex:1]; 277 | if(rsdURLRange.location != NSNotFound) 278 | rsdURL = [responseString substringWithRange:rsdURLRange]; 279 | } 280 | 281 | if (rsdURL == nil) { 282 | if (failure) { 283 | if (error == nil) { 284 | error = [NSError errorWithDomain:WordPressXMLRPCApiErrorDomain 285 | code:WordPressXMLRPCApiInvalid 286 | userInfo:@{NSLocalizedDescriptionKey:NSLocalizedString(@"Cannot find a valid WordPress XMLRPC endpoint", @"Message to show when not valid WordPress XMLRPC endpoint is found on the URL provided")}]; 287 | } 288 | failure(error); 289 | } 290 | return; 291 | } 292 | // Try removing "?rsd" from the url, it should point to the XML-RPC endpoint 293 | NSString *xmlrpc = [rsdURL stringByReplacingOccurrencesOfString:@"?rsd" withString:@""]; 294 | if (![xmlrpc isEqualToString:rsdURL]) { 295 | NSURL *xmlrpcURL = [NSURL URLWithString:xmlrpc]; 296 | [self validateXMLRPCUrl:xmlrpcURL success:^(NSURL *validatedXmlrpcURL){ 297 | if (success) { 298 | success(validatedXmlrpcURL); 299 | } 300 | } failure:^(NSError *error){ 301 | [self guessXMLRPCURLFromRSD:rsdURL success:success failure:failure]; 302 | }]; 303 | } else { 304 | [self guessXMLRPCURLFromRSD:rsdURL success:success failure:failure]; 305 | } 306 | } failure:^(AFHTTPRequestOperation *operation, NSError *error) { 307 | [self logError:error]; 308 | if (failure) failure(error); 309 | }]; 310 | NSOperationQueue *queue = [[NSOperationQueue alloc] init]; 311 | [queue addOperation:operation]; 312 | } 313 | 314 | + (void)guessXMLRPCURLFromRSD:(NSString *)rsd 315 | success:(void (^)(NSURL *xmlrpcURL))success 316 | failure:(void (^)(NSError *error))failure { 317 | [self logExtraInfo:@"Parse the RSD document at the following URL: %@", rsd]; 318 | NSURL *rsdURL = [NSURL URLWithString:rsd]; 319 | NSURLRequest *request = [NSURLRequest requestWithURL:rsdURL]; 320 | AFHTTPRequestOperation *operation = [[AFHTTPRequestOperation alloc] initWithRequest:request]; 321 | [operation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) { 322 | NSError *error; 323 | WPRSDParser *parser = [[WPRSDParser alloc] initWithXmlString:operation.responseString]; 324 | NSString *parsedEndpoint = [parser parsedEndpointWithError:&error]; 325 | if (parsedEndpoint == nil) { 326 | if (failure) { 327 | failure(error); 328 | } 329 | return; 330 | } 331 | NSString *xmlrpc = parsedEndpoint; 332 | NSURL *xmlrpcURL = [NSURL URLWithString:xmlrpc]; 333 | [self logExtraInfo:@"Bingo! We found the WordPress XML-RPC element: %@", xmlrpcURL]; 334 | [self validateXMLRPCUrl:xmlrpcURL success:^(NSURL *validatedXmlrpcURL){ 335 | if (success) { 336 | success(validatedXmlrpcURL); 337 | } 338 | } failure:^(NSError *error){ 339 | [self logError:error]; 340 | if (failure) failure(error); 341 | }]; 342 | } failure:^(AFHTTPRequestOperation *operation, NSError *error) { 343 | [self logError:error]; 344 | if (failure) failure(error); 345 | }]; 346 | NSOperationQueue *queue = [[NSOperationQueue alloc] init]; 347 | [queue addOperation:operation]; 348 | } 349 | #pragma mark - Private Methods 350 | 351 | - (NSArray *)buildParametersWithExtra:(id)extra { 352 | NSMutableArray *result = [NSMutableArray array]; 353 | [result addObject:@"1"]; 354 | [result addObject:self.username]; 355 | [result addObject:self.password]; 356 | if ([extra isKindOfClass:[NSArray class]]) { 357 | [result addObjectsFromArray:extra]; 358 | } else if ([extra isKindOfClass:[NSDictionary class]]) { 359 | [result addObject:extra]; 360 | } 361 | 362 | return [NSArray arrayWithArray:result]; 363 | } 364 | 365 | + (void)validateXMLRPCUrl:(NSURL *)url success:(void (^)(NSURL *validatedXmlrpURL))success failure:(void (^)(NSError *error))failure { 366 | WPXMLRPCClient *client = [WPXMLRPCClient clientWithXMLRPCEndpoint:url]; 367 | NSURLRequest *request = [client requestWithMethod:@"system.listMethods" parameters:@[]]; 368 | __block BOOL isRedirected = NO; 369 | AFHTTPRequestOperation *operation = [client HTTPRequestOperationWithRequest:request success:^(AFHTTPRequestOperation *operation, id responseObject) { 370 | NSArray *methods = responseObject; 371 | if ([methods isKindOfClass:[NSArray class]] && [methods containsObject:@"wp.getUsersBlogs"]) { 372 | NSURL *xmlrpcURL = operation.response.URL; 373 | [self logExtraInfo:@"Found XML-RPC endpoint at %@", xmlrpcURL]; 374 | if (success) { 375 | success(xmlrpcURL); 376 | } 377 | } else { 378 | if (failure) { 379 | NSError *error = [NSError errorWithDomain:WordPressXMLRPCApiErrorDomain code:WordPressXMLRPCApiNotWordPressError userInfo:@{NSLocalizedDescriptionKey: NSLocalizedStringFromTable(@"That doesn't look like a WordPress site", @"WordPressApi", nil)}]; 380 | failure(error); 381 | } 382 | } 383 | } failure:^(AFHTTPRequestOperation *operation, NSError *error) { 384 | if (isRedirected) { 385 | if (operation.responseString != nil 386 | && ([operation.responseString rangeOfString:@""].location != NSNotFound 387 | || [operation.responseString rangeOfString:@"dm404Container"].location != NSNotFound)) { 388 | error = [NSError errorWithDomain:WordPressXMLRPCApiErrorDomain code:WordPressXMLRPCApiMobilePluginRedirectedError userInfo:@{NSLocalizedDescriptionKey: NSLocalizedStringFromTable(@"You seem to have installed a mobile plugin from DudaMobile which is preventing the app to connect to your blog", @"WordPressApi", nil)}]; 389 | } 390 | } 391 | if (failure) { 392 | failure(error); 393 | } 394 | }]; 395 | [operation setRedirectResponseBlock:^NSURLRequest *(NSURLConnection *connection, NSURLRequest *redirectRequest, NSURLResponse *redirectResponse) { 396 | isRedirected = YES; 397 | 398 | if (redirectResponse) { 399 | [self logExtraInfo:@"Redirected to %@", redirectRequest.URL]; 400 | NSMutableURLRequest *postRequest = postRequest = [client requestWithMethod:@"system.listMethods" parameters:@[]]; 401 | [postRequest setURL:redirectRequest.URL]; 402 | return postRequest; 403 | } 404 | 405 | return redirectRequest; 406 | }]; 407 | 408 | [client enqueueHTTPRequestOperation:operation]; 409 | } 410 | 411 | + (void)logExtraInfo:(NSString *)format, ... { 412 | BOOL extraDebugIsActive = NO; 413 | NSNumber *extra_debug = [[NSUserDefaults standardUserDefaults] objectForKey:@"extra_debug"]; 414 | if ([extra_debug boolValue]) { 415 | extraDebugIsActive = YES; 416 | } 417 | #ifdef DEBUG 418 | extraDebugIsActive = YES; 419 | #endif 420 | 421 | if( extraDebugIsActive == NO ) return; 422 | 423 | va_list ap; 424 | va_start(ap, format); 425 | NSString *message = [[NSString alloc] initWithFormat:format arguments:ap]; 426 | NSLog(@"[WordPressApi] < %@", message); 427 | } 428 | 429 | + (void)logError:(NSError *)error { 430 | [self logExtraInfo:@"Error: %@", error]; 431 | } 432 | 433 | @end 434 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/wordpress-mobile/WordPress-API-iOS.svg?branch=master)](https://travis-ci.org/wordpress-mobile/WordPress-API-iOS) 2 | 3 | # WordPress API for iOS 4 | 5 | WordPress API for iOS is a library for iOS designed to make sharing on your WordPress blog easy. 6 | 7 | It's not meant to provide access to the full feature set of the WordPress APIs. 8 | 9 | # Disclaimer 10 | 11 | **Warning:** This API is a work in progress, and much of the basic functionality is not implemented yet. 12 | 13 | # Installation 14 | 15 | WordPress API uses [CocoaPods](http://cocoapods.org/) for easy 16 | dependency management. To install 17 | it, simply add the following line to your Podfile: 18 | 19 | pod 'WordPressApi' 20 | 21 | Another option, if you don't use CocoaPods, is to copy the `WordPressApi` 22 | folder to your project. 23 | 24 | # Example usage 25 | 26 | ## Posting a picture 27 | 28 | A hypothetical camera app called Cameramattic wants to add an option to share its pictures on WordPress 29 | 30 | NSURL *xmlrpcURL = [NSURL URLWithString:@"https://aphotoblog.wordpress.com"]; 31 | NSString *username = "aUsername"; 32 | NSString *password = "thePassword"; 33 | NSString *title = "My cat"; 34 | NSString *content = "She likes to sleep like that"; 35 | UIImage *image = ... // The image to upload 36 | 37 | WordPressXMLRPCApi *wp = [[WordPressXMLRPCApi alloc] initWithXMLRPCEndpoint:xmlrpcURL username:username password:password]; 38 | [wp publishPostWithImage:(UIImage *)image 39 | description:(NSString *)content 40 | title:(NSString *)title 41 | success:^(NSUInteger postId, NSURL *permalink) { 42 | NSLog(@"Image post successful with ID %d at %@", postId, permalink); 43 | } 44 | failure:^(NSError *error) { 45 | NSLog(@"Post upload failed: %@", [error localizedDescription]) 46 | }]; 47 | -------------------------------------------------------------------------------- /WordPressApi.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = "WordPressApi" 3 | s.version = "0.4.0" 4 | s.summary = "A simple Objective-C client to publish posts on the WordPress platform" 5 | s.homepage = "https://github.com/wordpress-mobile/WordPressApi" 6 | s.license = { :type => 'GPLv2', :file => 'LICENSE.md' } 7 | s.author = "WordPress" 8 | s.source = { :git => "https://github.com/wordpress-mobile/WordPressApi.git", :tag => s.version.to_s } 9 | s.source_files = 'Pod' 10 | s.requires_arc = true 11 | s.dependency 'AFNetworking', '~> 2.6.0' 12 | s.dependency 'wpxmlrpc', '~> 0.8' 13 | 14 | s.platform = :ios, '8.0' 15 | s.ios.deployment_target = '8.0' 16 | s.public_header_files = "Pod/**/*.h" 17 | s.frameworks = 'Foundation', 'UIKit', 'Security' 18 | end 19 | --------------------------------------------------------------------------------