001/* 002 * Copyright (c) 2010-2021 Mark Allen, Norbert Bartels. 003 * 004 * Permission is hereby granted, free of charge, to any person obtaining a copy 005 * of this software and associated documentation files (the "Software"), to deal 006 * in the Software without restriction, including without limitation the rights 007 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 008 * copies of the Software, and to permit persons to whom the Software is 009 * furnished to do so, subject to the following conditions: 010 * 011 * The above copyright notice and this permission notice shall be included in 012 * all copies or substantial portions of the Software. 013 * 014 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 015 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 016 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 017 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 018 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 019 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 020 * THE SOFTWARE. 021 */ 022package com.restfb; 023 024import static com.restfb.logging.RestFBLogger.CLIENT_LOGGER; 025import static com.restfb.util.EncodingUtils.decodeBase64; 026import static com.restfb.util.ObjectUtil.verifyParameterPresence; 027import static com.restfb.util.StringUtils.*; 028import static com.restfb.util.UrlUtils.urlEncode; 029import static java.lang.String.format; 030import static java.net.HttpURLConnection.*; 031import static java.util.Arrays.asList; 032import static java.util.Collections.emptyList; 033 034import java.io.IOException; 035import java.util.*; 036import java.util.stream.Collectors; 037import java.util.stream.Stream; 038 039import javax.crypto.Mac; 040import javax.crypto.spec.SecretKeySpec; 041 042import com.restfb.WebRequestor.Response; 043import com.restfb.batch.BatchRequest; 044import com.restfb.batch.BatchResponse; 045import com.restfb.exception.*; 046import com.restfb.exception.devicetoken.*; 047import com.restfb.exception.generator.DefaultFacebookExceptionGenerator; 048import com.restfb.exception.generator.FacebookExceptionGenerator; 049import com.restfb.json.*; 050import com.restfb.scope.ScopeBuilder; 051import com.restfb.types.DeviceCode; 052import com.restfb.util.EncodingUtils; 053import com.restfb.util.ObjectUtil; 054import com.restfb.util.StringUtils; 055 056/** 057 * Default implementation of a <a href="http://developers.facebook.com/docs/api">Facebook Graph API</a> client. 058 * 059 * @author <a href="http://restfb.com">Mark Allen</a> 060 */ 061public class DefaultFacebookClient extends BaseFacebookClient implements FacebookClient { 062 public static final String CLIENT_ID = "client_id"; 063 public static final String APP_ID = "appId"; 064 public static final String APP_SECRET = "appSecret"; 065 public static final String SCOPE = "scope"; 066 public static final String CANNOT_EXTRACT_ACCESS_TOKEN_MESSAGE = "Unable to extract access token from response."; 067 public static final String PARAM_CLIENT_SECRET = "client_secret"; 068 /** 069 * Graph API access token. 070 */ 071 protected String accessToken; 072 073 /** 074 * Graph API app secret. 075 */ 076 private String appSecret; 077 078 /** 079 * facebook exception generator to convert Facebook error json into java exceptions 080 */ 081 private FacebookExceptionGenerator graphFacebookExceptionGenerator; 082 083 /** 084 * holds the Facebook endpoint urls 085 */ 086 private FacebookEndpoints facebookEndpointUrls = new FacebookEndpoints() {}; 087 088 /** 089 * Reserved "multiple IDs" parameter name. 090 */ 091 protected static final String IDS_PARAM_NAME = "ids"; 092 093 /** 094 * Version of API endpoint. 095 */ 096 protected Version apiVersion; 097 098 /** 099 * By default this is <code>false</code>, so real http DELETE is used 100 */ 101 protected boolean httpDeleteFallback; 102 103 protected boolean accessTokenInHeader; 104 105 protected DefaultFacebookClient() { 106 this(Version.LATEST); 107 } 108 109 /** 110 * Creates a Facebook Graph API client with the given {@code apiVersion}. 111 * 112 * @param apiVersion 113 * Version of the api endpoint 114 */ 115 public DefaultFacebookClient(Version apiVersion) { 116 this(null, null, new DefaultWebRequestor(), new DefaultJsonMapper(), apiVersion); 117 } 118 119 /** 120 * Creates a Facebook Graph API client with the given {@code accessToken}. 121 * 122 * @param accessToken 123 * A Facebook OAuth access token. 124 * @param apiVersion 125 * Version of the api endpoint 126 * @since 1.6.14 127 */ 128 public DefaultFacebookClient(String accessToken, Version apiVersion) { 129 this(accessToken, null, new DefaultWebRequestor(), new DefaultJsonMapper(), apiVersion); 130 } 131 132 /** 133 * Creates a Facebook Graph API client with the given {@code accessToken}. 134 * 135 * @param accessToken 136 * A Facebook OAuth access token. 137 * @param appSecret 138 * A Facebook application secret. 139 * @param apiVersion 140 * Version of the api endpoint 141 * @since 1.6.14 142 */ 143 public DefaultFacebookClient(String accessToken, String appSecret, Version apiVersion) { 144 this(accessToken, appSecret, new DefaultWebRequestor(), new DefaultJsonMapper(), apiVersion); 145 } 146 147 /** 148 * Creates a Facebook Graph API client with the given {@code accessToken}. 149 * 150 * @param accessToken 151 * A Facebook OAuth access token. 152 * @param webRequestor 153 * The {@link WebRequestor} implementation to use for sending requests to the API endpoint. 154 * @param jsonMapper 155 * The {@link JsonMapper} implementation to use for mapping API response JSON to Java objects. 156 * @param apiVersion 157 * Version of the api endpoint 158 * @throws NullPointerException 159 * If {@code jsonMapper} or {@code webRequestor} is {@code null}. 160 * @since 1.6.14 161 */ 162 public DefaultFacebookClient(String accessToken, WebRequestor webRequestor, JsonMapper jsonMapper, 163 Version apiVersion) { 164 this(accessToken, null, webRequestor, jsonMapper, apiVersion); 165 } 166 167 /** 168 * Creates a Facebook Graph API client with the given {@code accessToken}, {@code webRequestor}, and 169 * {@code jsonMapper}. 170 * 171 * @param accessToken 172 * A Facebook OAuth access token. 173 * @param appSecret 174 * A Facebook application secret. 175 * @param webRequestor 176 * The {@link WebRequestor} implementation to use for sending requests to the API endpoint. 177 * @param jsonMapper 178 * The {@link JsonMapper} implementation to use for mapping API response JSON to Java objects. 179 * @param apiVersion 180 * Version of the api endpoint 181 * @throws NullPointerException 182 * If {@code jsonMapper} or {@code webRequestor} is {@code null}. 183 */ 184 public DefaultFacebookClient(String accessToken, String appSecret, WebRequestor webRequestor, JsonMapper jsonMapper, 185 Version apiVersion) { 186 super(); 187 188 verifyParameterPresence("jsonMapper", jsonMapper); 189 verifyParameterPresence("webRequestor", webRequestor); 190 191 this.accessToken = trimToNull(accessToken); 192 this.appSecret = trimToNull(appSecret); 193 194 this.webRequestor = webRequestor; 195 this.jsonMapper = jsonMapper; 196 this.jsonMapper.setFacebookClient(this); 197 this.apiVersion = Optional.ofNullable(apiVersion).orElse(Version.UNVERSIONED); 198 graphFacebookExceptionGenerator = new DefaultFacebookExceptionGenerator(); 199 } 200 201 /** 202 * Switch between access token in header and access token in query parameters (default) 203 * 204 * @param accessTokenInHttpHeader 205 * <code>true</code> use access token as header field, <code>false</code> use access token as query parameter 206 * (default) 207 */ 208 public void setHeaderAuthorization(boolean accessTokenInHttpHeader) { 209 this.accessTokenInHeader = accessTokenInHttpHeader; 210 } 211 212 /** 213 * override the default facebook exception generator to provide a custom handling for the facebook error objects 214 * 215 * @param exceptionGenerator 216 * the custom exception generator implementing the {@link FacebookExceptionGenerator} interface 217 */ 218 public void setFacebookExceptionGenerator(FacebookExceptionGenerator exceptionGenerator) { 219 graphFacebookExceptionGenerator = exceptionGenerator; 220 } 221 222 /** 223 * fetch the current facebook exception generator implementing the {@link FacebookExceptionGenerator} interface 224 * 225 * @return the current facebook exception generator 226 */ 227 public FacebookExceptionGenerator getFacebookExceptionGenerator() { 228 return graphFacebookExceptionGenerator; 229 } 230 231 @Override 232 public boolean deleteObject(String object, Parameter... parameters) { 233 verifyParameterPresence("object", object); 234 235 String responseString = makeRequest(object, true, true, null, parameters); 236 237 try { 238 JsonValue jObj = Json.parse(responseString); 239 boolean success = false; 240 if (jObj.isObject()) { 241 if (jObj.asObject().contains("success")) { 242 success = jObj.asObject().get("success").asBoolean(); 243 } 244 if (jObj.asObject().contains("result")) { 245 success = jObj.asObject().get("result").asString().contains("Successfully deleted"); 246 } 247 } else { 248 success = jObj.asBoolean(); 249 } 250 return success; 251 } catch (ParseException jex) { 252 CLIENT_LOGGER.trace("no valid JSON returned while deleting a object, using returned String instead", jex); 253 return "true".equals(responseString); 254 } 255 } 256 257 /** 258 * @see com.restfb.FacebookClient#fetchConnection(java.lang.String, java.lang.Class, com.restfb.Parameter[]) 259 */ 260 @Override 261 public <T> Connection<T> fetchConnection(String connection, Class<T> connectionType, Parameter... parameters) { 262 verifyParameterPresence("connection", connection); 263 verifyParameterPresence("connectionType", connectionType); 264 return new Connection<>(this, makeRequest(connection, parameters), connectionType); 265 } 266 267 /** 268 * @see com.restfb.FacebookClient#fetchConnectionPage(java.lang.String, java.lang.Class) 269 */ 270 @Override 271 public <T> Connection<T> fetchConnectionPage(final String connectionPageUrl, Class<T> connectionType) { 272 String connectionJson; 273 if (!isBlank(accessToken) && !isBlank(appSecret)) { 274 connectionJson = makeRequestAndProcessResponse(() -> webRequestor.executeGet(String.format("%s&%s=%s", 275 connectionPageUrl, urlEncode(APP_SECRET_PROOF_PARAM_NAME), obtainAppSecretProof(accessToken, appSecret)))); 276 } else { 277 connectionJson = makeRequestAndProcessResponse(() -> webRequestor.executeGet(connectionPageUrl, getHeaderAccessToken())); 278 } 279 280 return new Connection<>(this, connectionJson, connectionType); 281 } 282 283 /** 284 * @see com.restfb.FacebookClient#fetchObject(java.lang.String, java.lang.Class, com.restfb.Parameter[]) 285 */ 286 @Override 287 public <T> T fetchObject(String object, Class<T> objectType, Parameter... parameters) { 288 verifyParameterPresence("object", object); 289 verifyParameterPresence("objectType", objectType); 290 return jsonMapper.toJavaObject(makeRequest(object, parameters), objectType); 291 } 292 293 @Override 294 public FacebookClient createClientWithAccessToken(String accessToken) { 295 return new DefaultFacebookClient(accessToken, this.appSecret, this.apiVersion); 296 } 297 298 /** 299 * @see com.restfb.FacebookClient#fetchObjects(java.util.List, java.lang.Class, com.restfb.Parameter[]) 300 */ 301 @Override 302 @SuppressWarnings("unchecked") 303 public <T> T fetchObjects(List<String> ids, Class<T> objectType, Parameter... parameters) { 304 verifyParameterPresence("ids", ids); 305 verifyParameterPresence("connectionType", objectType); 306 307 if (ids.isEmpty()) { 308 throw new IllegalArgumentException("The list of IDs cannot be empty."); 309 } 310 311 if (Stream.of(parameters).anyMatch(p -> IDS_PARAM_NAME.equals(p.name))) { 312 throw new IllegalArgumentException("You cannot specify the '" + IDS_PARAM_NAME + "' URL parameter yourself - " 313 + "RestFB will populate this for you with the list of IDs you passed to this method."); 314 } 315 316 JsonArray idArray = new JsonArray(); 317 318 // Normalize the IDs 319 for (String id : ids) { 320 throwIAEonBlankId(id); 321 idArray.add(id.trim()); 322 } 323 324 try { 325 String jsonString = makeRequest("", 326 parametersWithAdditionalParameter(Parameter.with(IDS_PARAM_NAME, idArray.toString()), parameters)); 327 328 return jsonMapper.toJavaObject(jsonString, objectType); 329 } catch (ParseException e) { 330 throw new FacebookJsonMappingException("Unable to map connection JSON to Java objects", e); 331 } 332 } 333 334 private void throwIAEonBlankId(String id) { 335 if (StringUtils.isBlank(id)) { 336 throw new IllegalArgumentException("The list of IDs cannot contain blank strings."); 337 } 338 } 339 340 /** 341 * @see com.restfb.FacebookClient#publish(java.lang.String, java.lang.Class, com.restfb.BinaryAttachment, 342 * com.restfb.Parameter[]) 343 */ 344 @Override 345 public <T> T publish(String connection, Class<T> objectType, List<BinaryAttachment> binaryAttachments, 346 Parameter... parameters) { 347 verifyParameterPresence("connection", connection); 348 349 return jsonMapper.toJavaObject(makeRequest(connection, true, false, binaryAttachments, parameters), objectType); 350 } 351 352 /** 353 * @see com.restfb.FacebookClient#publish(java.lang.String, java.lang.Class, com.restfb.BinaryAttachment, 354 * com.restfb.Parameter[]) 355 */ 356 @Override 357 public <T> T publish(String connection, Class<T> objectType, BinaryAttachment binaryAttachment, 358 Parameter... parameters) { 359 List<BinaryAttachment> attachments = 360 Optional.ofNullable(binaryAttachment).map(Collections::singletonList).orElse(null); 361 return publish(connection, objectType, attachments, parameters); 362 } 363 364 /** 365 * @see com.restfb.FacebookClient#publish(java.lang.String, java.lang.Class, com.restfb.Parameter[]) 366 */ 367 @Override 368 public <T> T publish(String connection, Class<T> objectType, Parameter... parameters) { 369 return publish(connection, objectType, (List<BinaryAttachment>) null, parameters); 370 } 371 372 @Override 373 public String getLogoutUrl(String next) { 374 String parameterString; 375 if (next != null) { 376 Parameter p = Parameter.with("next", next); 377 parameterString = toParameterString(false, p); 378 } else { 379 parameterString = toParameterString(false); 380 } 381 382 final String fullEndPoint = createEndpointForApiCall("logout.php", false); 383 return fullEndPoint + "?" + parameterString; 384 } 385 386 /** 387 * @see com.restfb.FacebookClient#executeBatch(com.restfb.batch.BatchRequest[]) 388 */ 389 @Override 390 public List<BatchResponse> executeBatch(BatchRequest... batchRequests) { 391 return executeBatch(asList(batchRequests), Collections.emptyList()); 392 } 393 394 /** 395 * @see com.restfb.FacebookClient#executeBatch(java.util.List) 396 */ 397 @Override 398 public List<BatchResponse> executeBatch(List<BatchRequest> batchRequests) { 399 return executeBatch(batchRequests, Collections.emptyList()); 400 } 401 402 /** 403 * @see com.restfb.FacebookClient#executeBatch(java.util.List, java.util.List) 404 */ 405 @Override 406 public List<BatchResponse> executeBatch(List<BatchRequest> batchRequests, List<BinaryAttachment> binaryAttachments) { 407 verifyParameterPresence("binaryAttachments", binaryAttachments); 408 409 if (batchRequests == null || batchRequests.isEmpty()) { 410 throw new IllegalArgumentException("You must specify at least one batch request."); 411 } 412 413 return jsonMapper.toJavaList( 414 makeRequest("", true, false, binaryAttachments, Parameter.with("batch", jsonMapper.toJson(batchRequests, true))), 415 BatchResponse.class); 416 } 417 418 /** 419 * @see com.restfb.FacebookClient#convertSessionKeysToAccessTokens(java.lang.String, java.lang.String, 420 * java.lang.String[]) 421 */ 422 @Override 423 public List<AccessToken> convertSessionKeysToAccessTokens(String appId, String secretKey, String... sessionKeys) { 424 verifyParameterPresence(APP_ID, appId); 425 verifyParameterPresence("secretKey", secretKey); 426 427 if (sessionKeys == null || sessionKeys.length == 0) { 428 return emptyList(); 429 } 430 431 String json = makeRequest("/oauth/exchange_sessions", true, false, null, Parameter.with(CLIENT_ID, appId), 432 Parameter.with(PARAM_CLIENT_SECRET, secretKey), Parameter.with("sessions", String.join(",", sessionKeys))); 433 434 return jsonMapper.toJavaList(json, AccessToken.class); 435 } 436 437 /** 438 * @see com.restfb.FacebookClient#obtainAppAccessToken(java.lang.String, java.lang.String) 439 */ 440 @Override 441 public AccessToken obtainAppAccessToken(String appId, String appSecret) { 442 verifyParameterPresence(APP_ID, appId); 443 verifyParameterPresence(APP_SECRET, appSecret); 444 445 String response = makeRequest("oauth/access_token", Parameter.with("grant_type", "client_credentials"), 446 Parameter.with(CLIENT_ID, appId), Parameter.with(PARAM_CLIENT_SECRET, appSecret)); 447 448 try { 449 return getAccessTokenFromResponse(response); 450 } catch (Exception t) { 451 throw new FacebookResponseContentException(CANNOT_EXTRACT_ACCESS_TOKEN_MESSAGE, t); 452 } 453 } 454 455 @Override 456 public DeviceCode fetchDeviceCode(ScopeBuilder scope) { 457 verifyParameterPresence(SCOPE, scope); 458 ObjectUtil.requireNotNull(accessToken, 459 () -> new IllegalStateException("access token is required to fetch a device access token")); 460 461 String response = makeRequest("device/login", true, false, null, Parameter.with("type", "device_code"), 462 Parameter.with(SCOPE, scope.toString())); 463 return jsonMapper.toJavaObject(response, DeviceCode.class); 464 } 465 466 @Override 467 public AccessToken obtainDeviceAccessToken(String code) throws FacebookDeviceTokenCodeExpiredException, 468 FacebookDeviceTokenPendingException, FacebookDeviceTokenDeclinedException, FacebookDeviceTokenSlowdownException { 469 verifyParameterPresence("code", code); 470 471 ObjectUtil.requireNotNull(accessToken, 472 () -> new IllegalStateException("access token is required to fetch a device access token")); 473 474 try { 475 String response = makeRequest("device/login_status", true, false, null, Parameter.with("type", "device_token"), 476 Parameter.with("code", code)); 477 return getAccessTokenFromResponse(response); 478 } catch (FacebookOAuthException foae) { 479 DeviceTokenExceptionFactory.createFrom(foae); 480 return null; 481 } 482 } 483 484 /** 485 * @see com.restfb.FacebookClient#obtainUserAccessToken(java.lang.String, java.lang.String, java.lang.String, 486 * java.lang.String) 487 */ 488 @Override 489 public AccessToken obtainUserAccessToken(String appId, String appSecret, String redirectUri, 490 String verificationCode) { 491 verifyParameterPresence(APP_ID, appId); 492 verifyParameterPresence(APP_SECRET, appSecret); 493 verifyParameterPresence("verificationCode", verificationCode); 494 495 String response = makeRequest("oauth/access_token", Parameter.with(CLIENT_ID, appId), 496 Parameter.with(PARAM_CLIENT_SECRET, appSecret), Parameter.with("code", verificationCode), 497 Parameter.with("redirect_uri", redirectUri)); 498 499 try { 500 return getAccessTokenFromResponse(response); 501 } catch (Exception t) { 502 throw new FacebookResponseContentException(CANNOT_EXTRACT_ACCESS_TOKEN_MESSAGE, t); 503 } 504 } 505 506 /** 507 * @see com.restfb.FacebookClient#obtainExtendedAccessToken(java.lang.String, java.lang.String) 508 */ 509 @Override 510 public AccessToken obtainExtendedAccessToken(String appId, String appSecret) { 511 ObjectUtil.requireNotNull(accessToken, 512 () -> new IllegalStateException( 513 format("You cannot call this method because you did not construct this instance of %s with an access token.", 514 getClass().getSimpleName()))); 515 516 return obtainExtendedAccessToken(appId, appSecret, accessToken); 517 } 518 519 /** 520 * @see com.restfb.FacebookClient#obtainExtendedAccessToken(java.lang.String, java.lang.String, java.lang.String) 521 */ 522 @Override 523 public AccessToken obtainExtendedAccessToken(String appId, String appSecret, String accessToken) { 524 verifyParameterPresence(APP_ID, appId); 525 verifyParameterPresence(APP_SECRET, appSecret); 526 verifyParameterPresence("accessToken", accessToken); 527 528 String response = makeRequest("/oauth/access_token", false, false, null, Parameter.with(CLIENT_ID, appId), 529 Parameter.with(PARAM_CLIENT_SECRET, appSecret), Parameter.with("grant_type", "fb_exchange_token"), 530 Parameter.with("fb_exchange_token", accessToken)); 531 532 try { 533 return getAccessTokenFromResponse(response); 534 } catch (Exception t) { 535 throw new FacebookResponseContentException(CANNOT_EXTRACT_ACCESS_TOKEN_MESSAGE, t); 536 } 537 } 538 539 private AccessToken getAccessTokenFromResponse(String response) { 540 AccessToken token; 541 try { 542 token = getJsonMapper().toJavaObject(response, AccessToken.class); 543 } catch (FacebookJsonMappingException fjme) { 544 CLIENT_LOGGER.trace("could not map response to access token class try to fetch directly from String", fjme); 545 token = AccessToken.fromQueryString(response); 546 } 547 token.setClient(createClientWithAccessToken(token.getAccessToken())); 548 return token; 549 } 550 551 @Override 552 @SuppressWarnings("unchecked") 553 public <T> T parseSignedRequest(String signedRequest, String appSecret, Class<T> objectType) { 554 verifyParameterPresence("signedRequest", signedRequest); 555 verifyParameterPresence(APP_SECRET, appSecret); 556 verifyParameterPresence("objectType", objectType); 557 558 String[] signedRequestTokens = signedRequest.split("[.]"); 559 560 if (signedRequestTokens.length != 2) { 561 throw new FacebookSignedRequestParsingException(format( 562 "Signed request '%s' is expected to be signature and payload strings separated by a '.'", signedRequest)); 563 } 564 565 String encodedSignature = signedRequestTokens[0]; 566 String urlDecodedSignature = urlDecodeSignedRequestToken(encodedSignature); 567 byte[] signature = decodeBase64(urlDecodedSignature); 568 569 String encodedPayload = signedRequestTokens[1]; 570 String urlDecodedPayload = urlDecodeSignedRequestToken(encodedPayload); 571 String payload = StringUtils.toString(decodeBase64(urlDecodedPayload)); 572 573 // Convert payload to a JsonObject so we can pull algorithm data out of it 574 JsonObject payloadObject = getJsonMapper().toJavaObject(payload, JsonObject.class); 575 576 if (!payloadObject.contains("algorithm")) { 577 throw new FacebookSignedRequestParsingException("Unable to detect algorithm used for signed request"); 578 } 579 580 String algorithm = payloadObject.getString("algorithm", null); 581 582 if (!verifySignedRequest(appSecret, algorithm, encodedPayload, signature)) { 583 throw new FacebookSignedRequestVerificationException( 584 "Signed request verification failed. Are you sure the request was made for the app identified by the app secret you provided?"); 585 } 586 587 // Marshal to the user's preferred type. 588 // If the user asked for a JsonObject, send back the one we already parsed. 589 return objectType.equals(JsonObject.class) ? (T) payloadObject : getJsonMapper().toJavaObject(payload, objectType); 590 } 591 592 /** 593 * Decodes a component of a signed request received from Facebook using FB's special URL-encoding strategy. 594 * 595 * @param signedRequestToken 596 * Token to decode. 597 * @return The decoded token. 598 */ 599 protected String urlDecodeSignedRequestToken(String signedRequestToken) { 600 verifyParameterPresence("signedRequestToken", signedRequestToken); 601 return signedRequestToken.replace("-", "+").replace("_", "/").trim(); 602 } 603 604 @Override 605 public String getLoginDialogUrl(String appId, String redirectUri, ScopeBuilder scope, Parameter... parameters) { 606 verifyParameterPresence(APP_ID, appId); 607 verifyParameterPresence("redirectUri", redirectUri); 608 verifyParameterPresence(SCOPE, scope); 609 610 String dialogUrl = getFacebookEndpointUrls().getFacebookEndpoint() + "/dialog/oauth"; 611 612 List<Parameter> parameterList = new ArrayList<>(); 613 parameterList.add(Parameter.with(CLIENT_ID, appId)); 614 parameterList.add(Parameter.with("redirect_uri", redirectUri)); 615 parameterList.add(Parameter.with(SCOPE, scope.toString())); 616 617 // add optional parameters 618 Collections.addAll(parameterList, parameters); 619 620 return dialogUrl + "?" + toParameterString(false, parameterList.toArray(new Parameter[0])); 621 } 622 623 /** 624 * Verifies that the signed request is really from Facebook. 625 * 626 * @param appSecret 627 * The secret for the app that can verify this signed request. 628 * @param algorithm 629 * Signature algorithm specified by FB in the decoded payload. 630 * @param encodedPayload 631 * The encoded payload used to generate a signature for comparison against the provided {@code signature}. 632 * @param signature 633 * The decoded signature extracted from the signed request. Compared against a signature generated from 634 * {@code encodedPayload}. 635 * @return {@code true} if the signed request is verified, {@code false} if not. 636 */ 637 protected boolean verifySignedRequest(String appSecret, String algorithm, String encodedPayload, byte[] signature) { 638 verifyParameterPresence(APP_SECRET, appSecret); 639 verifyParameterPresence("algorithm", algorithm); 640 verifyParameterPresence("encodedPayload", encodedPayload); 641 verifyParameterPresence("signature", signature); 642 643 // Normalize algorithm name...FB calls it differently than Java does 644 if ("HMAC-SHA256".equalsIgnoreCase(algorithm)) { 645 algorithm = "HMACSHA256"; 646 } 647 648 try { 649 Mac mac = Mac.getInstance(algorithm); 650 mac.init(new SecretKeySpec(toBytes(appSecret), algorithm)); 651 byte[] payloadSignature = mac.doFinal(toBytes(encodedPayload)); 652 return Arrays.equals(signature, payloadSignature); 653 } catch (Exception e) { 654 throw new FacebookSignedRequestVerificationException("Unable to perform signed request verification", e); 655 } 656 } 657 658 /** 659 * @see com.restfb.FacebookClient#debugToken(java.lang.String) 660 */ 661 @Override 662 public DebugTokenInfo debugToken(String inputToken) { 663 verifyParameterPresence("inputToken", inputToken); 664 String response = makeRequest("/debug_token", Parameter.with("input_token", inputToken)); 665 666 try { 667 JsonObject json = Json.parse(response).asObject(); 668 JsonObject data = json.get("data").asObject(); 669 return getJsonMapper().toJavaObject(data.toString(), DebugTokenInfo.class); 670 } catch (Exception t) { 671 throw new FacebookResponseContentException("Unable to parse JSON from response.", t); 672 } 673 } 674 675 /** 676 * @see com.restfb.FacebookClient#getJsonMapper() 677 */ 678 @Override 679 public JsonMapper getJsonMapper() { 680 return jsonMapper; 681 } 682 683 /** 684 * @see com.restfb.FacebookClient#getWebRequestor() 685 */ 686 @Override 687 public WebRequestor getWebRequestor() { 688 return webRequestor; 689 } 690 691 /** 692 * Coordinates the process of executing the API request GET/POST and processing the response we receive from the 693 * endpoint. 694 * 695 * @param endpoint 696 * Facebook Graph API endpoint. 697 * @param parameters 698 * Arbitrary number of parameters to send along to Facebook as part of the API call. 699 * @return The JSON returned by Facebook for the API call. 700 * @throws FacebookException 701 * If an error occurs while making the Facebook API POST or processing the response. 702 */ 703 protected String makeRequest(String endpoint, Parameter... parameters) { 704 return makeRequest(endpoint, false, false, null, parameters); 705 } 706 707 /** 708 * Coordinates the process of executing the API request GET/POST and processing the response we receive from the 709 * endpoint. 710 * 711 * @param endpoint 712 * Facebook Graph API endpoint. 713 * @param executeAsPost 714 * {@code true} to execute the web request as a {@code POST}, {@code false} to execute as a {@code GET}. 715 * @param executeAsDelete 716 * {@code true} to add a special 'treat this request as a {@code DELETE}' parameter. 717 * @param binaryAttachments 718 * A list of binary files to include in a {@code POST} request. Pass {@code null} if no attachment should be 719 * sent. 720 * @param parameters 721 * Arbitrary number of parameters to send along to Facebook as part of the API call. 722 * @return The JSON returned by Facebook for the API call. 723 * @throws FacebookException 724 * If an error occurs while making the Facebook API POST or processing the response. 725 */ 726 protected String makeRequest(String endpoint, final boolean executeAsPost, final boolean executeAsDelete, 727 final List<BinaryAttachment> binaryAttachments, Parameter... parameters) { 728 verifyParameterLegality(parameters); 729 730 if (executeAsDelete && isHttpDeleteFallback()) { 731 parameters = parametersWithAdditionalParameter(Parameter.with(METHOD_PARAM_NAME, "delete"), parameters); 732 } 733 734 if (!endpoint.startsWith("/")) { 735 endpoint = "/" + endpoint; 736 } 737 738 final String fullEndpoint = 739 createEndpointForApiCall(endpoint, binaryAttachments != null && !binaryAttachments.isEmpty()); 740 final String parameterString = toParameterString(parameters); 741 742 return makeRequestAndProcessResponse(() -> { 743 if (executeAsDelete && !isHttpDeleteFallback()) { 744 return webRequestor.executeDelete(fullEndpoint + "?" + parameterString, getHeaderAccessToken()); 745 } 746 747 if (executeAsPost) { 748 return webRequestor.executePost(fullEndpoint, parameterString, binaryAttachments, getHeaderAccessToken()); 749 } 750 751 return webRequestor.executeGet(fullEndpoint + "?" + parameterString, getHeaderAccessToken()); 752 }); 753 } 754 755 private String getHeaderAccessToken() { 756 if (accessTokenInHeader) { 757 return this.accessToken; 758 } 759 760 return null; 761 } 762 763 /** 764 * @see com.restfb.FacebookClient#obtainAppSecretProof(java.lang.String, java.lang.String) 765 */ 766 @Override 767 public String obtainAppSecretProof(String accessToken, String appSecret) { 768 verifyParameterPresence("accessToken", accessToken); 769 verifyParameterPresence(APP_SECRET, appSecret); 770 return EncodingUtils.encodeAppSecretProof(appSecret, accessToken); 771 } 772 773 /** 774 * returns if the fallback post method (<code>true</code>) is used or the http delete (<code>false</code>) 775 * 776 * @return {@code true} if POST is used instead of HTTP DELETE (default) 777 */ 778 public boolean isHttpDeleteFallback() { 779 return httpDeleteFallback; 780 } 781 782 /** 783 * Set to <code>true</code> if the facebook http delete fallback should be used. Facebook allows to use the http POST 784 * with the parameter "method=delete" to override the post and use delete instead. This feature allow http client that 785 * do not support the whole http method set, to delete objects from facebook 786 * 787 * @param httpDeleteFallback 788 * <code>true</code> if the the http Delete Fallback is used 789 */ 790 public void setHttpDeleteFallback(boolean httpDeleteFallback) { 791 this.httpDeleteFallback = httpDeleteFallback; 792 } 793 794 protected interface Requestor { 795 Response makeRequest() throws IOException; 796 } 797 798 protected String makeRequestAndProcessResponse(Requestor requestor) { 799 Response response; 800 801 // Perform a GET or POST to the API endpoint 802 try { 803 response = requestor.makeRequest(); 804 } catch (Exception t) { 805 throw new FacebookNetworkException(t); 806 } 807 808 // If we get any HTTP response code other than a 200 OK or 400 Bad Request 809 // or 401 Not Authorized or 403 Forbidden or 404 Not Found or 500 Internal 810 // Server Error or 302 Not Modified 811 // throw an exception. 812 if (HTTP_OK != response.getStatusCode() && HTTP_BAD_REQUEST != response.getStatusCode() 813 && HTTP_UNAUTHORIZED != response.getStatusCode() && HTTP_NOT_FOUND != response.getStatusCode() 814 && HTTP_INTERNAL_ERROR != response.getStatusCode() && HTTP_FORBIDDEN != response.getStatusCode() 815 && HTTP_NOT_MODIFIED != response.getStatusCode()) { 816 throw new FacebookNetworkException(response.getStatusCode()); 817 } 818 819 String json = response.getBody(); 820 821 try { 822 // If the response contained an error code, throw an exception. 823 getFacebookExceptionGenerator().throwFacebookResponseStatusExceptionIfNecessary(json, response.getStatusCode()); 824 } catch (FacebookErrorMessageException feme) { 825 Optional.ofNullable(getWebRequestor()).map(WebRequestor::getDebugHeaderInfo).ifPresent(feme::setDebugHeaderInfo); 826 throw feme; 827 } 828 829 // If there was no response error information and this was a 500 or 401 830 // error, something weird happened on Facebook's end. Bail. 831 if (HTTP_INTERNAL_ERROR == response.getStatusCode() || HTTP_UNAUTHORIZED == response.getStatusCode()) { 832 throw new FacebookNetworkException(response.getStatusCode()); 833 } 834 835 return json; 836 } 837 838 /** 839 * Generate the parameter string to be included in the Facebook API request. 840 * 841 * @param parameters 842 * Arbitrary number of extra parameters to include in the request. 843 * @return The parameter string to include in the Facebook API request. 844 * @throws FacebookJsonMappingException 845 * If an error occurs when building the parameter string. 846 */ 847 protected String toParameterString(Parameter... parameters) { 848 return toParameterString(true, parameters); 849 } 850 851 /** 852 * Generate the parameter string to be included in the Facebook API request. 853 * 854 * @param withJsonParameter 855 * add additional parameter format with type json 856 * @param parameters 857 * Arbitrary number of extra parameters to include in the request. 858 * @return The parameter string to include in the Facebook API request. 859 * @throws FacebookJsonMappingException 860 * If an error occurs when building the parameter string. 861 */ 862 protected String toParameterString(boolean withJsonParameter, Parameter... parameters) { 863 if (!isBlank(accessToken) && !accessTokenInHeader) { 864 parameters = parametersWithAdditionalParameter(Parameter.with(ACCESS_TOKEN_PARAM_NAME, accessToken), parameters); 865 } 866 867 if (!isBlank(accessToken) && !isBlank(appSecret)) { 868 parameters = parametersWithAdditionalParameter( 869 Parameter.with(APP_SECRET_PROOF_PARAM_NAME, obtainAppSecretProof(accessToken, appSecret)), parameters); 870 } 871 872 if (withJsonParameter) { 873 parameters = parametersWithAdditionalParameter(Parameter.with(FORMAT_PARAM_NAME, "json"), parameters); 874 } 875 876 return Stream.of(parameters).map(p -> urlEncode(p.name) + "=" + urlEncodedValueForParameterName(p.name, p.value)) 877 .collect(Collectors.joining("&")); 878 } 879 880 /** 881 * @see com.restfb.BaseFacebookClient#createEndpointForApiCall(java.lang.String,boolean) 882 */ 883 @Override 884 protected String createEndpointForApiCall(String apiCall, boolean hasAttachment) { 885 while (apiCall.startsWith("/")) 886 apiCall = apiCall.substring(1); 887 888 String baseUrl = getFacebookGraphEndpointUrl(); 889 890 if (hasAttachment && (apiCall.endsWith("/videos") || apiCall.endsWith("/advideos"))) { 891 baseUrl = getFacebookGraphVideoEndpointUrl(); 892 } else if (apiCall.endsWith("logout.php")) { 893 baseUrl = getFacebookEndpointUrls().getFacebookEndpoint(); 894 } 895 896 return format("%s/%s", baseUrl, apiCall); 897 } 898 899 /** 900 * Returns the base endpoint URL for the Graph API. 901 * 902 * @return The base endpoint URL for the Graph API. 903 */ 904 protected String getFacebookGraphEndpointUrl() { 905 if (apiVersion.isUrlElementRequired()) { 906 return getFacebookEndpointUrls().getGraphEndpoint() + '/' + apiVersion.getUrlElement(); 907 } else { 908 return getFacebookEndpointUrls().getGraphEndpoint(); 909 } 910 } 911 912 /** 913 * Returns the base endpoint URL for the Graph API's video upload functionality. 914 * 915 * @return The base endpoint URL for the Graph API's video upload functionality. 916 * @since 1.6.5 917 */ 918 protected String getFacebookGraphVideoEndpointUrl() { 919 if (apiVersion.isUrlElementRequired()) { 920 return getFacebookEndpointUrls().getGraphVideoEndpoint() + '/' + apiVersion.getUrlElement(); 921 } else { 922 return getFacebookEndpointUrls().getGraphVideoEndpoint(); 923 } 924 } 925 926 public FacebookEndpoints getFacebookEndpointUrls() { 927 return facebookEndpointUrls; 928 } 929 930 public void setFacebookEndpointUrls(FacebookEndpoints facebookEndpointUrls) { 931 this.facebookEndpointUrls = facebookEndpointUrls; 932 } 933}