package onlinefrontlines.facebook; import java.io.*; import java.net.*; import java.util.*; import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; import org.json.simple.parser.JSONParser; import org.apache.commons.codec.binary.Base64; import org.apache.commons.io.IOUtils; import onlinefrontlines.utils.Cache; import onlinefrontlines.utils.GlobalProperties; import onlinefrontlines.utils.Tools; /** * Helper class that wraps the Facebook API * * @author jorrit * * Copyright (C) 2009-2013 Jorrit Rouwe * * This file is part of Online Frontlines. * * Online Frontlines is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Online Frontlines is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Online Frontlines. If not, see <http://www.gnu.org/licenses/>. */ public class Facebook { /** * Helper class that contains parsed authentication cookie */ private static class AuthenticationCookie { String uid; String code; } /** * Parse authentication cookie and return results */ private static Cache<String, AuthenticationCookie> authenticationCookieCache = new Cache<String, AuthenticationCookie>("FBAuthenticationCookieCache") { @Override protected AuthenticationCookie load(String cookie) throws Throwable { // Parse cookie into signature and payload String[] params = cookie.split("\\."); if (params.length != 2) throw new Exception("Cookie could not be split"); byte[] signature = Base64.decodeBase64(params[0]); String payload = params[1]; String payloadDecoded = new String(Base64.decodeBase64(payload)); // Parse JSON JSONParser parser = new JSONParser(); @SuppressWarnings("unchecked") Map<String, Object> payloadJson = (Map<String, Object>)parser.parse(payloadDecoded); // Check signing algorithm Object algorithm = payloadJson.get("algorithm"); if (algorithm == null || !algorithm.toString().toUpperCase().equals("HMAC-SHA256")) throw new Exception("Algorithm was incorrect"); // Get user id Object userIdObject = payloadJson.get("user_id"); if (userIdObject == null) throw new Exception("User id is missing"); // Get code Object codeObject = payloadJson.get("code"); if (codeObject == null) throw new Exception("Code is missing"); // Validate signature SecretKeySpec secretKey = new SecretKeySpec((GlobalProperties.getInstance().getString("facebook.secret")).getBytes(), "HmacSHA256"); Mac mac = Mac.getInstance("HmacSHA256"); mac.init(secretKey); byte[] calculatedSignature = mac.doFinal(payload.getBytes()); if (signature.length != calculatedSignature.length) throw new Exception("Signature length mismatch"); for (int i = 0; i < signature.length; ++i) if (signature[i] != calculatedSignature[i]) throw new Exception("Signatures don't match"); // Construct cookie AuthenticationCookie r = new AuthenticationCookie(); r.uid = userIdObject.toString(); r.code = codeObject.toString(); return r; } }; /** * Given an OAuth 2.0 code, return access token */ private static Cache<String, String> accessTokenCache = new Cache<String, String>("FBAccessTokenCache") { @Override protected String load(String code) throws Throwable { URL tokenUrl = new URL("https://graph.facebook.com/oauth/access_token?client_id=" + GlobalProperties.getInstance().getString("facebook.api_key") + "&redirect_uri=&client_secret=" + GlobalProperties.getInstance().getString("facebook.secret") + "&code=" + code); String tokenResult = IOUtils.toString(tokenUrl.openStream()); for (String p : tokenResult.split("&")) { int equals = p.indexOf('='); if (equals >= 0) { String key = p.substring(0, equals); if (key.equals("access_token")) return p.substring(equals + 1); } } throw new Exception("Did not find access token"); } }; /** * Helper class that allows easy parsing of Facebook API result */ public static class Result { private final Object result; /** * Constructor * * @param result JSON result */ public Result(Object result) { this.result = result; } /** * Get a sub node * * @param name Name of node */ public Result getNode(String name) { if (!(result instanceof Map)) return new Result(new Boolean(false)); Map<?, ?> map = (Map<?, ?>)result; Object value = map.get(name); return new Result(value != null? value : new Boolean(false)); } /** * Treat current node as a list */ public List<Result> getList() { if (!(result instanceof List)) return new ArrayList<Result>(); List<?> list = (List<?>)result; ArrayList<Result> result = new ArrayList<Result>(); for (Object o : list) result.add(new Result(o)); return result; } /** * Check if current node is of type boolean with value false */ public boolean isBooleanFalse() { if (!(result instanceof Boolean)) return false; return !((Boolean)result).booleanValue(); } /** * Convert current node to string */ public String getString() { return result != null? result.toString() : null; } } /** * Call a faceboook API * * @param url URL to call (ususally https://graph.facebook.com/...&access_token=...) * @return */ private static Result callApi(String url) { try { // Get my data URL urlObject = new URL(url); JSONParser parser = new JSONParser(); return new Result(parser.parse(new InputStreamReader(urlObject.openStream()))); } catch (Exception e) { Tools.logException(e); return null; } } /** * Helper class that contains information on an authenticated user */ public static class AuthenticatedUser { public String uid; public String accessToken; } /** * Parses cookie string and returns authenticated user * * @param cookie Value of facebook fbrs_* cookie */ public static AuthenticatedUser getAuthenticatedUser(String cookie) throws Exception { // Crack cookie AuthenticationCookie authCookie; try { authCookie = authenticationCookieCache.get(cookie); } catch (Exception e) { Tools.logException(e); return null; } // Get access token String accessToken; try { accessToken = accessTokenCache.get(authCookie.code); } catch (Exception e) { Tools.logException(e); return null; } // Return data AuthenticatedUser u = new AuthenticatedUser(); u.uid = authCookie.uid; u.accessToken = accessToken; return u; } /** * Helper class that contains properties about a user */ public static class UserDetails { public String facebookId; public String name; public String website; public String email; } /** * Get properties for current user * * @param accessToken Facebook access token */ public static UserDetails getMyDetails(String accessToken) { Result result = callApi("https://graph.facebook.com/me?access_token=" + accessToken); if (result == null || result.isBooleanFalse()) return null; UserDetails user = new UserDetails(); user.facebookId = result.getNode("id").getString(); user.name = result.getNode("name").getString(); user.website = result.getNode("link").getString(); user.email = result.getNode("email").getString(); return user; } /** * Helper class that lists invitation details * */ public static class RequestDetails { public String requestId; public String senderName; public String senderFacebookId; public String data; public String getRequestId() { return requestId; } public String getSenderName() { return senderName; } public String getSenderFacebookId() { return senderFacebookId; } public String getData() { return data; } } /** * Get number of pending invitations * * @param accessToken Facebook access token */ public static int getPendingRequestCount(String accessToken) { try { String query = "SELECT request_id FROM apprequest WHERE recipient_uid=me() AND app_id=" + GlobalProperties.getInstance().getString("facebook.api_key"); Result result = callApi("https://graph.facebook.com/fql?q=" + URLEncoder.encode(query, "ISO-8859-1") + "&access_token=" + accessToken); if (result == null || result.isBooleanFalse()) return 0; List<Result> list = result.getNode("data").getList(); return list.size(); } catch (UnsupportedEncodingException e) { Tools.logException(e); return 0; } } /** * Get all pending invitations * * @param accessToken Facebook access token */ public static ArrayList<RequestDetails> getAllPendingRequests(String accessToken) { try { String query = "SELECT request_id FROM apprequest WHERE recipient_uid=me() AND app_id=" + GlobalProperties.getInstance().getString("facebook.api_key"); Result result = callApi("https://graph.facebook.com/fql?q=" + URLEncoder.encode(query, "ISO-8859-1") + "&access_token=" + accessToken); if (result == null || result.isBooleanFalse()) return null; List<Result> list = result.getNode("data").getList(); ArrayList<RequestDetails> details = new ArrayList<RequestDetails>(); for (Result r : list) { RequestDetails d = getRequestDetails(r.getNode("request_id").getString(), accessToken); if (d != null) details.add(d); } return details; } catch (UnsupportedEncodingException e) { Tools.logException(e); return null; } } /** * Get details for invitation * * @param requestId Facebook request id: <requestid>_<userid> * @param accessToken Facebook access token */ public static RequestDetails getRequestDetails(String requestId, String accessToken) { assert(requestId.indexOf('_') >= 0); // requestId is of the form requestid_userid Result result = callApi("https://graph.facebook.com/" + requestId + "?access_token=" + accessToken); if (result == null || result.isBooleanFalse()) return null; RequestDetails details = new RequestDetails(); details.requestId = result.getNode("id").getString(); details.senderName = result.getNode("from").getNode("name").getString(); details.senderFacebookId = result.getNode("from").getNode("id").getString(); details.data = result.getNode("data").getString(); return details; } /** * Delete invitation * * @param requestId Facebook request id: <requestid>_<userid> * @param accessToken Facebook access token */ public static void deleteRequest(String requestId, String accessToken) { assert(requestId.indexOf('_') >= 0); // requestId is of the form requestid_userid callApi("https://graph.facebook.com/" + requestId + "?access_token=" + accessToken + "&method=delete"); } }