Java tutorial
/** * Copyright (c) 2016 VMware, Inc. All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of * the License at http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed * under the License is distributed on an "AS IS" BASIS, without warranties or * conditions of any kind, EITHER EXPRESS OR IMPLIED. See the License for the * specific language governing permissions and limitations under the License. */ package controllers; import java.io.UnsupportedEncodingException; import java.net.URLEncoder; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Date; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.UUID; import org.apache.commons.codec.digest.DigestUtils; import org.apache.commons.lang3.StringEscapeUtils; import com.avaje.ebean.Ebean; import com.avaje.ebean.SqlUpdate; import com.avaje.ebean.annotation.Transactional; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ArrayNode; import it.innove.play.pdf.PdfGenerator; import models.Cla; import models.ClaInputField; import models.Organization; import models.ProjectCla; import models.SignedCla; import models.SignedClaGitHubPullRequest; import models.SignedClaInputField; import play.Logger; import play.Play; import play.i18n.Messages; import play.libs.F.Promise; import play.libs.mailer.Email; import play.libs.mailer.MailerPlugin; import play.libs.ws.WS; import play.libs.ws.WSRequestHolder; import play.libs.ws.WSResponse; import play.mvc.Controller; import play.mvc.Http.MultipartFormData; import play.mvc.Result; import views.html.claEmail; import views.html.claPdf; import views.html.externalReviewEmail; import views.html.index; import views.html.message; import views.html.reviewEmail; import utils.GitHubApiUtils; public class ClaController extends Controller { public static final String CLA_REJECTED_LABEL = "cla-rejected"; public static final String CLA_NOT_REQUIRED_LABEL = "cla-not-required"; private static SignedCla getSignedCla(String uuid) throws ResultException { SignedCla cla = SignedCla.find.where().eq("uuid", uuid).findUnique(); if (cla == null) { throw new ResultException(notFound(message.render("The contributor license agreement does not exist"))); } if (!cla.getState().equals(SignedCla.STATE_NEW) && !cla.getState().equals(SignedCla.STATE_REJECTED) && !cla.getState().equals(SignedCla.STATE_PENDING_EXTERNAL)) { throw new ResultException( badRequest(message.render("The contributor license agreement has already been signed"))); } return cla; } public static String getField(String key, Map<String, String[]> fields, int maxLength) { String[] values = fields.get(key); if (values == null || values.length == 0) { throw new IllegalStateException(key + " can not be missing"); } String value = StringEscapeUtils.unescapeHtml4(values[0].trim()); if (maxLength > 0) { value = value.substring(0, Math.min(value.length(), maxLength)); } return value; } public static String getToken(String uuid, String email) { StringBuilder builder = new StringBuilder(); builder.append(uuid); builder.append(email); builder.append(Play.application().configuration().getString("application.secret")); return DigestUtils.sha256Hex(builder.toString()); } public static Result index(String uuid) { session().clear(); SignedCla cla = null; try { cla = getSignedCla(uuid); } catch (ResultException e) { return e.getResult(); } String redirectUrl = Play.application().configuration().getString("app.host"); redirectUrl += controllers.routes.ClaController.authCallback(uuid, null).url(); redirectUrl = redirectUrl.substring(0, redirectUrl.indexOf("?")); StringBuilder builder = new StringBuilder(); builder.append("https://github.com/login/oauth/authorize/?client_id="); builder.append(Play.application().configuration().getString("app.github.clientid")); builder.append("&redirect_uri="); try { builder.append(URLEncoder.encode(redirectUrl, "UTF-8")); } catch (UnsupportedEncodingException e) { /* Should never happen */ return internalServerError(); } builder.append("&scope=user:email"); String authUrl = builder.toString(); return ok(index.render(cla, authUrl)); } public static Result authCallback(String uuid, String code) { SignedCla cla = null; try { cla = getSignedCla(uuid); } catch (ResultException e) { return e.getResult(); } WSRequestHolder holder = WS.url("https://github.com/login/oauth/access_token"); holder = holder.setQueryParameter("client_id", Play.application().configuration().getString("app.github.clientid")); holder = holder.setQueryParameter("client_secret", Play.application().configuration().getString("app.github.clientsecret")); holder = holder.setQueryParameter("code", code); holder = holder.setHeader("Accept", "application/json"); Promise<WSResponse> response = holder.post(""); JsonNode json = response.get(30000).asJson(); String token = json.get("access_token").asText(); String header = GitHubApiUtils.getAuthHeader(token); response = WS.url("https://api.github.com/user").setHeader("Authorization", header).get(); json = response.get(30000).asJson(); String uid = json.get("id").asText(); if (!uid.equals(cla.getGitHubUid())) { return unauthorized( message.render("This contributor license agreement request does not belong to you")); } List<String> emails = new ArrayList<String>(); response = WS.url("https://api.github.com/user/emails").setHeader("Authorization", header).get(); json = response.get(30000).asJson(); if (json.isArray()) { ArrayNode array = (ArrayNode) json; Iterator<JsonNode> iterator = array.iterator(); while (iterator.hasNext()) { JsonNode node = iterator.next(); emails.add(node.get("email").asText()); } } if (emails.isEmpty()) { return badRequest( message.render("Unable to get e-mail address from GitHub for " + cla.getGitHubLogin())); } boolean employerCheck = cla.getLegalContactEmail() != null; Map<Long, String> responses = new HashMap<Long, String>(); for (SignedClaInputField field : cla.getInputFields()) { responses.put(field.getInputField().getId(), field.getResponse()); } session().clear(); session("login", cla.getGitHubUid()); String expiration = Play.application().configuration().getString("app.ccla.expiration"); return ok(views.html.cla.render(emails, cla, employerCheck, responses, expiration)); } @Transactional public static Result signCla(String uuid) { String uid = session("login"); if (uid == null) { return forbidden(message.render("You must be signed in to access this page")); } SignedCla cla = null; try { cla = getSignedCla(uuid); } catch (ResultException e) { return e.getResult(); } if (!uid.equals(cla.getGitHubUid())) { session().clear(); return unauthorized( message.render("This contributor license agreement request does not belong to you")); } cla.setState(SignedCla.STATE_PENDING); cla.setUpdateComment(null); MultipartFormData data = request().body().asMultipartFormData(); Map<String, String[]> values = data.asFormUrlEncoded(); boolean employerChecked = false; try { employerChecked = values.containsKey("employerCheck"); String email = getField("email", values, 128); String signature = getField("signature", values, 0); cla.setEmail(email); cla.setSignature(signature); if (employerChecked) { cla.setState(SignedCla.STATE_PENDING_EXTERNAL); String legalContactEmail = getField("legalContactEmail", values, 128); if (email.equals(legalContactEmail)) { return badRequest(message.render("The legal contact e-mail can not match the selected e-mail")); } cla.setLegalContactEmail(legalContactEmail); cla.setLegalState(SignedCla.STATE_PENDING); } else { cla.setLegalState(null); cla.setLegalContactEmail(null); } List<ClaInputField> fields = cla.getCla().getInputFields(); Map<Long, Boolean> checkMap = new HashMap<Long, Boolean>(); for (ClaInputField field : fields) { if (field.getInputField().getRequiredForEmployer()) { if (employerChecked) { checkMap.put(field.getInputField().getId(), false); } } else { checkMap.put(field.getInputField().getId(), false); } } /* If this is a re-sign, delete the old values */ SqlUpdate sql = Ebean.createSqlUpdate("DELETE FROM SignedClaInputFields WHERE signedClaId = :id"); sql.setParameter("id", cla.getId()); sql.execute(); List<SignedClaInputField> signedFields = new ArrayList<SignedClaInputField>(); for (String key : values.keySet()) { if (key.startsWith("field")) { int id = Integer.parseInt(key.replace("field", "")); for (ClaInputField field : fields) { if (field.getInputField().getId().intValue() == id) { SignedClaInputField signedField = new SignedClaInputField(); signedField.setInputField(field.getInputField()); signedField.setSignedCla(cla); signedField.setResponse(getField(key, values, 128)); signedFields.add(signedField); checkMap.put(field.getInputField().getId(), true); } } } } cla.setInputFields(signedFields); for (Boolean hasField : checkMap.values()) { if (!hasField) { return badRequest(message.render("Required field missing")); } } } catch (IllegalStateException e) { return badRequest(message.render(e.getMessage())); } catch (NumberFormatException e) { return badRequest(message.render("Invalid field ID")); } Ebean.save(cla); String status = "success"; String statusMessage = Messages.get("github.status.review"); if (employerChecked) { status = "pending"; statusMessage = Messages.get("github.status.reviewexternal"); } for (SignedClaGitHubPullRequest pullRequest : cla.getPullRequests()) { GitHubApiUtils.updateGitHubStatus(cla.getUuid(), status, statusMessage, pullRequest.getGitHubStatusUrl()); } /* E-mail to signer */ String body = claEmail.render(cla.getProject(), cla.getGitHubLogin(), cla.getEmail()).body(); Email email = new Email(); email.setSubject("New CLA Signed"); email.setFrom(Play.application().configuration().getString("app.noreply.email")); email.addTo(cla.getEmail()); email.setBodyHtml(body); String name = "SignedCla_" + cla.getProject() + "_" + cla.getGitHubLogin() + "_" + new SimpleDateFormat("yyyy-MM-dd").format(new Date()) + ".pdf"; byte[] pdf = PdfGenerator.toBytes(claPdf.render(cla), ""); email.addAttachment(name, pdf, "application/pdf"); MailerPlugin.send(email); String confirmation = Messages.get("sign.confirmation"); if (employerChecked) { /* E-mail to external legal contact */ confirmation = Messages.get("sign.confirmation.ccla", Play.application().configuration().getString("app.ccla.expiration")); String token = getToken(uuid, cla.getLegalContactEmail()); String url = Play.application().configuration().getString("app.host") + controllers.routes.ExternalReviewController.review(uuid, token).url(); body = externalReviewEmail.render(url, cla.getProject(), cla.getGitHubLogin(), cla.getEmail()).body(); email = new Email(); email.setSubject( "Open Source Contributor License Agreement for " + cla.getProject() + " needs your review"); email.setFrom(Play.application().configuration().getString("app.noreply.email")); email.addTo(cla.getLegalContactEmail()); email.setBodyHtml(body); MailerPlugin.send(email); } else { /* E-mail to internal reviewers */ String url = Play.application().configuration().getString("app.internal.host") + controllers.routes.AdminController.review(uuid).url(); body = reviewEmail.render(url, cla.getProject(), cla.getGitHubLogin(), cla.getEmail()).body(); email = new Email(); email.setSubject("New CLA Signed"); email.setFrom(Play.application().configuration().getString("app.noreply.email")); email.addTo(Play.application().configuration().getString("app.notification.email")); email.setBodyHtml(body); MailerPlugin.send(email); } session().clear(); return ok(message.render(confirmation)); } private static boolean isInRange(SignedCla signedCla, ProjectCla projectCla) { int revision = signedCla.getCla().getRevision(); int minRevision = projectCla.getMinCla().getRevision(); int maxRevision = projectCla.getMaxCla().getRevision(); return revision >= minRevision && revision <= maxRevision; } private static boolean hasPullRequest(String pullRequestUrl, List<SignedClaGitHubPullRequest> pullRequests) { for (SignedClaGitHubPullRequest pullRequest : pullRequests) { if (pullRequestUrl.equals(pullRequest.getGitHubPullRequestUrl())) { return true; } } return false; } public static void handlePullRequest(SignedCla cla, ProjectCla projectCla, String uid, String login, String pullRequestUrl, String statusUrl, String issueUrl, String repoUrl) { String state = null; String description = null; String comment = null; if (cla == null) { state = "pending"; description = Messages.get("github.status.sign"); cla = new SignedCla(); cla.setUuid(UUID.randomUUID().toString().replace("-", "")); cla.setCla(projectCla.getMaxCla()); cla.setProject(projectCla.getProject()); cla.setGitHubUid(uid); cla.setGitHubLogin(login); cla.setPullRequests(new ArrayList<SignedClaGitHubPullRequest>()); cla.setState(SignedCla.STATE_NEW); cla.setCreated(new Date()); String signUrl = GitHubApiUtils.formatLink("here", Play.application().configuration().getString("app.host") + controllers.routes.ClaController.index(cla.getUuid()).url()); comment = Messages.get("github.issue.sign", login, signUrl); } else if (!isInRange(cla, projectCla)) { state = "pending"; description = Messages.get("github.status.outdated"); cla = new SignedCla(); cla.setUuid(UUID.randomUUID().toString().replace("-", "")); cla.setCla(projectCla.getMaxCla()); cla.setProject(projectCla.getProject()); cla.setGitHubUid(uid); cla.setGitHubLogin(login); cla.setPullRequests(new ArrayList<SignedClaGitHubPullRequest>()); cla.setState(SignedCla.STATE_NEW); cla.setCreated(new Date()); String signUrl = GitHubApiUtils.formatLink("here", Play.application().configuration().getString("app.host") + controllers.routes.ClaController.index(cla.getUuid()).url()); comment = Messages.get("github.issue.outdated", login, signUrl); } else if (cla.getState().equals(SignedCla.STATE_NEW)) { state = "pending"; description = Messages.get("github.status.sign"); String signUrl = GitHubApiUtils.formatLink("here", Play.application().configuration().getString("app.host") + controllers.routes.ClaController.index(cla.getUuid()).url()); comment = Messages.get("github.issue.sign", login, signUrl); } else if (cla.getState().equals(SignedCla.STATE_PENDING)) { state = "success"; description = Messages.get("github.status.review"); } else if (cla.getState().equals(SignedCla.STATE_PENDING_EXTERNAL)) { state = "pending"; description = Messages.get("github.status.reviewexternal"); } else if (cla.getState().equals(SignedCla.STATE_APPROVED)) { state = "success"; description = Messages.get("github.status.approved"); } else if (cla.getState().equals(SignedCla.STATE_REJECTED)) { state = "failure"; description = Messages.get("github.status.rejected"); String signUrl = GitHubApiUtils.formatLink("here", Play.application().configuration().getString("app.host") + controllers.routes.ClaController.index(cla.getUuid()).url()); comment = Messages.get("github.issue.rejected", login, signUrl); } if (!hasPullRequest(pullRequestUrl, cla.getPullRequests())) { SignedClaGitHubPullRequest pullRequest = new SignedClaGitHubPullRequest(); pullRequest.setGitHubPullRequestUrl(pullRequestUrl); pullRequest.setGitHubStatusUrl(statusUrl); pullRequest.setGitHubIssueUrl(issueUrl); cla.getPullRequests().add(pullRequest); } Ebean.save(cla); GitHubApiUtils.updateGitHubStatus(cla.getUuid(), state, description, statusUrl); GitHubApiUtils.addIssueLabel(repoUrl, CLA_REJECTED_LABEL, "fc2929"); if (comment != null) { GitHubApiUtils.addIssueComment(comment, issueUrl + "/comments"); } } @Transactional public static Result pullRequestHookCallback() { JsonNode json = request().body().asJson(); if (json == null) { return ok(); } if (!json.has("action")) { return ok(); } String action = json.get("action").asText(); if (action.equals("opened")) { JsonNode pullRequestNode = json.get("pull_request"); JsonNode userNode = pullRequestNode.get("user"); JsonNode repositoryNode = json.get("repository"); JsonNode ownerNode = repositoryNode.get("owner"); String uid = userNode.get("id").asText(); String login = userNode.get("login").asText(); String pullRequestUrl = pullRequestNode.get("url").asText(); String statusUrl = pullRequestNode.get("statuses_url").asText(); String issueUrl = pullRequestNode.get("issue_url").asText(); String repo = repositoryNode.get("name").asText(); String repoUrl = repositoryNode.get("url").asText(); String owner = ownerNode.get("login").asText(); Logger.info("Pull request " + pullRequestUrl + " opened"); /* Reject hook callbacks from other organizations */ boolean foundOrg = false; List<Organization> orgs = Organization.find.findList(); for (Organization org : orgs) { if (org.getName().equals(owner)) { foundOrg = true; break; } } if (!foundOrg) { return ok(); } /* Org members do not need to sign the CLA */ if (GitHubApiUtils.isOrgMember(owner, login)) { GitHubApiUtils.addIssueLabel(repoUrl, CLA_NOT_REQUIRED_LABEL, "159818"); GitHubApiUtils.attachIssueLabel(issueUrl + "/labels", CLA_NOT_REQUIRED_LABEL); return ok(); } SignedCla cla = null; ProjectCla projectCla = ProjectCla.find.where().eq("project", owner + "/" + repo).findUnique(); if (projectCla == null) { Cla defaultCla = Cla.find.where().eq("isDefault", true).findUnique(); projectCla = new ProjectCla(); projectCla.setMinCla(defaultCla); projectCla.setMaxCla(defaultCla); projectCla.setProject(owner + "/" + repo); cla = SignedCla.find.setForUpdate(true).where().eq("claId", defaultCla.getId()).eq("gitHubUid", uid) .ne("state", SignedCla.STATE_REVOKED).findUnique(); } else { List<Cla> clas = Cla.find.where().eq("name", projectCla.getMinCla().getName()).findList(); for (Cla c : clas) { cla = SignedCla.find.setForUpdate(true).where().eq("claId", c.getId()).eq("gitHubUid", uid) .ne("state", SignedCla.STATE_REVOKED).findUnique(); if (cla != null) { if (isInRange(cla, projectCla)) { break; } else { cla = null; } } } } handlePullRequest(cla, projectCla, uid, login, pullRequestUrl, statusUrl, issueUrl, repoUrl); } return ok(); } }