001/* 002 * Copyright (c) 2010-2026 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.requireNotEmpty; 027import static com.restfb.util.ObjectUtil.verifyParameterPresence; 028import static com.restfb.util.StringUtils.*; 029import static com.restfb.util.UrlUtils.urlEncode; 030import static java.lang.String.format; 031import static java.net.HttpURLConnection.*; 032import static java.util.Arrays.asList; 033import static java.util.Collections.emptyList; 034 035import java.io.IOException; 036import java.time.Duration; 037import java.util.*; 038import java.util.function.Supplier; 039import java.util.stream.Collectors; 040import java.util.stream.Stream; 041 042import javax.crypto.Mac; 043import javax.crypto.spec.SecretKeySpec; 044 045import com.restfb.WebRequestor.Response; 046import com.restfb.batch.BatchRequest; 047import com.restfb.batch.BatchResponse; 048import com.restfb.exception.*; 049import com.restfb.exception.devicetoken.*; 050import com.restfb.exception.generator.DefaultFacebookExceptionGenerator; 051import com.restfb.exception.generator.FacebookExceptionGenerator; 052import com.restfb.json.*; 053import com.restfb.scope.ScopeBuilder; 054import com.restfb.types.DebugTokenInfo; 055import com.restfb.types.DeviceCode; 056import com.restfb.util.EncodingUtils; 057import com.restfb.util.ObjectUtil; 058import com.restfb.util.StringUtils; 059 060/** 061 * Default implementation of a <a href="http://developers.facebook.com/docs/api">Facebook Graph API</a> client. 062 * 063 * @author <a href="http://restfb.com">Mark Allen</a> 064 */ 065public class DefaultFacebookClient extends BaseFacebookClient implements FacebookClient { 066 public static final String CLIENT_ID = "client_id"; 067 public static final String APP_ID = "appId"; 068 public static final String APP_SECRET = "appSecret"; 069 public static final String SCOPE = "scope"; 070 public static final String CANNOT_EXTRACT_ACCESS_TOKEN_MESSAGE = "Unable to extract access token from response."; 071 public static final String PARAM_CLIENT_SECRET = "client_secret"; 072 073 public static final String CONNECTION = "connection"; 074 public static final String CONNECTION_TYPE = "connectionType"; 075 public static final String ALGORITHM = "algorithm"; 076 public static final String PATH_OAUTH_ACCESS_TOKEN = "oauth/access_token"; 077 public static final String REDIRECT_URI = "redirect_uri"; 078 public static final String GRANT_TYPE = "grant_type"; 079 public static final String CODE = "code"; 080 /** 081 * Graph API access token. 082 */ 083 protected String accessToken; 084 085 /** 086 * Graph API app secret. 087 */ 088 protected String appSecret; 089 090 /** 091 * facebook exception generator to convert Facebook error json into java exceptions 092 */ 093 private FacebookExceptionGenerator graphFacebookExceptionGenerator; 094 095 /** 096 * holds the Facebook endpoint urls 097 */ 098 private FacebookEndpoints facebookEndpointUrls = new FacebookEndpoints() {}; 099 100 /** 101 * Reserved "multiple IDs" parameter name. 102 */ 103 protected static final String IDS_PARAM_NAME = "ids"; 104 105 private static final int MAX_BATCH_SIZE = 50; 106 107 /** 108 * Version of API endpoint. 109 */ 110 protected Version apiVersion; 111 112 /** 113 * By default, this is <code>false</code>, so real http DELETE is used 114 */ 115 protected boolean httpDeleteFallback; 116 117 protected boolean accessTokenInHeader; 118 119 protected DefaultFacebookClient() { 120 this(Version.LATEST); 121 } 122 123 /** 124 * Creates a Facebook Graph API client with the given {@code apiVersion}. 125 * 126 * @param apiVersion 127 * Version of the api endpoint 128 */ 129 public DefaultFacebookClient(Version apiVersion) { 130 this(null, null, new DefaultWebRequestor(), new DefaultJsonMapper(), apiVersion); 131 } 132 133 /** 134 * Creates a Facebook Graph API client with the given {@code accessToken}. 135 * 136 * @param accessToken 137 * A Facebook OAuth access token. 138 * @param apiVersion 139 * Version of the api endpoint 140 * @since 1.6.14 141 */ 142 public DefaultFacebookClient(String accessToken, Version apiVersion) { 143 this(accessToken, null, new DefaultWebRequestor(), new DefaultJsonMapper(), apiVersion); 144 } 145 146 /** 147 * Creates a Facebook Graph API client with the given {@code accessToken}. 148 * 149 * @param accessToken 150 * A Facebook OAuth access token. 151 * @param appSecret 152 * A Facebook application secret. 153 * @param apiVersion 154 * Version of the api endpoint 155 * @since 1.6.14 156 */ 157 public DefaultFacebookClient(String accessToken, String appSecret, Version apiVersion) { 158 this(accessToken, appSecret, new DefaultWebRequestor(), new DefaultJsonMapper(), apiVersion); 159 } 160 161 /** 162 * Creates a Facebook Graph API client with the given {@code accessToken}. 163 * 164 * @param accessToken 165 * A Facebook OAuth access token. 166 * @param webRequestor 167 * The {@link WebRequestor} implementation to use for sending requests to the API endpoint. 168 * @param jsonMapper 169 * The {@link JsonMapper} implementation to use for mapping API response JSON to Java objects. 170 * @param apiVersion 171 * Version of the api endpoint 172 * @throws NullPointerException 173 * If {@code jsonMapper} or {@code webRequestor} is {@code null}. 174 * @since 1.6.14 175 */ 176 public DefaultFacebookClient(String accessToken, WebRequestor webRequestor, JsonMapper jsonMapper, 177 Version apiVersion) { 178 this(accessToken, null, webRequestor, jsonMapper, apiVersion); 179 } 180 181 /** 182 * Creates a Facebook Graph API client with the given {@code accessToken}, {@code webRequestor}, and 183 * {@code jsonMapper}. 184 * 185 * @param accessToken 186 * A Facebook OAuth access token. 187 * @param appSecret 188 * A Facebook application secret. 189 * @param webRequestor 190 * The {@link WebRequestor} implementation to use for sending requests to the API endpoint. 191 * @param jsonMapper 192 * The {@link JsonMapper} implementation to use for mapping API response JSON to Java objects. 193 * @param apiVersion 194 * Version of the api endpoint 195 * @throws NullPointerException 196 * If {@code jsonMapper} or {@code webRequestor} is {@code null}. 197 */ 198 public DefaultFacebookClient(String accessToken, String appSecret, WebRequestor webRequestor, JsonMapper jsonMapper, 199 Version apiVersion) { 200 super(); 201 202 verifyParameterPresence("jsonMapper", jsonMapper); 203 verifyParameterPresence("webRequestor", webRequestor); 204 205 this.accessToken = trimToNull(accessToken); 206 this.appSecret = trimToNull(appSecret); 207 208 this.webRequestor = webRequestor; 209 this.jsonMapper = jsonMapper; 210 this.jsonMapper.setFacebookClient(this); 211 this.apiVersion = Optional.ofNullable(apiVersion).orElse(Version.UNVERSIONED); 212 graphFacebookExceptionGenerator = new DefaultFacebookExceptionGenerator(); 213 } 214 215 /** 216 * Switch between access token in header and access token in query parameters (default) 217 * 218 * @param accessTokenInHttpHeader 219 * <code>true</code> use access token as header field, <code>false</code> use access token as query parameter 220 * (default) 221 */ 222 public void setHeaderAuthorization(boolean accessTokenInHttpHeader) { 223 this.accessTokenInHeader = accessTokenInHttpHeader; 224 } 225 226 /** 227 * override the default facebook exception generator to provide a custom handling for the facebook error objects 228 * 229 * @param exceptionGenerator 230 * the custom exception generator implementing the {@link FacebookExceptionGenerator} interface 231 */ 232 public void setFacebookExceptionGenerator(FacebookExceptionGenerator exceptionGenerator) { 233 graphFacebookExceptionGenerator = exceptionGenerator; 234 } 235 236 /** 237 * fetch the current facebook exception generator implementing the {@link FacebookExceptionGenerator} interface 238 * 239 * @return the current facebook exception generator 240 */ 241 public FacebookExceptionGenerator getFacebookExceptionGenerator() { 242 return graphFacebookExceptionGenerator; 243 } 244 245 @Override 246 public boolean deleteObject(String object, Parameter... parameters) { 247 return deleteObjectWithResult(object, parameters).getResult(); 248 } 249 250 @Override 251 public ApiResult<Boolean> deleteObjectWithResult(String object, Parameter... parameters) { 252 verifyParameterPresence("object", object); 253 254 RequestExecutionResult executionResult = makeRequestWithMetadata(object, true, true, null, parameters); 255 Response response = executionResult.getResponse(); 256 String responseString = response.getBody(); 257 258 try { 259 JsonValue jObj = Json.parse(responseString); 260 boolean success = false; 261 if (jObj.isObject()) { 262 if (jObj.asObject().contains("success")) { 263 success = jObj.asObject().get("success").asBoolean(); 264 } 265 if (jObj.asObject().contains("result")) { 266 success = jObj.asObject().get("result").asString().contains("Successfully deleted"); 267 } 268 } else { 269 success = jObj.asBoolean(); 270 } 271 return toApiResult(success, executionResult); 272 } catch (ParseException jex) { 273 CLIENT_LOGGER.trace("no valid JSON returned while deleting a object, using returned String instead", jex); 274 boolean fallbackResult = "true".equals(responseString); 275 return toApiResult(fallbackResult, executionResult); 276 } 277 } 278 279 /** 280 * @see com.restfb.FacebookClient#fetchConnection(java.lang.String, java.lang.Class, com.restfb.Parameter[]) 281 */ 282 @Override 283 public <T> Connection<T> fetchConnection(String connection, Class<T> connectionType, Parameter... parameters) { 284 verifyParameterPresence(CONNECTION, connection); 285 verifyParameterPresence(CONNECTION_TYPE, connectionType); 286 RequestExecutionResult executionResult = makeRequestWithMetadata(connection, parameters); 287 Response response = executionResult.getResponse(); 288 Connection<T> connectionResult = createConnection(response.getBody(), connectionType); 289 connectionResult.setResponseMetadata(toResponseMetadata(executionResult)); 290 return connectionResult; 291 } 292 293 /** 294 * @see com.restfb.FacebookClient#fetchConnectionPage(java.lang.String, java.lang.Class) 295 */ 296 @Override 297 public <T> Connection<T> fetchConnectionPage(final String connectionPageUrl, Class<T> connectionType) { 298 verifyParameterPresence("connectionPageUrl", connectionPageUrl); 299 verifyParameterPresence(CONNECTION_TYPE, connectionType); 300 301 RequestExecutionResult executionResult = fetchConnectionPageResponse(connectionPageUrl); 302 String connectionJson = executionResult.getResponse().getBody(); 303 Connection<T> connection = createConnection(connectionJson, connectionType); 304 connection.setResponseMetadata(toResponseMetadata(executionResult)); 305 return connection; 306 } 307 308 private RequestExecutionResult fetchConnectionPageResponse(String connectionPageUrl) { 309 WebRequestor.Request request; 310 if (!isBlank(accessToken) && !isBlank(appSecret)) { 311 if (isAppSecretProofWithTime()) { 312 long now = System.currentTimeMillis() / 1000; 313 request = 314 new WebRequestor.Request( 315 String.format("%s&%s=%s&%s=%s", connectionPageUrl, urlEncode(APP_SECRET_PROOF_TIME_PARAM_NAME), now, 316 urlEncode(APP_SECRET_PROOF_PARAM_NAME), obtainAppSecretProof(accessToken + "|" + now, appSecret)), 317 null); 318 } else { 319 request = new WebRequestor.Request(String.format("%s&%s=%s", connectionPageUrl, 320 urlEncode(APP_SECRET_PROOF_PARAM_NAME), obtainAppSecretProof(accessToken, appSecret)), null); 321 } 322 } else { 323 request = new WebRequestor.Request(connectionPageUrl, getHeaderAccessToken()); 324 } 325 326 return executeGetRequestWithInfo(request); 327 } 328 329 /** 330 * @see com.restfb.FacebookClient#fetchObject(java.lang.String, java.lang.Class, com.restfb.Parameter[]) 331 */ 332 @Override 333 public <T> T fetchObject(String object, Class<T> objectType, Parameter... parameters) { 334 return fetchObjectWithResult(object, objectType, parameters).getResult(); 335 } 336 337 @Override 338 public <T> ApiResult<T> fetchObjectWithResult(String object, Class<T> objectType, Parameter... parameters) { 339 verifyParameterPresence("object", object); 340 verifyParameterPresence("objectType", objectType); 341 RequestExecutionResult executionResult = makeRequestWithMetadata(object, parameters); 342 T mapped = jsonMapper.toJavaObject(executionResult.getResponse().getBody(), objectType); 343 return toApiResult(mapped, executionResult); 344 } 345 346 @Override 347 public FacebookClient createClientWithAccessToken(String accessToken) { 348 return new DefaultFacebookClient(accessToken, this.appSecret, getWebRequestor(), getJsonMapper(), this.apiVersion); 349 } 350 351 /** 352 * @see com.restfb.FacebookClient#fetchObjects(java.util.List, java.lang.Class, com.restfb.Parameter[]) 353 */ 354 @Override 355 public <T> T fetchObjects(List<String> ids, Class<T> objectType, Parameter... parameters) { 356 return fetchObjectsWithResult(ids, objectType, parameters).getResult(); 357 } 358 359 @Override 360 public <T> ApiResult<T> fetchObjectsWithResult(List<String> ids, Class<T> objectType, Parameter... parameters) { 361 verifyParameterPresence("ids", ids); 362 verifyParameterPresence(CONNECTION_TYPE, objectType); 363 requireNotEmpty(ids, "The list of IDs cannot be empty."); 364 365 if (ids.size() > MAX_BATCH_SIZE) { 366 throw new IllegalArgumentException("The list of IDs cannot contain more than " + MAX_BATCH_SIZE + " entries."); 367 } 368 369 if (Stream.of(parameters).anyMatch(p -> IDS_PARAM_NAME.equals(p.name))) { 370 throw new IllegalArgumentException("You cannot specify the '" + IDS_PARAM_NAME + "' URL parameter yourself - " 371 + "the list of IDs passed to this method determines the requested objects."); 372 } 373 374 List<String> normalizedIds = new ArrayList<>(ids.size()); 375 List<BatchRequest> batchRequests = new ArrayList<>(ids.size()); 376 377 // Normalize the IDs 378 for (String id : ids) { 379 throwIAEonBlankId(id); 380 String normalizedId = id.trim(); 381 normalizedIds.add(normalizedId); 382 if (normalizedId.regionMatches(true, 0, "http://", 0, 7) 383 || normalizedId.regionMatches(true, 0, "https://", 0, 8)) { 384 Parameter[] urlParameters = parametersWithAdditionalParameter(Parameter.with("id", normalizedId), parameters); 385 batchRequests.add(new BatchRequest.BatchRequestBuilder("").parameters(urlParameters).build()); 386 } else { 387 batchRequests.add(new BatchRequest.BatchRequestBuilder(normalizedId).parameters(parameters).build()); 388 } 389 } 390 391 try { 392 RequestExecutionResult executionResult = makeRequestWithMetadata("", true, false, emptyList(), 393 Parameter.with("batch", jsonMapper.toJson(batchRequests, true))); 394 List<BatchResponse> batchResponses = 395 jsonMapper.toJavaList(executionResult.getResponse().getBody(), BatchResponse.class); 396 397 if (batchResponses.size() != normalizedIds.size()) { 398 throw new FacebookJsonMappingException("The number of batch responses does not match the number of IDs."); 399 } 400 401 JsonObject combinedResponse = new JsonObject(); 402 for (int i = 0; i < batchResponses.size(); i++) { 403 BatchResponse batchResponse = batchResponses.get(i); 404 if (batchResponse == null || batchResponse.getCode() == null || batchResponse.getBody() == null) { 405 throw new FacebookJsonMappingException("Facebook returned an invalid batch response."); 406 } 407 408 try { 409 getFacebookExceptionGenerator().throwFacebookResponseStatusExceptionIfNecessary(batchResponse.getBody(), 410 batchResponse.getCode()); 411 } catch (FacebookGraphException e) { 412 if (Integer.valueOf(100).equals(e.getErrorCode()) && "GraphMethodException".equals(e.getErrorType())) { 413 continue; 414 } 415 Optional.ofNullable(executionResult.getResponse().getDebugHeaderInfo()).ifPresent(e::setDebugHeaderInfo); 416 throw e; 417 } 418 419 if (batchResponse.getCode() >= HTTP_OK && batchResponse.getCode() < 300) { 420 combinedResponse.set(normalizedIds.get(i), Json.parse(batchResponse.getBody())); 421 continue; 422 } 423 424 throw new FacebookNetworkException(batchResponse.getCode()); 425 } 426 427 T mapped = jsonMapper.toJavaObject(combinedResponse.toString(), objectType); 428 return toApiResult(mapped, executionResult); 429 } catch (ParseException e) { 430 throw new FacebookJsonMappingException("Unable to map batch response JSON to Java objects", e); 431 } 432 } 433 434 private void throwIAEonBlankId(String id) { 435 if (StringUtils.isBlank(id)) { 436 throw new IllegalArgumentException("The list of IDs cannot contain blank strings."); 437 } 438 } 439 440 private <T> ApiResult<T> toApiResult(T result, RequestExecutionResult executionResult) { 441 return ApiResult.withMetadata(result, toResponseMetadata(executionResult)); 442 } 443 444 private ResponseMetadata toResponseMetadata(RequestExecutionResult executionResult) { 445 if (executionResult == null) { 446 return null; 447 } 448 Response response = executionResult.getResponse(); 449 DebugHeaderInfo debugHeaderInfo = Optional.ofNullable(response).map(Response::getDebugHeaderInfo).orElse(null); 450 Map<String, List<String>> headers = Optional.ofNullable(response).map(Response::getHeaders).orElse(null); 451 Duration duration = Optional.ofNullable(executionResult).map(RequestExecutionResult::getDuration).orElse(null); 452 String httpMethod = Optional.ofNullable(executionResult).map(RequestExecutionResult::getHttpMethod).orElse(null); 453 String requestUrl = Optional.ofNullable(executionResult).map(RequestExecutionResult::getRequestUrl).orElse(null); 454 return ResponseMetadata.of(debugHeaderInfo, headers, duration, httpMethod, requestUrl); 455 } 456 457 /** 458 * @see com.restfb.FacebookClient#publish(java.lang.String, java.lang.Class, com.restfb.BinaryAttachment, 459 * com.restfb.Parameter[]) 460 */ 461 @Override 462 public <T> T publish(String connection, Class<T> objectType, List<BinaryAttachment> binaryAttachments, 463 Parameter... parameters) { 464 return publishWithResult(connection, objectType, binaryAttachments, parameters).getResult(); 465 } 466 467 @Override 468 public <T> ApiResult<T> publishWithResult(String connection, Class<T> objectType, 469 List<BinaryAttachment> binaryAttachments, Parameter... parameters) { 470 verifyParameterPresence(CONNECTION, connection); 471 RequestExecutionResult executionResult = 472 makeRequestWithMetadata(connection, true, false, binaryAttachments, parameters); 473 T mapped = jsonMapper.toJavaObject(executionResult.getResponse().getBody(), objectType); 474 return toApiResult(mapped, executionResult); 475 } 476 477 /** 478 * @see com.restfb.FacebookClient#publish(java.lang.String, java.lang.Class, com.restfb.BinaryAttachment, 479 * com.restfb.Parameter[]) 480 */ 481 @Override 482 public <T> T publish(String connection, Class<T> objectType, BinaryAttachment binaryAttachment, 483 Parameter... parameters) { 484 return publishWithResult(connection, objectType, binaryAttachment, parameters).getResult(); 485 } 486 487 @Override 488 public <T> ApiResult<T> publishWithResult(String connection, Class<T> objectType, BinaryAttachment binaryAttachment, 489 Parameter... parameters) { 490 List<BinaryAttachment> attachments = 491 Optional.ofNullable(binaryAttachment).map(Collections::singletonList).orElse(null); 492 return publishWithResult(connection, objectType, attachments, parameters); 493 } 494 495 /** 496 * @see com.restfb.FacebookClient#publish(java.lang.String, java.lang.Class, com.restfb.Parameter[]) 497 */ 498 @Override 499 public <T> T publish(String connection, Class<T> objectType, Parameter... parameters) { 500 return publishWithResult(connection, objectType, parameters).getResult(); 501 } 502 503 @Override 504 public <T> ApiResult<T> publishWithResult(String connection, Class<T> objectType, Parameter... parameters) { 505 return publishWithResult(connection, objectType, (List<BinaryAttachment>) null, parameters); 506 } 507 508 @Override 509 public <T> T publish(String connection, Class<T> objectType, Body body, Parameter... parameters) { 510 return publishWithResult(connection, objectType, body, parameters).getResult(); 511 } 512 513 @Override 514 public <T> ApiResult<T> publishWithResult(String connection, Class<T> objectType, Body body, 515 Parameter... parameters) { 516 verifyParameterPresence(CONNECTION, connection); 517 RequestExecutionResult executionResult = makeRequestWithMetadata(connection, true, false, null, body, parameters); 518 T mapped = jsonMapper.toJavaObject(executionResult.getResponse().getBody(), objectType); 519 return toApiResult(mapped, executionResult); 520 } 521 522 @Override 523 public String getLogoutUrl(String next) { 524 String parameterString; 525 if (next != null) { 526 Parameter p = Parameter.with("next", next); 527 parameterString = toParameterString(false, p); 528 } else { 529 parameterString = toParameterString(false); 530 } 531 532 final String fullEndPoint = createEndpointForApiCall("logout.php", false, false); 533 return fullEndPoint + "?" + parameterString; 534 } 535 536 /** 537 * @see com.restfb.FacebookClient#executeBatch(com.restfb.batch.BatchRequest[]) 538 */ 539 @Override 540 public List<BatchResponse> executeBatch(BatchRequest... batchRequests) { 541 return executeBatch(asList(batchRequests), Collections.emptyList()); 542 } 543 544 /** 545 * @see com.restfb.FacebookClient#executeBatch(java.util.List) 546 */ 547 @Override 548 public List<BatchResponse> executeBatch(List<BatchRequest> batchRequests) { 549 return executeBatch(batchRequests, Collections.emptyList()); 550 } 551 552 /** 553 * @see com.restfb.FacebookClient#executeBatch(java.util.List, java.util.List) 554 */ 555 @Override 556 public List<BatchResponse> executeBatch(List<BatchRequest> batchRequests, List<BinaryAttachment> binaryAttachments) { 557 verifyParameterPresence("binaryAttachments", binaryAttachments); 558 requireNotEmpty(batchRequests, "You must specify at least one batch request."); 559 560 return jsonMapper.toJavaList( 561 makeRequest("", true, false, binaryAttachments, Parameter.with("batch", jsonMapper.toJson(batchRequests, true))), 562 BatchResponse.class); 563 } 564 565 /** 566 * @see com.restfb.FacebookClient#convertSessionKeysToAccessTokens(java.lang.String, java.lang.String, 567 * java.lang.String[]) 568 */ 569 @Override 570 public List<AccessToken> convertSessionKeysToAccessTokens(String appId, String secretKey, String... sessionKeys) { 571 verifyParameterPresence(APP_ID, appId); 572 verifyParameterPresence("secretKey", secretKey); 573 574 if (sessionKeys == null || sessionKeys.length == 0) { 575 return emptyList(); 576 } 577 578 String json = makeRequest("/oauth/exchange_sessions", true, false, null, Parameter.with(CLIENT_ID, appId), 579 Parameter.with(PARAM_CLIENT_SECRET, secretKey), Parameter.with("sessions", String.join(",", sessionKeys))); 580 581 return jsonMapper.toJavaList(json, AccessToken.class); 582 } 583 584 /** 585 * @see com.restfb.FacebookClient#obtainAppAccessToken(java.lang.String, java.lang.String) 586 */ 587 @Override 588 public AccessToken obtainAppAccessToken(String appId, String appSecret) { 589 verifyParameterPresence(APP_ID, appId); 590 verifyParameterPresence(APP_SECRET, appSecret); 591 592 String response = makeRequest(PATH_OAUTH_ACCESS_TOKEN, Parameter.with(GRANT_TYPE, "client_credentials"), 593 Parameter.with(CLIENT_ID, appId), Parameter.with(PARAM_CLIENT_SECRET, appSecret)); 594 595 try { 596 return getAccessTokenFromResponse(response); 597 } catch (Exception t) { 598 throw new FacebookResponseContentException(CANNOT_EXTRACT_ACCESS_TOKEN_MESSAGE, t); 599 } 600 } 601 602 @Override 603 public DeviceCode fetchDeviceCode(ScopeBuilder scope) { 604 verifyParameterPresence(SCOPE, scope); 605 ObjectUtil.requireNotNull(accessToken, 606 () -> new IllegalStateException("access token is required to fetch a device access token")); 607 608 String response = makeRequest("device/login", true, false, null, Parameter.with("type", "device_code"), 609 Parameter.with(SCOPE, scope.toString())); 610 return jsonMapper.toJavaObject(response, DeviceCode.class); 611 } 612 613 @Override 614 public AccessToken obtainDeviceAccessToken(String code) throws FacebookDeviceTokenCodeExpiredException, 615 FacebookDeviceTokenPendingException, FacebookDeviceTokenDeclinedException, FacebookDeviceTokenSlowdownException { 616 verifyParameterPresence(CODE, code); 617 618 ObjectUtil.requireNotNull(accessToken, 619 () -> new IllegalStateException("access token is required to fetch a device access token")); 620 621 try { 622 String response = makeRequest("device/login_status", true, false, null, Parameter.with("type", "device_token"), 623 Parameter.with(CODE, code)); 624 return getAccessTokenFromResponse(response); 625 } catch (FacebookOAuthException foae) { 626 DeviceTokenExceptionFactory.createFrom(foae); 627 return null; 628 } 629 } 630 631 /** 632 * @see com.restfb.FacebookClient#obtainUserAccessToken(java.lang.String, java.lang.String, java.lang.String, 633 * java.lang.String) 634 */ 635 @Override 636 public AccessToken obtainUserAccessToken(String appId, String appSecret, String redirectUri, 637 String verificationCode) { 638 verifyParameterPresence(APP_ID, appId); 639 verifyParameterPresence(APP_SECRET, appSecret); 640 verifyParameterPresence("verificationCode", verificationCode); 641 642 String response = makeRequest(PATH_OAUTH_ACCESS_TOKEN, Parameter.with(CLIENT_ID, appId), 643 Parameter.with(PARAM_CLIENT_SECRET, appSecret), Parameter.with(CODE, verificationCode), 644 Parameter.with(REDIRECT_URI, redirectUri)); 645 646 try { 647 return getAccessTokenFromResponse(response); 648 } catch (Exception t) { 649 throw new FacebookResponseContentException(CANNOT_EXTRACT_ACCESS_TOKEN_MESSAGE, t); 650 } 651 } 652 653 /** 654 * @see com.restfb.FacebookClient#obtainExtendedAccessToken(java.lang.String, java.lang.String) 655 */ 656 @Override 657 public AccessToken obtainExtendedAccessToken(String appId, String appSecret) { 658 ObjectUtil.requireNotNull(accessToken, 659 () -> new IllegalStateException( 660 format("You cannot call this method because you did not construct this instance of %s with an access token.", 661 getClass().getSimpleName()))); 662 663 return obtainExtendedAccessToken(appId, appSecret, accessToken); 664 } 665 666 @Override 667 public AccessToken obtainRefreshedExtendedAccessToken() { 668 throw new UnsupportedOperationException( 669 "obtaining a refreshed extended access token is not supported by this client"); 670 } 671 672 /** 673 * @see com.restfb.FacebookClient#obtainExtendedAccessToken(java.lang.String, java.lang.String, java.lang.String) 674 */ 675 @Override 676 public AccessToken obtainExtendedAccessToken(String appId, String appSecret, String accessToken) { 677 verifyParameterPresence(APP_ID, appId); 678 verifyParameterPresence(APP_SECRET, appSecret); 679 verifyParameterPresence("accessToken", accessToken); 680 681 String response = makeRequest("/oauth/access_token", false, false, null, // 682 Parameter.with(CLIENT_ID, appId), // 683 Parameter.with(PARAM_CLIENT_SECRET, appSecret), // 684 Parameter.with(GRANT_TYPE, "fb_exchange_token"), // 685 Parameter.with("fb_exchange_token", accessToken), // 686 Parameter.withFields("access_token,expires_in,token_type")); 687 688 try { 689 return getAccessTokenFromResponse(response); 690 } catch (Exception t) { 691 throw new FacebookResponseContentException(CANNOT_EXTRACT_ACCESS_TOKEN_MESSAGE, t); 692 } 693 } 694 695 protected AccessToken getAccessTokenFromResponse(String response) { 696 AccessToken token; 697 try { 698 token = getJsonMapper().toJavaObject(response, AccessToken.class); 699 } catch (FacebookJsonMappingException fjme) { 700 CLIENT_LOGGER.trace("could not map response to access token class try to fetch directly from String", fjme); 701 token = AccessToken.fromQueryString(response); 702 } 703 token.setClient(createClientWithAccessToken(token.getAccessToken())); 704 return token; 705 } 706 707 @Override 708 @SuppressWarnings("unchecked") 709 public <T> T parseSignedRequest(String signedRequest, String appSecret, Class<T> objectType) { 710 verifyParameterPresence("signedRequest", signedRequest); 711 verifyParameterPresence(APP_SECRET, appSecret); 712 verifyParameterPresence("objectType", objectType); 713 714 String[] signedRequestTokens = signedRequest.split("[.]"); 715 716 if (signedRequestTokens.length != 2) { 717 throw new FacebookSignedRequestParsingException(format( 718 "Signed request '%s' is expected to be signature and payload strings separated by a '.'", signedRequest)); 719 } 720 721 String encodedSignature = signedRequestTokens[0]; 722 String urlDecodedSignature = urlDecodeSignedRequestToken(encodedSignature); 723 byte[] signature = decodeBase64(urlDecodedSignature); 724 725 String encodedPayload = signedRequestTokens[1]; 726 String urlDecodedPayload = urlDecodeSignedRequestToken(encodedPayload); 727 String payload = StringUtils.toString(decodeBase64(urlDecodedPayload)); 728 729 // Convert payload to a JsonObject, so we can pull algorithm data out of it 730 JsonObject payloadObject = getJsonMapper().toJavaObject(payload, JsonObject.class); 731 732 if (!payloadObject.contains(ALGORITHM)) { 733 throw new FacebookSignedRequestParsingException("Unable to detect algorithm used for signed request"); 734 } 735 736 String algorithm = payloadObject.getString(ALGORITHM, null); 737 738 if (!verifySignedRequest(appSecret, algorithm, encodedPayload, signature)) { 739 throw new FacebookSignedRequestVerificationException( 740 "Signed request verification failed. Are you sure the request was made for the app identified by the app secret you provided?"); 741 } 742 743 // Marshal to the user's preferred type. 744 // If the user asked for a JsonObject, send back the one we already parsed. 745 return objectType.equals(JsonObject.class) ? (T) payloadObject : getJsonMapper().toJavaObject(payload, objectType); 746 } 747 748 /** 749 * Decodes a component of a signed request received from Facebook using FB's special URL-encoding strategy. 750 * 751 * @param signedRequestToken 752 * Token to decode. 753 * @return The decoded token. 754 */ 755 protected String urlDecodeSignedRequestToken(String signedRequestToken) { 756 verifyParameterPresence("signedRequestToken", signedRequestToken); 757 return signedRequestToken.replace("-", "+").replace("_", "/").trim(); 758 } 759 760 @Override 761 public String getLoginDialogUrl(String appId, String redirectUri, ScopeBuilder scope, String state, 762 Parameter... parameters) { 763 List<Parameter> parameterList = asList(parameters); 764 return getGenericLoginDialogUrl(appId, redirectUri, scope, 765 () -> getFacebookEndpointUrls().getFacebookEndpoint() + "/dialog/oauth", state, parameterList); 766 } 767 768 @Override 769 public String getLoginDialogUrl(String appId, String redirectUri, ScopeBuilder scope, Parameter... parameters) { 770 return getLoginDialogUrl(appId, redirectUri, scope, null, parameters); 771 } 772 773 @Override 774 public String getBusinessLoginDialogUrl(String appId, String redirectUri, String configId, String state, 775 Parameter... parameters) { 776 verifyParameterPresence("configId", configId); 777 778 List<Parameter> parameterList = new ArrayList<>(asList(parameters)); 779 parameterList.add(Parameter.with("config_id", configId)); 780 parameterList.add(Parameter.with("response_type", "code")); 781 parameterList.add(Parameter.with("override_default_response_type", true)); 782 783 return getGenericLoginDialogUrl(appId, redirectUri, new ScopeBuilder(true), 784 () -> getFacebookEndpointUrls().getFacebookEndpoint() + "/dialog/oauth", state, parameterList); 785 } 786 787 protected String getGenericLoginDialogUrl(String appId, String redirectUri, ScopeBuilder scope, 788 Supplier<String> endpointSupplier, String state, List<Parameter> parameters) { 789 verifyParameterPresence(APP_ID, appId); 790 verifyParameterPresence("redirectUri", redirectUri); 791 verifyParameterPresence(SCOPE, scope); 792 793 String dialogUrl = endpointSupplier.get(); 794 795 List<Parameter> parameterList = new ArrayList<>(); 796 parameterList.add(Parameter.with(CLIENT_ID, appId)); 797 parameterList.add(Parameter.with(REDIRECT_URI, redirectUri)); 798 if (!scope.toString().isEmpty()) { 799 parameterList.add(Parameter.with(SCOPE, scope.toString())); 800 } 801 802 if (StringUtils.isNotBlank(state)) { 803 parameterList.add(Parameter.with("state", state)); 804 } 805 806 // add optional parameters 807 parameterList.addAll(parameters); 808 return dialogUrl + "?" + toParameterString(false, parameterList.toArray(new Parameter[0])); 809 } 810 811 /** 812 * Verifies that the signed request is really from Facebook. 813 * 814 * @param appSecret 815 * The secret for the app that can verify this signed request. 816 * @param algorithm 817 * Signature algorithm specified by FB in the decoded payload. 818 * @param encodedPayload 819 * The encoded payload used to generate a signature for comparison against the provided {@code signature}. 820 * @param signature 821 * The decoded signature extracted from the signed request. Compared against a signature generated from 822 * {@code encodedPayload}. 823 * @return {@code true} if the signed request is verified, {@code false} if not. 824 */ 825 protected boolean verifySignedRequest(String appSecret, String algorithm, String encodedPayload, byte[] signature) { 826 verifyParameterPresence(APP_SECRET, appSecret); 827 verifyParameterPresence(ALGORITHM, algorithm); 828 verifyParameterPresence("encodedPayload", encodedPayload); 829 verifyParameterPresence("signature", signature); 830 831 // Normalize algorithm name...FB calls it differently than Java does 832 if ("HMAC-SHA256".equalsIgnoreCase(algorithm)) { 833 algorithm = "HMACSHA256"; 834 } 835 836 try { 837 Mac mac = Mac.getInstance(algorithm); 838 mac.init(new SecretKeySpec(toBytes(appSecret), algorithm)); 839 byte[] payloadSignature = mac.doFinal(toBytes(encodedPayload)); 840 return Arrays.equals(signature, payloadSignature); 841 } catch (Exception e) { 842 throw new FacebookSignedRequestVerificationException("Unable to perform signed request verification", e); 843 } 844 } 845 846 /** 847 * @see com.restfb.FacebookClient#debugToken(java.lang.String) 848 */ 849 @Override 850 public DebugTokenInfo debugToken(String inputToken) { 851 verifyParameterPresence("inputToken", inputToken); 852 String response = makeRequest("/debug_token", Parameter.with("input_token", inputToken)); 853 854 try { 855 JsonObject json = Json.parse(response).asObject(); 856 857 // FB sometimes returns an empty DebugTokenInfo and then the 'data' field is an array 858 if (json.contains("data") && json.get("data").isArray()) { 859 return null; 860 } 861 862 JsonObject data = json.get("data").asObject(); 863 return getJsonMapper().toJavaObject(data.toString(), DebugTokenInfo.class); 864 } catch (Exception t) { 865 throw new FacebookResponseContentException("Unable to parse JSON from response.", t); 866 } 867 } 868 869 /** 870 * @see com.restfb.FacebookClient#getJsonMapper() 871 */ 872 @Override 873 public JsonMapper getJsonMapper() { 874 return jsonMapper; 875 } 876 877 /** 878 * @see com.restfb.FacebookClient#getWebRequestor() 879 */ 880 @Override 881 public WebRequestor getWebRequestor() { 882 return webRequestor; 883 } 884 885 /** 886 * Coordinates the process of executing the API request GET/POST and processing the response we receive from the 887 * endpoint. 888 * 889 * @param endpoint 890 * Facebook Graph API endpoint. 891 * @param parameters 892 * Arbitrary number of parameters to send along to Facebook as part of the API call. 893 * @return The JSON returned by Facebook for the API call. 894 * @throws FacebookException 895 * If an error occurs while making the Facebook API POST or processing the response. 896 */ 897 protected String makeRequest(String endpoint, Parameter... parameters) { 898 return makeRequestForResponse(endpoint, parameters).getBody(); 899 } 900 901 protected String makeRequest(String endpoint, final boolean executeAsPost, final boolean executeAsDelete, 902 final List<BinaryAttachment> binaryAttachments, Parameter... parameters) { 903 return makeRequestForResponse(endpoint, executeAsPost, executeAsDelete, binaryAttachments, parameters).getBody(); 904 } 905 906 protected String makeRequest(String endpoint, final boolean executeAsPost, final boolean executeAsDelete, 907 final List<BinaryAttachment> binaryAttachments, Body body, Parameter... parameters) { 908 return makeRequestForResponse(endpoint, executeAsPost, executeAsDelete, binaryAttachments, body, parameters) 909 .getBody(); 910 } 911 912 protected Response makeRequestForResponse(String endpoint, Parameter... parameters) { 913 return makeRequestWithMetadata(endpoint, parameters).getResponse(); 914 } 915 916 protected Response makeRequestForResponse(String endpoint, final boolean executeAsPost, final boolean executeAsDelete, 917 final List<BinaryAttachment> binaryAttachments, Parameter... parameters) { 918 return makeRequestWithMetadata(endpoint, executeAsPost, executeAsDelete, binaryAttachments, null, parameters) 919 .getResponse(); 920 } 921 922 /** 923 * Coordinates the process of executing the API request GET/POST and returning the raw response. 924 * 925 * @param endpoint 926 * Facebook Graph API endpoint. 927 * @param executeAsPost 928 * {@code true} to execute the web request as a {@code POST}, {@code false} to execute as a {@code GET}. 929 * @param executeAsDelete 930 * {@code true} to add a special 'treat this request as a {@code DELETE}' parameter. 931 * @param binaryAttachments 932 * A list of binary files to include in a {@code POST} request. Pass {@code null} if no attachment should be 933 * sent. 934 * @param body 935 * Optional body used for POST requests. 936 * @param parameters 937 * Arbitrary number of parameters to send along to Facebook as part of the API call. 938 * @return The raw response returned by Facebook for the API call. 939 * @throws FacebookException 940 * If an error occurs while making the Facebook API POST or processing the response. 941 */ 942 protected RequestExecutionResult makeRequestWithMetadata(String endpoint, final boolean executeAsPost, 943 final boolean executeAsDelete, final List<BinaryAttachment> binaryAttachments, Body body, 944 Parameter... parameters) { 945 verifyParameterLegality(parameters); 946 947 if (executeAsDelete && isHttpDeleteFallback()) { 948 parameters = parametersWithAdditionalParameter(Parameter.with(METHOD_PARAM_NAME, "delete"), parameters); 949 } 950 951 if (!endpoint.startsWith("/")) { 952 endpoint = "/" + endpoint; 953 } 954 955 boolean hasAttachment = binaryAttachments != null && !binaryAttachments.isEmpty(); 956 boolean hasReel = hasAttachment && binaryAttachments.get(0).isFacebookReel(); 957 958 final String fullEndpoint = createEndpointForApiCall(endpoint, hasAttachment, hasReel); 959 final String parameterString = toParameterString(parameters); 960 961 String headerAccessToken = (hasReel) ? accessToken : getHeaderAccessToken(); 962 963 WebRequestor.Request request = new WebRequestor.Request(fullEndpoint, headerAccessToken, parameterString); 964 request.setBinaryAttachments(binaryAttachments); 965 request.setBody(body); 966 967 String httpMethod; 968 Requestor requestor; 969 970 if (executeAsDelete && !isHttpDeleteFallback()) { 971 httpMethod = "DELETE"; 972 requestor = () -> webRequestor.executeDelete(request); 973 } else if (executeAsPost) { 974 httpMethod = "POST"; 975 requestor = () -> webRequestor.executePost(request); 976 } else { 977 httpMethod = "GET"; 978 requestor = () -> webRequestor.executeGet(request); 979 } 980 981 long requestStartTime = System.currentTimeMillis(); 982 try { 983 return executeRequestWithMetadata(httpMethod, request.getFullUrl(), requestor); 984 } catch (FacebookException facebookException) { 985 facebookException.withInfoData(httpMethod, request.getUrl(), parameterString, headerAccessToken, 986 requestStartTime); 987 throw facebookException; 988 } 989 } 990 991 protected Response makeRequestForResponse(String endpoint, final boolean executeAsPost, final boolean executeAsDelete, 992 final List<BinaryAttachment> binaryAttachments, Body body, Parameter... parameters) { 993 return makeRequestWithMetadata(endpoint, executeAsPost, executeAsDelete, binaryAttachments, body, parameters) 994 .getResponse(); 995 } 996 997 protected RequestExecutionResult makeRequestWithMetadata(String endpoint, Parameter... parameters) { 998 return makeRequestWithMetadata(endpoint, false, false, null, null, parameters); 999 } 1000 1001 protected RequestExecutionResult makeRequestWithMetadata(String endpoint, final boolean executeAsPost, 1002 final boolean executeAsDelete, final List<BinaryAttachment> binaryAttachments, Parameter... parameters) { 1003 return makeRequestWithMetadata(endpoint, executeAsPost, executeAsDelete, binaryAttachments, null, parameters); 1004 } 1005 1006 private RequestExecutionResult executeGetRequest(WebRequestor.Request request) { 1007 return executeRequestWithMetadata("GET", request.getFullUrl(), () -> webRequestor.executeGet(request)); 1008 } 1009 1010 private RequestExecutionResult executeGetRequestWithInfo(WebRequestor.Request request) { 1011 long startTime = System.currentTimeMillis(); 1012 try { 1013 return executeGetRequest(request); 1014 } catch (FacebookException facebookException) { 1015 facebookException.withInfoData("GET", request.getUrl(), request.getParameters(), request.getHeaderAccessToken(), 1016 startTime); 1017 throw facebookException; 1018 } 1019 } 1020 1021 private String getHeaderAccessToken() { 1022 if (accessTokenInHeader) { 1023 return this.accessToken; 1024 } 1025 1026 return null; 1027 } 1028 1029 /** 1030 * @see com.restfb.FacebookClient#obtainAppSecretProof(java.lang.String, java.lang.String) 1031 */ 1032 @Override 1033 public String obtainAppSecretProof(String accessToken, String appSecret) { 1034 verifyParameterPresence("accessToken", accessToken); 1035 verifyParameterPresence(APP_SECRET, appSecret); 1036 return EncodingUtils.encodeAppSecretProof(appSecret, accessToken); 1037 } 1038 1039 /** 1040 * returns if the fallback post method (<code>true</code>) is used or the http delete (<code>false</code>) 1041 * 1042 * @return {@code true} if POST is used instead of HTTP DELETE (default) 1043 */ 1044 public boolean isHttpDeleteFallback() { 1045 return httpDeleteFallback; 1046 } 1047 1048 /** 1049 * Set to <code>true</code> if the facebook http delete fallback should be used. Facebook allows to use the http POST 1050 * with the parameter "method=delete" to override the post and use delete instead. This feature allow http client that 1051 * do not support the whole http method set, to delete objects from facebook 1052 * 1053 * @param httpDeleteFallback 1054 * <code>true</code> if the http Delete Fallback is used 1055 */ 1056 public void setHttpDeleteFallback(boolean httpDeleteFallback) { 1057 this.httpDeleteFallback = httpDeleteFallback; 1058 } 1059 1060 protected interface Requestor { 1061 Response makeRequest() throws IOException; 1062 } 1063 1064 protected RequestExecutionResult executeRequestWithMetadata(String httpMethod, String requestUrl, 1065 Requestor requestor) { 1066 Response response; 1067 long start = System.nanoTime(); 1068 1069 // Perform a GET or POST to the API endpoint 1070 try { 1071 response = requestor.makeRequest(); 1072 } catch (IOException ioe) { 1073 if (ioe.getMessage() != null && ioe.getMessage().contains("RST_STREAM")) { 1074 throw new FacebookRstStreamNetworkException(ioe.getMessage(), ioe); 1075 } 1076 if (ioe.getMessage() != null && ioe.getMessage().contains("GOAWAY")) { 1077 throw new FacebookGoawayNetworkException(ioe.getMessage(), ioe); 1078 } 1079 throw new FacebookNetworkException(ioe); 1080 } catch (Exception t) { 1081 throw new FacebookNetworkException(t); 1082 } 1083 1084 // If we get any HTTP response code other than a 200 OK or 400 Bad Request 1085 // or 401 Not Authorized or 403 Forbidden or 404 Not Found or 500 Internal 1086 // Server Error or 302 Not Modified 1087 // throw an exception. 1088 if (HTTP_OK != response.getStatusCode() && HTTP_BAD_REQUEST != response.getStatusCode() 1089 && HTTP_UNAUTHORIZED != response.getStatusCode() && HTTP_NOT_FOUND != response.getStatusCode() 1090 && HTTP_INTERNAL_ERROR != response.getStatusCode() && HTTP_FORBIDDEN != response.getStatusCode() 1091 && HTTP_NOT_MODIFIED != response.getStatusCode()) { 1092 throw new FacebookNetworkException(response.getStatusCode()); 1093 } 1094 1095 try { 1096 // If the response contained an error code, throw an exception. 1097 getFacebookExceptionGenerator().throwFacebookResponseStatusExceptionIfNecessary(response.getBody(), 1098 response.getStatusCode()); 1099 } catch (FacebookErrorMessageException feme) { 1100 Optional.ofNullable(response).map(Response::getDebugHeaderInfo).ifPresent(feme::setDebugHeaderInfo); 1101 throw feme; 1102 } 1103 1104 // If there was no response error information and this was a 500 or 401 1105 // error, something weird happened on Facebook's end. Bail. 1106 if (HTTP_INTERNAL_ERROR == response.getStatusCode() || HTTP_UNAUTHORIZED == response.getStatusCode()) { 1107 throw new FacebookNetworkException(response.getStatusCode()); 1108 } 1109 1110 Duration duration = Duration.ofNanos(System.nanoTime() - start); 1111 return new RequestExecutionResult(response, duration, httpMethod, requestUrl); 1112 } 1113 1114 protected static class RequestExecutionResult { 1115 private final Response response; 1116 private final Duration duration; 1117 private final String httpMethod; 1118 private final String requestUrl; 1119 1120 RequestExecutionResult(Response response, Duration duration, String httpMethod, String requestUrl) { 1121 this.response = response; 1122 this.duration = duration; 1123 this.httpMethod = httpMethod; 1124 this.requestUrl = requestUrl; 1125 } 1126 1127 public Response getResponse() { 1128 return response; 1129 } 1130 1131 public Duration getDuration() { 1132 return duration; 1133 } 1134 1135 public String getHttpMethod() { 1136 return httpMethod; 1137 } 1138 1139 public String getRequestUrl() { 1140 return requestUrl; 1141 } 1142 } 1143 1144 /** 1145 * Generate the parameter string to be included in the Facebook API request. 1146 * 1147 * @param parameters 1148 * Arbitrary number of extra parameters to include in the request. 1149 * @return The parameter string to include in the Facebook API request. 1150 * @throws FacebookJsonMappingException 1151 * If an error occurs when building the parameter string. 1152 */ 1153 protected String toParameterString(Parameter... parameters) { 1154 return toParameterString(true, parameters); 1155 } 1156 1157 /** 1158 * Generate the parameter string to be included in the Facebook API request. 1159 * 1160 * @param withJsonParameter 1161 * add additional parameter format with type json 1162 * @param parameters 1163 * Arbitrary number of extra parameters to include in the request. 1164 * @return The parameter string to include in the Facebook API request. 1165 * @throws FacebookJsonMappingException 1166 * If an error occurs when building the parameter string. 1167 */ 1168 protected String toParameterString(boolean withJsonParameter, Parameter... parameters) { 1169 if (!isBlank(accessToken) && !accessTokenInHeader) { 1170 parameters = parametersWithAdditionalParameter(Parameter.with(ACCESS_TOKEN_PARAM_NAME, accessToken), parameters); 1171 } 1172 1173 if (!isBlank(accessToken) && !isBlank(appSecret)) { 1174 if (isAppSecretProofWithTime()) { 1175 long now = System.currentTimeMillis() / 1000; 1176 parameters = parametersWithAdditionalParameter( 1177 Parameter.with(APP_SECRET_PROOF_TIME_PARAM_NAME, String.valueOf(now)), parameters); 1178 parameters = parametersWithAdditionalParameter( 1179 Parameter.with(APP_SECRET_PROOF_PARAM_NAME, obtainAppSecretProof(accessToken + "|" + now, appSecret)), 1180 parameters); 1181 } else { 1182 parameters = parametersWithAdditionalParameter( 1183 Parameter.with(APP_SECRET_PROOF_PARAM_NAME, obtainAppSecretProof(accessToken, appSecret)), parameters); 1184 } 1185 } 1186 1187 if (withJsonParameter) { 1188 parameters = parametersWithAdditionalParameter(Parameter.with(FORMAT_PARAM_NAME, "json"), parameters); 1189 } 1190 1191 return Stream.of(parameters).map(p -> urlEncode(p.name) + "=" + urlEncodedValueForParameterName(p.name, p.value)) 1192 .collect(Collectors.joining("&")); 1193 } 1194 1195 /** 1196 * @see com.restfb.BaseFacebookClient#createEndpointForApiCall(java.lang.String,boolean,boolean) 1197 */ 1198 @Override 1199 protected String createEndpointForApiCall(String apiCall, boolean hasAttachment, boolean hasReel) { 1200 while (apiCall.startsWith("/")) { 1201 apiCall = apiCall.substring(1); 1202 } 1203 1204 String baseUrl = createBaseUrlForEndpoint(apiCall, hasAttachment, hasReel); 1205 1206 return format("%s/%s", baseUrl, apiCall); 1207 } 1208 1209 protected String createBaseUrlForEndpoint(String apiCall, boolean hasAttachment, boolean hasReel) { 1210 String baseUrl = getFacebookGraphEndpointUrl(); 1211 if (hasAttachment && hasReel) { 1212 baseUrl = getFacebookReelsUploadEndpointUrl(); 1213 } else if (apiCall.endsWith("logout.php")) { 1214 baseUrl = getFacebookEndpointUrls().getFacebookEndpoint(); 1215 } 1216 return baseUrl; 1217 } 1218 1219 /** 1220 * Returns the base endpoint URL for the Graph API. 1221 * 1222 * @return The base endpoint URL for the Graph API. 1223 */ 1224 protected String getFacebookGraphEndpointUrl() { 1225 if (apiVersion.isUrlElementRequired()) { 1226 return getFacebookEndpointUrls().getGraphEndpoint() + '/' + apiVersion.getUrlElement(); 1227 } else { 1228 return getFacebookEndpointUrls().getGraphEndpoint(); 1229 } 1230 } 1231 1232 /** 1233 * Returns the base endpoint URL for the Graph APIs video upload functionality. 1234 * 1235 * @return The base endpoint URL for the Graph APIs video upload functionality. 1236 * @since 1.6.5 1237 * @deprecated the Graph Video endpoint is deprecated; video uploads use the Graph API endpoint instead. 1238 */ 1239 @Deprecated 1240 protected String getFacebookGraphVideoEndpointUrl() { 1241 if (apiVersion.isUrlElementRequired()) { 1242 return getFacebookEndpointUrls().getGraphVideoEndpoint() + '/' + apiVersion.getUrlElement(); 1243 } else { 1244 return getFacebookEndpointUrls().getGraphVideoEndpoint(); 1245 } 1246 } 1247 1248 /** 1249 * Returns the Facebook Reels Upload endpoint URL for handling the Reels Upload 1250 * 1251 * @return the Facebook Reels Upload endpoint URL 1252 */ 1253 protected String getFacebookReelsUploadEndpointUrl() { 1254 if (apiVersion.isUrlElementRequired()) { 1255 return getFacebookEndpointUrls().getReelUploadEndpoint() + "/" + apiVersion.getUrlElement(); 1256 } 1257 1258 return getFacebookEndpointUrls().getReelUploadEndpoint(); 1259 } 1260 1261 public FacebookEndpoints getFacebookEndpointUrls() { 1262 return facebookEndpointUrls; 1263 } 1264 1265 public void setFacebookEndpointUrls(FacebookEndpoints facebookEndpointUrls) { 1266 this.facebookEndpointUrls = facebookEndpointUrls; 1267 } 1268}