# HG changeset patch # User Jaroslav Tulach # Date 1267727878 -3600 # Node ID ecddc9f373bba27e0c45880cf2dbcda67b212558 # Parent 4a94cefe6555e9163d13b6a04226db6bf45c9707 Changing an email needs to be confirmed by receiving an email and responding to an URL send inside it diff -r 4a94cefe6555 -r ecddc9f373bb freemarkerdor/pom.xml --- a/freemarkerdor/pom.xml Tue Mar 02 17:09:06 2010 +0100 +++ b/freemarkerdor/pom.xml Thu Mar 04 19:37:58 2010 +0100 @@ -13,6 +13,11 @@ http://maven.apache.org + javax.mail + mail + 1.4.1 + + ${project.groupId} webidor ${webidorVersion} @@ -66,6 +71,12 @@ test + org.netbeans.api + org-netbeans-modules-nbjunit + RELEASE65 + test + + freemarker freemarker 2.3.8 diff -r 4a94cefe6555 -r ecddc9f373bb freemarkerdor/src/main/java/cz/xelfi/quoridor/freemarkerdor/EmailService.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/freemarkerdor/src/main/java/cz/xelfi/quoridor/freemarkerdor/EmailService.java Thu Mar 04 19:37:58 2010 +0100 @@ -0,0 +1,88 @@ +/* + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * The contents of this file are subject to the terms of either the GNU + * General Public License Version 2 only ("GPL") or the Common + * Development and Distribution License("CDDL") (collectively, the + * "License"). You may not use this file except in compliance with the + * License. You can obtain a copy of the License at + * http://www.netbeans.org/cddl-gplv2.html + * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the + * specific language governing permissions and limitations under the + * License. When distributing the software, include this License Header + * Notice in each file and include the License file at + * nbbuild/licenses/CDDL-GPL-2-CP. Sun designates this + * particular file as subject to the "Classpath" exception as provided + * by Sun in the GPL Version 2 section of the License file that + * accompanied this code. If applicable, add the following below the + * License Header, with the fields enclosed by brackets [] replaced by + * your own identifying information: + * "Portions Copyrighted [year] [name of copyright owner]" + * + * Contributor(s): + * + * Portions Copyrighted 2009 Jaroslav Tulach + */ + +package cz.xelfi.quoridor.freemarkerdor; + +import java.io.IOException; +import java.util.Iterator; +import java.util.Properties; +import java.util.ServiceLoader; +import javax.mail.Message; +import javax.mail.MessagingException; +import javax.mail.Session; +import javax.mail.Transport; +import javax.mail.internet.InternetAddress; +import javax.mail.internet.MimeMessage; +import org.openide.util.Exceptions; + +/** Send emails. + * + * @author Jaroslav Tulach + */ +public abstract class EmailService { + private static EmailService DEFAULT; + + public abstract void sendEmail( + String address, + String subject, + String text + ) throws IOException; + + public static synchronized EmailService getDefault() { + if (DEFAULT == null) { + Iterator it = ServiceLoader.load(EmailService.class).iterator(); + DEFAULT = it.hasNext() ? it.next() : new Impl(); + } + return DEFAULT; + } + + private static class Impl extends EmailService { + private final Session session; + public Impl() { + Properties props = System.getProperties(); + final String host = "mail.smtp.host"; + if (props.get(host) == null) { + props.put(host,"localhost"); + } + session = Session.getDefaultInstance(props, null); + } + + @Override + public void sendEmail(String address, String subject, String text) throws IOException { + try { + MimeMessage message = new MimeMessage(session); + message.setFrom(new InternetAddress("quoridor@xelfi.cz")); + message.addRecipient(Message.RecipientType.TO, new InternetAddress(address)); + message.setSubject(subject); + message.setText(text); + Transport.send(message); + } catch (MessagingException ex) { + throw new IOException(ex); + } + + } + } +} diff -r 4a94cefe6555 -r ecddc9f373bb freemarkerdor/src/main/java/cz/xelfi/quoridor/freemarkerdor/Requests.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/freemarkerdor/src/main/java/cz/xelfi/quoridor/freemarkerdor/Requests.java Thu Mar 04 19:37:58 2010 +0100 @@ -0,0 +1,81 @@ +/* + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * The contents of this file are subject to the terms of either the GNU + * General Public License Version 2 only ("GPL") or the Common + * Development and Distribution License("CDDL") (collectively, the + * "License"). You may not use this file except in compliance with the + * License. You can obtain a copy of the License at + * http://www.netbeans.org/cddl-gplv2.html + * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the + * specific language governing permissions and limitations under the + * License. When distributing the software, include this License Header + * Notice in each file and include the License file at + * nbbuild/licenses/CDDL-GPL-2-CP. Sun designates this + * particular file as subject to the "Classpath" exception as provided + * by Sun in the GPL Version 2 section of the License file that + * accompanied this code. If applicable, add the following below the + * License Header, with the fields enclosed by brackets [] replaced by + * your own identifying information: + * "Portions Copyrighted [year] [name of copyright owner]" + * + * Contributor(s): + * + * Portions Copyrighted 2009 Jaroslav Tulach + */ + +package cz.xelfi.quoridor.freemarkerdor; + +import com.sun.jersey.api.client.WebResource; +import com.sun.jersey.api.client.WebResource.Builder; +import java.net.URI; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.core.Cookie; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; + +/** Keeps "requests". A request is UUID that maps to a URL which can + * be executed later. + * + * @author Jaroslav Tulach + */ +public final class Requests extends Object { + private final Map requests = new HashMap(); + private final WebResource root; + + Requests(WebResource path) { + root = path; + } + + @GET + @Path("{id}") + @Produces(MediaType.TEXT_HTML) + public Response redirect(@PathParam("id") String uuid) { + UUID r = UUID.fromString(uuid); + Builder web = requests.get(r); + if (web == null) { + return Response.status(Response.Status.NOT_FOUND).build(); + } + return Response.ok(web.get(String.class)).build(); + } + + boolean isVerified(String uuid) { + return uuid != null && requests.containsKey(UUID.fromString(uuid)); + } + + URI register(String login, WebResource request) { + UUID uuid = UUID.randomUUID(); + WebResource callback = root.path(uuid.toString()); + Builder builder = request.queryParam("verified", uuid.toString()). + cookie(Cookie.valueOf("login=" + login)); + requests.put(uuid, builder); + return callback.getURI(); + } + +} diff -r 4a94cefe6555 -r ecddc9f373bb freemarkerdor/src/main/java/cz/xelfi/quoridor/freemarkerdor/UI.java --- a/freemarkerdor/src/main/java/cz/xelfi/quoridor/freemarkerdor/UI.java Tue Mar 02 17:09:06 2010 +0100 +++ b/freemarkerdor/src/main/java/cz/xelfi/quoridor/freemarkerdor/UI.java Thu Mar 04 19:37:58 2010 +0100 @@ -40,6 +40,7 @@ import java.io.InputStream; import java.io.StringWriter; import java.net.URI; +import java.text.MessageFormat; import java.util.Date; import java.util.HashMap; import java.util.List; @@ -89,6 +90,8 @@ } private static WebResource base; private static WebResource stat; + private static WebResource web; + private static Requests requests; @Context private HttpHeaders headers; @@ -98,10 +101,17 @@ public UI() { } - private Viewable checkLogin() { + private String login() { Cookie cookie = headers.getCookies().get("login"); if (cookie != null) { - String id = cookie.getValue(); + return cookie.getValue(); + } + return null; + } + + private Viewable checkLogin() { + String id = login(); + if (id != null) { UserInfo us; try { us = base.path("users").queryParam("loginID", id). @@ -342,23 +352,45 @@ return viewable("index.fmt", got, args); } + @Path("requests") + public Requests getRequests() { + if (requests == null) { + requests = new Requests(web.path("requests")); + } + return requests; + } + @GET @Path("options") public Response changeOptions( @QueryParam("email") String email, - @QueryParam("language") String language - ) { + @QueryParam("language") String language, + @QueryParam("verified") String verified + ) throws IOException { Viewable v = checkLogin(); if (v != null) { return Response.status(Response.Status.FORBIDDEN).entity(v).build(); } if (email != null) { - UserInfo ui = base.path("users/" + user.getId()). - queryParam("loginID", uuid). - queryParam("name", "email"). - queryParam("value", email).accept(MediaType.TEXT_XML).post(UserInfo.class); + if (getRequests().isVerified(verified)) { + UserInfo ui = base.path("users/" + user.getId()). + queryParam("loginID", uuid). + queryParam("name", "email"). + queryParam("value", email).accept(MediaType.TEXT_XML).post(UserInfo.class); + } else { + WebResource request; + request = web.path("options").queryParam("name", "email").queryParam("email", email); + URI callback = getRequests().register(login(), request); + + ResourceBundle rb = bundle(null); + String subject = rb.getString("MSG_ChangeEmailSubject"); + String text = MessageFormat.format(rb.getString("MSG_ChangeEmailText"), user.getId(), callback); + EmailService.getDefault().sendEmail(email, subject, text); + return Response.ok(viewable("email.fmt", null)).build(); + + } } if (language != null) { @@ -490,6 +522,8 @@ final String baseUri = "http://localhost:" + port + "/"; final HttpServer server = HttpServerFactory.create(baseUri, rc); + Client c3 = new Client(); + web = c3.resource(baseUri); server.start(); System.out.println("Quoridor started at port " + port); @@ -504,24 +538,27 @@ }; } - private Viewable viewable(String page, Document doc, Object... more) { + private ResourceBundle bundle(Locale[] locale) { ResourceBundle rb = null; String lng = user == null ? null : user.getProperty("language"); // NOI18N - Locale locale = null; if (lng != null) { - try { - Locale l = new Locale(lng); - rb = ResourceBundle.getBundle("cz.xelfi.quoridor.freemarkerdor.UI.Bundle", l); - locale = l; - } catch (MissingResourceException e) { - // OK + try { + Locale l = new Locale(lng); + rb = ResourceBundle.getBundle("cz.xelfi.quoridor.freemarkerdor.UI.Bundle", l); + if (locale != null) { + locale[0] = l; } + } catch (MissingResourceException e) { + // OK + } } if (rb == null) { for (Locale l : headers.getAcceptableLanguages()) { try { rb = ResourceBundle.getBundle("cz.xelfi.quoridor.freemarkerdor.UI.Bundle", l); - locale = l; + if (locale != null) { + locale[0] = l; + } break; } catch (MissingResourceException e) { // OK @@ -530,8 +567,16 @@ } if (rb == null) { rb = ResourceBundle.getBundle("cz.xelfi.quoridor.freemarkerdor.UI.Bundle", Locale.ENGLISH); - locale = Locale.ENGLISH; + if (locale != null) { + locale[0] = Locale.ENGLISH; + } } + return rb; + } + + private Viewable viewable(String page, Document doc, Object... more) { + Locale[] locale = new Locale[1]; + ResourceBundle rb = bundle(locale); Map map = new HashMap(); class ConvertToDate extends HashMap { @@ -542,7 +587,7 @@ } } - map.put("locale", locale.toString()); + map.put("locale", locale[0].toString()); map.put("doc", doc); if (user != null) { map.put("user", user.getId()); diff -r 4a94cefe6555 -r ecddc9f373bb freemarkerdor/src/main/resources/cz/xelfi/quoridor/freemarkerdor/UI/Bundle.properties --- a/freemarkerdor/src/main/resources/cz/xelfi/quoridor/freemarkerdor/UI/Bundle.properties Tue Mar 02 17:09:06 2010 +0100 +++ b/freemarkerdor/src/main/resources/cz/xelfi/quoridor/freemarkerdor/UI/Bundle.properties Thu Mar 04 19:37:58 2010 +0100 @@ -85,3 +85,17 @@ OPENINGS=Openings WHITE_WON=White won BLACK_WON=Black won + +MSG_ChangeEmailSubject=Quoridor Email Change +# {0} user id +# {1} request URL +MSG_ChangeEmailText=Dear Quoridor user!\n\ + User {0} have requested change in email settings.\n\ + \n\ + To confirm change of the email address, please visit\n\ + following link {1}\n\ + \n\ + Thank you for playing Quoridor! +EMAIL_SENT=Email Sent +EMAIL_SENT_TO=Email Sent to {0} +EMAIL_BACK=Return to games... \ No newline at end of file diff -r 4a94cefe6555 -r ecddc9f373bb freemarkerdor/src/main/resources/cz/xelfi/quoridor/freemarkerdor/UI/email.fmt --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/freemarkerdor/src/main/resources/cz/xelfi/quoridor/freemarkerdor/UI/email.fmt Thu Mar 04 19:37:58 2010 +0100 @@ -0,0 +1,16 @@ + + + + ${bundle.EMAIL_SENT} + + + +

