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}