package org.karmaexchange.provider;
import static java.lang.String.format;
import static org.karmaexchange.util.OfyService.ofy;
import static org.karmaexchange.util.Properties.Property.FACEBOOK_APP_SECRET;
import java.io.IOException;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Response;
import lombok.Data;
import org.karmaexchange.auth.AuthProviderType;
import org.karmaexchange.auth.GlobalUid;
import org.karmaexchange.auth.GlobalUidMapping;
import org.karmaexchange.auth.GlobalUidType;
import org.karmaexchange.auth.AuthProvider.UserInfo;
import org.karmaexchange.dao.Address;
import org.karmaexchange.dao.ImageProviderType;
import org.karmaexchange.dao.User;
import org.karmaexchange.dao.User.RegisteredEmail;
import org.karmaexchange.resources.msg.ErrorResponseMsg;
import org.karmaexchange.resources.msg.ErrorResponseMsg.ErrorInfo;
import org.karmaexchange.util.AdminTaskServlet;
import org.karmaexchange.util.AdminUtil;
import org.karmaexchange.util.Properties;
import org.karmaexchange.util.ServletUtil;
import com.googlecode.objectify.Key;
import com.restfb.DefaultFacebookClient;
import com.restfb.Facebook;
import com.restfb.exception.FacebookException;
// TODO(avaliani): This class is now out of sync. It's missing age_range, gender
@SuppressWarnings("serial")
public class FacebookRegistrationServlet extends AdminTaskServlet {
private static final Logger logger =
Logger.getLogger(FacebookRegistrationServlet.class.getName());
public static final Level REGISTRATION_LOG_LEVEL = Level.FINE;
private static final String SIGNED_REQUEST_PARAM = "signed_request";
private static final String EXPECTED_FIELDS_METADATA =
"[{'name':'name'},{'name':'first_name'},{'name':'last_name'},{'name':'email'}," +
"{'name':'location'},{'name':'captcha'}]";
public FacebookRegistrationServlet() {
super(AdminUtil.AdminTaskType.REGISTRATION);
}
@Override
public void execute() throws IOException {
try {
String signedRequestStr = req.getParameter(SIGNED_REQUEST_PARAM);
if (signedRequestStr == null) {
throw ErrorResponseMsg.createException("signed request missing",
ErrorInfo.Type.BAD_REQUEST);
}
String appSecret = Properties.get(FACEBOOK_APP_SECRET);
SignedRegistrationRequest registrationReq;
try {
registrationReq = new DefaultFacebookClient().parseSignedRequest(
signedRequestStr, appSecret, SignedRegistrationRequest.class);
} catch (FacebookException e) {
throw ErrorResponseMsg.createException(e, ErrorInfo.Type.BAD_REQUEST);
}
registrationReq.validate();
UserInfo userInfo = registrationReq.createUser();
Key<User> persistedUserKey = User.upsertNewUser(userInfo);
GlobalUidMapping mapping = new GlobalUidMapping(
new GlobalUid(GlobalUidType.toGlobalUidType(AuthProviderType.FACEBOOK), registrationReq.getUserId()),
persistedUserKey);
ofy().save().entity(mapping); // Asynchronously save the new mapping
resp.sendRedirect("/");
} catch (WebApplicationException e) {
Response errMsg = e.getResponse();
logger.log(REGISTRATION_LOG_LEVEL, "Failed to register user:\n " + errMsg.getEntity());
ServletUtil.setResponse(resp, e);
// TODO(avaliani): If registration fails it would be good to have a redirect page to
// paste the error to.
}
}
@Data
private static class SignedRegistrationRequest {
@Facebook("oauth_token")
private String oAuthToken;
@Facebook("registration")
private RegistrationInfo registrationInfo;
@Facebook("registration_metadata")
private RegistrationMetadata registrationMetadata;
@Facebook("user_id")
private String userId;
public void validate() {
checkFieldNotNull(oAuthToken, "oAuthToken");
checkFieldNotNull(registrationInfo, "registrationInfo");
checkFieldNotNull(registrationInfo.name, "registrationInfo.name");
checkFieldNotNull(registrationInfo.firstName, "registrationInfo.firstName");
// skip last name - it's okay for someone not to specify this
// skip email - it's okay for someone not to specify this
// skip location - it's okay for someone not to specify this
checkFieldNotNull(registrationMetadata, "registrationMetadata");
checkFieldNotNull(registrationMetadata.fields, "registrationMetadata.fields");
checkFieldNotNull(userId, "userId");
// Handle fields meta data attack mentioned in:
// https://developers.facebook.com/docs/plugins/registration/advanced/
String metadataNoWs = registrationMetadata.fields.replaceAll("\\s", "");
if (!metadataNoWs.equals(EXPECTED_FIELDS_METADATA)) {
throw ErrorResponseMsg.createException(
"registration form field mis-match: '" + metadataNoWs + "'",
ErrorInfo.Type.BAD_REQUEST);
}
}
private void checkFieldNotNull(Object value, String fieldName) {
if (value == null) {
throw ErrorResponseMsg.createException(
format("bad registration request: %s is null", fieldName),
ErrorInfo.Type.BAD_REQUEST);
}
}
private UserInfo createUser() {
User user = User.create();
user.setFirstName(registrationInfo.firstName);
user.setLastName(registrationInfo.lastName);
if (registrationInfo.email != null) {
user.getRegisteredEmails().add(new RegisteredEmail(registrationInfo.email, true));
}
user.setAddress(parseCity());
return new UserInfo(user,
new UserInfo.ProfileImage(
ImageProviderType.FACEBOOK,
FacebookSocialNetworkProvider.getProfileImageUrl(userId)) );
}
private Address parseCity() {
if ((registrationInfo.currentCity == null) || (registrationInfo.currentCity.name == null)) {
return null;
}
return FacebookSocialNetworkProvider.parseCity(registrationInfo.currentCity.name);
}
@Data
private static class RegistrationInfo {
@Facebook
private String name;
@Facebook("first_name")
private String firstName;
@Facebook("last_name")
private String lastName;
@Facebook
private String email;
@Facebook("location")
private NameAndIdFacebookType currentCity;
}
@Data
private static class RegistrationMetadata {
@Facebook
private String fields;
}
}
// TODO(avaliani): Verify if NamedFacebookType can safely replace it. Most likely it can.
@Data
private static class NameAndIdFacebookType {
@Facebook
private String name;
@Facebook
private Long id;
}
}