${bundle("EMAIL_SENT_TO", user)}

+ + ${bundle.EMAIL_BACK} + +
+ ${bundle("copyright", version)} + + + \ No newline at end of file diff -r 4a94cefe6555 -r ecddc9f373bb freemarkerdor/src/test/java/cz/xelfi/quoridor/freemarkerdor/ChangeEmailTest.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/freemarkerdor/src/test/java/cz/xelfi/quoridor/freemarkerdor/ChangeEmailTest.java Thu Mar 04 19:37:58 2010 +0100 @@ -0,0 +1,176 @@ +/* + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * The contents of this file are subject to the terms of either the GNU + * General Public License Version 2 only ("GPL") or the Common + * Development and Distribution License("CDDL") (collectively, the + * "License"). You may not use this file except in compliance with the + * License. You can obtain a copy of the License at + * http://www.netbeans.org/cddl-gplv2.html + * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the + * specific language governing permissions and limitations under the + * License. When distributing the software, include this License Header + * Notice in each file and include the License file at + * nbbuild/licenses/CDDL-GPL-2-CP. Sun designates this + * particular file as subject to the "Classpath" exception as provided + * by Sun in the GPL Version 2 section of the License file that + * accompanied this code. If applicable, add the following below the + * License Header, with the fields enclosed by brackets [] replaced by + * your own identifying information: + * "Portions Copyrighted [year] [name of copyright owner]" + * + * Contributor(s): + * + * Portions Copyrighted 2010 Jaroslav Tulach + */ + +package cz.xelfi.quoridor.freemarkerdor; + +import com.sun.jersey.api.client.Client; +import com.sun.jersey.api.client.ClientResponse; +import com.sun.jersey.api.client.WebResource; +import com.sun.jersey.core.header.MediaTypes; +import com.sun.jersey.core.util.MultivaluedMapImpl; +import com.sun.net.httpserver.HttpServer; +import cz.xelfi.quoridor.webidor.resources.Quoridor; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.util.Locale; +import java.util.concurrent.Callable; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import javax.ws.rs.core.Cookie; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.MultivaluedMap; +import javax.ws.rs.core.UriBuilder; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; +import org.netbeans.junit.MockServices; +import static org.junit.Assert.*; + +/** + * + * @author Jaroslav Tulach + */ +public class ChangeEmailTest extends Object { + private static File dir; + private static HttpServer stopAPI; + private static HttpServer stopStatistics; + private static Callable stop; + private WebResource webResource; + private WebResource apiResource; + private WebResource statResource; + private Client client; + + public ChangeEmailTest() throws Exception { + } + + @BeforeClass + public static void localeEnglish() throws Exception { + Locale.setDefault(Locale.ENGLISH); + dir = File.createTempFile("quoridor", ".dir"); + dir.delete(); + dir.mkdirs(); + System.setProperty("quoridor.dir", dir.getPath()); + stopAPI = Quoridor.start(7990); + stopStatistics = Quoridor.start(7992); + stop = UI.startServers(7991, "http://localhost:7990", "http://localhost:7992"); + + File passwd = new File(dir, "passwd"); + FileOutputStream os = new FileOutputStream(passwd); + os.write("test=pes\nJarda=darda\n".getBytes("UTF-8")); + os.close(); + + MockServices.setServices(MockEmailer.class); + } + + @Before + public void setUp() throws Exception { + MockEmailer.clear(); + + client = new Client(); + webResource = client.resource(new URI("http://localhost:7991/")); + apiResource = client.resource(new URI("http://localhost:7990/")); + statResource = client.resource(new URI("http://localhost:7992/")); + } + + @AfterClass + public static void cleanUpAll() throws Exception { + deleteRec(dir); + if (stop != null) { + stop.call(); + } + if (stopAPI != null) { + stopAPI.stop(0); + } + MockServices.setServices(); + } + + static void deleteRec(File dir) throws IOException { + if (dir == null) { + return; + } + File[] arr = dir.listFiles(); + if (arr != null) { + for (File f : arr) { + deleteRec(f); + } + } + dir.delete(); + } + + @Test public void testChangeEmail() throws Exception { + String logTest = apiResource.path("login"). + queryParam("name", "test"). + queryParam("password", "pes"). + accept(MediaType.TEXT_PLAIN). + put(String.class); + ClientResponse res = webResource.path("options"). + queryParam("email", "xxx@xxx.cz"). + cookie(Cookie.valueOf("login=" + logTest)). + accept("text/html"). + get(ClientResponse.class); + final String txt = res.getEntity(String.class); + + assertEquals("Emails sent to", "xxx@xxx.cz", MockEmailer.address); + + int urlIndex = MockEmailer.text.indexOf(webResource.getURI().toString()); + assertTrue("Found in " + MockEmailer.text, urlIndex >= 0); + + int endIndex = urlIndex; + while (!Character.isWhitespace(MockEmailer.text.charAt(endIndex))) { + endIndex++; + } + final String url = MockEmailer.text.substring(urlIndex, endIndex); + WebResource requestURL = client.resource(url); + + String response = requestURL.accept("text/html").get(String.class); + Pattern p = Pattern.compile("1.1 1.15 1.0-SNAPSHOT - 1.58 + 1.59 1.0 1.8