Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
Binary file not shown.
Binary file not shown.
Binary file not shown.
Empty file.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Empty file.
Binary file not shown.
2 changes: 2 additions & 0 deletions java/demoapp/.gradle/buildOutputCleanup/cache.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
#Thu Apr 02 09:44:30 CLST 2026
gradle.version=8.9
Empty file.
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
import java.io.InputStream;
import java.io.OutputStream;
import java.util.StringTokenizer;
import java.util.UUID;

@WebServlet(name = "Migrate", value = "/migrate")
public class ServletMain extends HttpServlet {
Expand Down Expand Up @@ -67,6 +68,8 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws S
String file = "";
String fileName = "";
String customRecipe = "";
String sessionToken = (String) req.getSession().getAttribute("csrf_token");
boolean isTokenValid = false;

try {
upload.setSizeMax(MAX_UPLOAD_SIZE);
Expand All @@ -79,9 +82,11 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws S
|| item.getFieldName().equals("fileCountryCode"))) {
countryCode = Streams.asString(in);

} else if (item.isFormField() && item.getFieldName().equals("number")) {
number = Streams.asString(in);

} else if (item.isFormField() && item.getFieldName().equals("csrf_token")) {
String providedToken = Streams.asString(in);
if (sessionToken != null && sessionToken.equals(providedToken)) {
isTokenValid = true;
}
} else if (item.getFieldName().equals("file")) {
fileName = item.getName();
try {
Expand All @@ -102,6 +107,11 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws S
e.printStackTrace();
}

if (!isTokenValid(req)) {
resp.sendError(HttpServletResponse.SC_FORBIDDEN, "Invalid or missing CSRF token.");
return;
}

if (!number.isEmpty() && !countryCode.isEmpty()) {
/*
number and country code are being set again to allow users to see their inputs after the http request has
Expand Down Expand Up @@ -131,6 +141,12 @@ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws Se
matcher.removeFrom(req.getParameter("fileName"));
String fileContent = req.getParameter("fileContent");

if (fileContent == null) {
req.setAttribute("csrf_token", getOrGenerateCsrfToken(req));
req.getRequestDispatcher("index.jsp").forward(req, resp);
return;
}

resp.setContentType("text/plain");
resp.setHeader("Content-Disposition", "attachment; filename=" + fileName);
try {
Expand Down Expand Up @@ -258,4 +274,36 @@ public static ImmutableList<String> getMigrationResultOutputList(ImmutableList<M
if (resultList == null) return ImmutableList.of();
return resultList.stream().map(MigrationResult::toString).collect(ImmutableList.toImmutableList());
}

protected String getOrGenerateCsrfToken(HttpServletRequest req) {
String csrfToken = (String) req.getSession().getAttribute("csrf_token");
if (csrfToken == null) {
csrfToken = UUID.randomUUID().toString();
req.getSession().setAttribute("csrf_token", csrfToken);
}
return csrfToken;
}

protected boolean isTokenValid(HttpServletRequest req) {
String sessionToken = (String) req.getSession().getAttribute("csrf_token");
if (sessionToken == null) {
return false;
}

try {
ServletFileUpload upload = new ServletFileUpload();
FileItemIterator iterator = upload.getItemIterator(req);
while (iterator.hasNext()) {
FileItemStream item = iterator.next();
if (item.isFormField() && item.getFieldName().equals("csrf_token")) {
try (InputStream in = item.openStream()) {
return sessionToken.equals(Streams.asString(in));
}
}
}
} catch (FileUploadException | IOException e) {
return false;
}
return false;
}
}
83 changes: 39 additions & 44 deletions migrator/migrator-servlet/src/main/webapp/index.jsp
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
<%@ page import="com.google.phonenumbers.migrator.MigrationResult" %>
<!DOCTYPE html>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%
final String E164_NUMBERS_LINK = "https://support.twilio.com/hc/en-us/articles/223183008-Formatting-International-Phone-Numbers";
final String COUNTRY_CODE_LINK = "https://countrycode.org/";
Expand Down Expand Up @@ -140,62 +141,55 @@
</div>

<div class="migration-result">
<%
if (request.getAttribute("numberError") == null && request.getAttribute("number") != null) {
if (request.getAttribute("validMigration") != null) {
out.print("<h3 class='valid'>Valid +" + request.getAttribute("numberCountryCode") + " Phone Number Produced!</h3>");
out.print("<p>The stale number '" + request.getAttribute("number") + "' was successfully migrated into the" +
" phone number: +" + request.getAttribute("validMigration") + "</p>");
} else if (request.getAttribute("invalidMigration") != null) {
out.print("<h3 class='invalid-migration'>Invalid +" + request.getAttribute("numberCountryCode") + " Migration</h3>");
out.print("<p>The stale number '" + request.getAttribute("number") + "' was migrated into the phone number:" +
" +" + request.getAttribute("invalidMigration") + ". However this was not seen as valid using our internal" +
" metadata for country code +" + request.getAttribute("numberCountryCode") + ".</p>");
} else if (request.getAttribute("alreadyValidNumber") != null) {
out.print("<h3 class='valid'>Already Valid +" + request.getAttribute("numberCountryCode") + " Phone Number!</h3>");
out.print("<p>The entered phone number was already seen as being in a valid, dialable format based on our" +
" metadata for country code +" + request.getAttribute("numberCountryCode") + ". Here is the number in" +
" its clean E.164 format: +" + request.getAttribute("alreadyValidNumber") + "</p>");
} else {
out.print("<h3 class='invalid-number'>Non-migratable +" + request.getAttribute("numberCountryCode") + " Phone Number</h3>");
out.print("<p>The phone number '" + request.getAttribute("number") + "' was not seen as a valid number and" +
" no migration recipe could be found for country code +" + request.getAttribute("numberCountryCode") +
" to migrate it. This may be because you have entered a country code which does not correctly correspond" +
" to the given phone number or the specified number has never been valid.</p>");
}
out.print("<p style='color: red; font-size: 14px'>Think there's an issue? File one <a href='" + ISSUE_TRACKER_LINK +
"' target='_blank'>here</a> following the given <a href='" + GUIDELINES_LINK + "' target='_blank'>guidelines</a>.</p>");
} else if (request.getAttribute("fileError") == null && request.getAttribute("fileName") != null) {
out.print("<h3>'" + request.getAttribute("fileName") + "' Migration Report for Country Code: +" + request.getAttribute("fileCountryCode") + "</h3>");
out.print("<p>Below is a chart showing the ratio of numbers from the entered file that were able to be migrated" +
" using '+" + request.getAttribute("fileCountryCode") + "' migration recipes. To understand more," +
" select a given segment from the chart below.</p>");
out.print("<div class='chart-wrap'><div id='migration-chart' class='chart'></div></div>");

out.print("<form action='" + request.getContextPath() + "/migrate' method='get' style='margin-bottom: 1rem'>");
out.print("<input type='hidden' name='countryCode' value='" + request.getAttribute("fileCountryCode") + "'/>");
out.print("<input type='hidden' name='fileName' value='" + request.getAttribute("fileName") + "'/>");
out.print("<input type='hidden' name='fileContent' value='" + request.getAttribute("fileContent") + "'/>");
out.print("<input type='submit' value='Export Results' class='button'/>");
out.print("</form>");
}
%>
<c:if test="${empty numberError and not empty number}">
<c:choose>
<c:when test="${not empty validMigration}">
<h3 class='valid'>Valid +<c:out value="${numberCountryCode}"/> Phone Number Produced!</h3>
<p>The stale number '<c:out value="${number}"/>' was successfully migrated into the phone number: +<c:out value="${validMigration}"/></p>
</c:when>
<c:when test="${not empty invalidMigration}">
<h3 class='invalid-migration'>Invalid +<c:out value="${numberCountryCode}"/> Migration</h3>
<p>The stale number '<c:out value="${number}"/>' was migrated into the phone number: +<c:out value="${invalidMigration}"/>. However this was not seen as valid using our internal metadata for country code +<c:out value="${numberCountryCode}"/>.</p>
</c:when>
<c:when test="${not empty alreadyValidNumber}">
<h3 class='valid'>Already Valid +<c:out value="${numberCountryCode}"/> Phone Number!</h3>
<p>The entered phone number was already seen as being in a valid, dialable format based on our metadata for country code +<c:out value="${numberCountryCode}"/>. Here is the number in its clean E.164 format: +<c:out value="${alreadyValidNumber}"/></p>
</c:when>
<c:otherwise>
<h3 class='invalid-number'>Non-migratable +<c:out value="${numberCountryCode}"/> Phone Number</h3>
<p>The phone number '<c:out value="${number}"/>' was not seen as a valid number and no migration recipe could be found for country code +<c:out value="${numberCountryCode}"/> to migrate it. This may be because you have entered a country code which does not correctly correspond to the given phone number or the specified number has never been valid.</p>
</c:otherwise>
</c:choose>
<p style='color: red; font-size: 14px'>Think there's an issue? File one <a href='<%= ISSUE_TRACKER_LINK %>' target='_blank'>here</a> following the given <a href='<%= GUIDELINES_LINK %>' target='_blank'>guidelines</a>.</p>
</c:if>
<c:if test="${empty fileError and not empty fileName}">
<h3>'<c:out value="${fileName}"/>' Migration Report for Country Code: +<c:out value="${fileCountryCode}"/></h3>
<p>Below is a chart showing the ratio of numbers from the entered file that were able to be migrated using '+<c:out value="${fileCountryCode}"/>' migration recipes. To understand more, select a given segment from the chart below.</p>
<div class='chart-wrap'><div id='migration-chart' class='chart'></div></div>
<form action='${pageContext.request.contextPath}/migrate' method='get' style='margin-bottom: 1rem'>
<input type='hidden' name='countryCode' value='<c:out value="${fileCountryCode}"/>'/>
<input type='hidden' name='fileName' value='<c:out value="${fileName}"/>'/>
<input type='hidden' name='fileContent' value='<c:out value="${fileContent}"/>'/>
<input type='submit' value='Export Results' class='button'/>
</form>
</c:if>
</div>

<div class="migration-forms">
<div class="migration-form">
<h3>Single Number Migration</h3>
<div class="error-message"><%=request.getAttribute("numberError") == null ? "" : request.getAttribute("numberError")%></div>
<div class="error-message"><c:out value="${numberError}"/></div>
<form action="${pageContext.request.contextPath}/migrate" method="post" enctype="multipart/form-data">
<input type="hidden" name="csrf_token" value="<c:out value='${csrf_token}'/>"/>
<label for="number">Phone number:</label>
<p>Enter a phone number in E.164 format. Inputted numbers can include spaces, curved brackets and hyphens</p>
<input type="text" name="number" id="number" placeholder="+841205555555" required
value="<%=request.getAttribute("number") == null ? "" : request.getAttribute("number")%>"/>
value="<c:out value='${number}'/>"/>

<label for="numberCountryCode">Country Code:</label>
<p>Enter the BCP-47 country code in which the specified E.164 phone number belongs to</p>
<input type="number" name="numberCountryCode" id="numberCountryCode" placeholder="84" required
value="<%=request.getAttribute("numberCountryCode") == null ? "" : request.getAttribute("numberCountryCode")%>"/>
value="<c:out value='${numberCountryCode}'/>"/>

<label for="numberCustomRecipe">Custom Recipe:</label>
<p>
Expand All @@ -210,8 +204,9 @@

<div class="migration-form">
<h3>File Migration</h3>
<div class="error-message"><%=request.getAttribute("fileError") == null ? "" : request.getAttribute("fileError")%></div>
<div class="error-message"><c:out value="${fileError}"/></div>
<form action="${pageContext.request.contextPath}/migrate" method="post" enctype="multipart/form-data">
<input type="hidden" name="csrf_token" value="<c:out value='${csrf_token}'/>"/>
<label for="file">File:</label>
<p>Upload a file containing one E.164 phone number per line. Numbers can include spaces, curved brackets and hyphens</p>
<input type="file" name="file" id="file" accept="text/plain" required/>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package com.google.phonenumbers;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;

import org.junit.Before;
import org.junit.Test;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;

public class ServletMainTest {

private ServletMain servlet;
private Map<String, Object> sessionAttributes;
private Map<String, Object> requestAttributes;

@Before
public void setUp() {
servlet = new ServletMain();
sessionAttributes = new HashMap<>();
requestAttributes = new HashMap<>();
}

@Test
public void testGetOrGenerateCsrfToken_NewSession() {
HttpServletRequest mockRequest = createMockRequest();

String token = servlet.getOrGenerateCsrfToken(mockRequest);

assertNotNull(token);
assertEquals(token, sessionAttributes.get("csrf_token"));
}

@Test
public void testGetOrGenerateCsrfToken_ExistingSession() {
String existingToken = UUID.randomUUID().toString();
sessionAttributes.put("csrf_token", existingToken);
HttpServletRequest mockRequest = createMockRequest();

String token = servlet.getOrGenerateCsrfToken(mockRequest);

assertEquals(existingToken, token);
}

@Test
public void testIsTokenValid_NoSessionToken() {
HttpServletRequest mockRequest = createMockRequest();
// No token in session

assertFalse(servlet.isTokenValid(mockRequest));
}

// Note: Testing isTokenValid with a real multipart request would require
// complex mocking of ServletFileUpload and FileItemIterator.
// For the purpose of this PR, we've extracted the logic to ensure
// the core session-handling part is testable.

private HttpServletRequest createMockRequest() {
return (HttpServletRequest) java.lang.reflect.Proxy.newProxyInstance(
HttpServletRequest.class.getClassLoader(),
new Class[] { HttpServletRequest.class },
(proxy, method, args) -> {
if (method.getName().equals("getSession")) {
return createMockSession();
} else if (method.getName().equals("setAttribute")) {
requestAttributes.put((String) args[0], args[1]);
return null;
} else if (method.getName().equals("getAttribute")) {
return requestAttributes.get(args[0]);
}
return null;
});
}

private HttpSession createMockSession() {
return (HttpSession) java.lang.reflect.Proxy.newProxyInstance(
HttpSession.class.getClassLoader(),
new Class[] { HttpSession.class },
(proxy, method, args) -> {
if (method.getName().equals("getAttribute")) {
return sessionAttributes.get(args[0]);
} else if (method.getName().equals("setAttribute")) {
sessionAttributes.put((String) args[0], args[1]);
return null;
}
return null;
});
}
}
Loading