diff --git a/db-updates/0006-add-user-coach.sql b/db-updates/0006-add-user-coach.sql index 7e44c668..b43e604b 100644 --- a/db-updates/0006-add-user-coach.sql +++ b/db-updates/0006-add-user-coach.sql @@ -1,4 +1,9 @@ /* * Add coach email address field to support email cc option. */ -ALTER TABLE user ADD COLUMN coach_email VARCHAR(250) AFTER screen_name; + +/* + * This DDL has been moved to the Cloud Session Server project + */ + +-- ALTER TABLE user ADD COLUMN coach_email VARCHAR(250) AFTER screen_name; diff --git a/db-updates/0007-coppa-support.sql b/db-updates/0007-coppa-support.sql new file mode 100644 index 00000000..895ccaef --- /dev/null +++ b/db-updates/0007-coppa-support.sql @@ -0,0 +1,35 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ + +/** + * Add fields to user table to support US COPPA compliance + * + * Author: Jim Ewald + * Created: Apr 27, 2017A + * + * birth_month - is a range of [1 - 12] + * birth_year - is a range from [1930 - current year] + * parent_email - is used to register a child under the ae of 13. This is the + * email address of the parent, guardian or instructor that is + * creating the account on behalf of the child + * + * parent_email_source - is a integer designator to characterize the parent + * email adress noted above. Current options are: + * 0 - undefined + * 1 - child's parent + * 2 - child's guardian + * 3 - child's instructor or teacher + */ + +/* + * This DDL has been moved to the Cloud Session Server project + */ + +-- ALTER TABLE cloudsession.user ADD birth_month INT NOT NULL; +-- ALTER TABLE cloudsession.user ADD birth_year INT NOT NULL; +-- ALTER TABLE cloudsession.user ADD parent_email VARCHAR(250) NULL; +-- ALTER TABLE cloudsession.user ADD parent_email_source INT DEFAULT 0 NULL; +-- ALTER TABLE cloudsession.user DROP coach_email; \ No newline at end of file diff --git a/pom.xml b/pom.xml index 0fec4254..6b74f138 100644 --- a/pom.xml +++ b/pom.xml @@ -127,7 +127,7 @@ .* blocklyprop - + GregorianCalendar @@ -361,6 +361,13 @@ commons-io 1.3.2 + + + commons-validator + commons-validator + 1.6 + + diff --git a/src/main/java/com/parallax/server/blocklyprop/config/ServletsModule.java b/src/main/java/com/parallax/server/blocklyprop/config/ServletsModule.java index 3b674f3d..d17b25cc 100644 --- a/src/main/java/com/parallax/server/blocklyprop/config/ServletsModule.java +++ b/src/main/java/com/parallax/server/blocklyprop/config/ServletsModule.java @@ -7,6 +7,7 @@ import com.google.inject.servlet.ServletModule; import com.parallax.server.blocklyprop.servlets.AuthenticationServlet; +import com.parallax.server.blocklyprop.servlets.PrivacyPolicyServlet; import com.parallax.server.blocklyprop.servlets.ConfirmRequestServlet; import com.parallax.server.blocklyprop.servlets.ConfirmServlet; import com.parallax.server.blocklyprop.servlets.HelpSearchServlet; @@ -32,7 +33,8 @@ import com.parallax.server.blocklyprop.servlets.TextileLicenseServlet; /** - * + * Map each URI to a class that will handle the request + * * @author Michel */ public class ServletsModule extends ServletModule { @@ -78,7 +80,10 @@ protected void configureServlets() { // API Endpoints // Get the time left in a session - serve("/sessionapi").with(SessionStateServlet.class); + serve("/sessionapi").with(SessionStateServlet.class); + + // COPPA support + serve("/privacy-policy").with(PrivacyPolicyServlet.class); } } diff --git a/src/main/java/com/parallax/server/blocklyprop/db/dao/impl/ProjectDaoImpl.java b/src/main/java/com/parallax/server/blocklyprop/db/dao/impl/ProjectDaoImpl.java index 04bdaf7d..76ea0026 100644 --- a/src/main/java/com/parallax/server/blocklyprop/db/dao/impl/ProjectDaoImpl.java +++ b/src/main/java/com/parallax/server/blocklyprop/db/dao/impl/ProjectDaoImpl.java @@ -34,8 +34,19 @@ @Singleton public class ProjectDaoImpl implements ProjectDao { + /** + * + */ private static final int Min_BlocklyCodeSize = 48; + + /** + * + */ private static final Logger LOG = LoggerFactory.getLogger(ProjectDao.class); + + /** + * + */ private DSLContext create; @Inject @@ -97,8 +108,10 @@ public ProjectRecord createProject( LOG.info("Creating a new project with existing code."); Long idUser = BlocklyPropSecurityUtils.getCurrentUserId(); Long idCloudUser = BlocklyPropSecurityUtils.getCurrentSessionUserId(); - - ProjectRecord record = create + ProjectRecord record = null; + try { + + record = create .insertInto(Tables.PROJECT, Tables.PROJECT.ID_USER, Tables.PROJECT.ID_CLOUDUSER, @@ -122,8 +135,13 @@ public ProjectRecord createProject( sharedProject) .returning() .fetchOne(); - - return record; + } + catch (org.jooq.exception.DataAccessException sqex) { + LOG.error("Database error encountered {}", sqex.getMessage()); + } + finally { + return record; + } } /** @@ -771,6 +789,12 @@ private String fixPropcProjectBlocks(String newCode, ProjectType projType) { newCode = newCode.replaceAll("field name=\"UNIT\">CM_cm", + "field name=\"COMMENT_TEXT\">"); + newCode = newCode.replaceAll("block type=\"controls_boolean_if\"", "block type=\"controls_if\""); diff --git a/src/main/java/com/parallax/server/blocklyprop/db/dao/impl/SessionDaoImpl.java b/src/main/java/com/parallax/server/blocklyprop/db/dao/impl/SessionDaoImpl.java index b78729fc..42bf8faa 100644 --- a/src/main/java/com/parallax/server/blocklyprop/db/dao/impl/SessionDaoImpl.java +++ b/src/main/java/com/parallax/server/blocklyprop/db/dao/impl/SessionDaoImpl.java @@ -17,8 +17,6 @@ import java.util.Arrays; import java.util.Collection; import java.util.HashMap; -//import java.util.logging.Level; -//import java.util.logging.Logger; import org.apache.commons.configuration.Configuration; import org.jooq.DSLContext; import org.slf4j.Logger; @@ -34,8 +32,14 @@ public class SessionDaoImpl implements SessionDao { // Get a logger instance private static final Logger LOG = LoggerFactory.getLogger(SessionDaoImpl.class); + /** + * + */ private DSLContext create; + /** + * An instance of the application configuration settings + */ private Configuration configuration; @Inject @@ -43,11 +47,19 @@ public void setDSLContext(DSLContext dsl) { this.create = dsl; } + /** + * + * @param configuration + */ @Inject public void setConfiguration(Configuration configuration) { this.configuration = configuration; } + /** + * + * @param session + */ @Override public void create(SessionRecord session) { LOG.info("Create a session. Timeout set to: {}", session.getTimeout()); @@ -55,74 +67,111 @@ public void create(SessionRecord session) { // Log session details if the configuration file permits it printSessionInfo("create", session); - create.insertInto(Tables.SESSION) - .columns( - Tables.SESSION.IDSESSION, - Tables.SESSION.STARTTIMESTAMP, - Tables.SESSION.LASTACCESSTIME, - Tables.SESSION.TIMEOUT, - Tables.SESSION.HOST, - Tables.SESSION.ATTRIBUTES) - .values( - session.getIdsession(), - session.getStarttimestamp(), - session.getLastaccesstime(), - session.getTimeout(), - session.getHost(), - session.getAttributes()) - .execute(); + try { + create.insertInto(Tables.SESSION) + .columns( + Tables.SESSION.IDSESSION, + Tables.SESSION.STARTTIMESTAMP, + Tables.SESSION.LASTACCESSTIME, + Tables.SESSION.TIMEOUT, + Tables.SESSION.HOST, + Tables.SESSION.ATTRIBUTES) + .values( + session.getIdsession(), + session.getStarttimestamp(), + session.getLastaccesstime(), + session.getTimeout(), + session.getHost(), + session.getAttributes()) + .execute(); + } + catch (org.jooq.exception.DataAccessException sqex) { + LOG.error("Database exception {}", sqex.getMessage()); + } } + /** + * + * @param idSession + * @return + * @throws NullPointerException + */ @Override public SessionRecord readSession(String idSession) throws NullPointerException { LOG.debug("Getting session details"); + SessionRecord sessionRecord = null; + + try { + sessionRecord = create.selectFrom(Tables.SESSION) + .where(Tables.SESSION.IDSESSION.eq(idSession)) + .fetchOne(); - SessionRecord sessionRecord - = create.selectFrom(Tables.SESSION) - .where(Tables.SESSION.IDSESSION - .eq(idSession)) - .fetchOne(); - - // Log session details if the configuration file permits it - printSessionInfo("read", sessionRecord); - - return sessionRecord; - } + // Log session details if the configuration file permits it + printSessionInfo("read", sessionRecord); + } + catch (org.jooq.exception.DataAccessException sqex) { + LOG.error("Database exception {}", sqex.getMessage()); + } + finally { + return sessionRecord; + } + } + /** + * + * @param session + * @throws NullPointerException + */ @Override public void updateSession(SessionRecord session) throws NullPointerException { LOG.debug("Update a session"); + + try { + // Get the current session record + SessionRecord dbRecord = readSession(session.getIdsession()); + + if (dbRecord == null) { + throw new NullPointerException("Session not found"); + } + + dbRecord.setStarttimestamp(session.getStarttimestamp()); + dbRecord.setLastaccesstime(session.getLastaccesstime()); + dbRecord.setTimeout(session.getTimeout()); + dbRecord.setHost(session.getHost()); + dbRecord.setAttributes(session.getAttributes()); - SessionRecord dbRecord = readSession(session.getIdsession()); + // Log session details if the configuration file permits it + printSessionInfo("update from", session); + printSessionInfo("update to", dbRecord); - if (dbRecord == null) { - throw new NullPointerException(); + dbRecord.update(); + } + catch (org.jooq.exception.DataAccessException sqex) { + LOG.error("Database exception {}", sqex.getMessage()); + throw new NullPointerException("Database error"); } - - dbRecord.setStarttimestamp(session.getStarttimestamp()); - dbRecord.setLastaccesstime(session.getLastaccesstime()); - dbRecord.setTimeout(session.getTimeout()); - dbRecord.setHost(session.getHost()); - dbRecord.setAttributes(session.getAttributes()); - - // Log session details if the configuration file permits it - printSessionInfo("update from", session); - printSessionInfo("update to", dbRecord); - - dbRecord.update(); } + /** + * + * @param idSession + */ @Override public void deleteSession(String idSession) { LOG.info("Deleting session {}", idSession); create.deleteFrom(Tables.SESSION).where(Tables.SESSION.IDSESSION.eq(idSession)).execute(); } + /** + * + * @return + */ @Override public Collection getActiveSessions() { return Arrays.asList(create.selectFrom(Tables.SESSION).fetchArray()); } + private void printSessionInfo(String action, SessionRecord session) { if (configuration.getBoolean("debug.session", false)) { try { diff --git a/src/main/java/com/parallax/server/blocklyprop/db/dao/impl/UserDaoImpl.java b/src/main/java/com/parallax/server/blocklyprop/db/dao/impl/UserDaoImpl.java index 6004370c..aea30f49 100644 --- a/src/main/java/com/parallax/server/blocklyprop/db/dao/impl/UserDaoImpl.java +++ b/src/main/java/com/parallax/server/blocklyprop/db/dao/impl/UserDaoImpl.java @@ -29,10 +29,17 @@ @Singleton public class UserDaoImpl implements UserDao { - private static final Logger log = LoggerFactory.getLogger(UserDao.class); - + /** + * Logging facility + */ + private static final Logger LOG = LoggerFactory.getLogger(UserDao.class); + + /** + * Database connection context + */ private DSLContext create; + @Inject public void setDSLContext(DSLContext dsl) { this.create = dsl; @@ -41,8 +48,15 @@ public void setDSLContext(DSLContext dsl) { @Override public UserRecord create(Long idCloudSession) { //create.insertInto(Tables.PROJECT).set(project).execute(); - UserRecord record = create.insertInto(Tables.USER, Tables.USER.IDCLOUDSESSION) - .values(idCloudSession).returning().fetchOne(); + + if (idCloudSession == 0) { + LOG.warn("Cannot create cloud session user. Invalid cloud session ID."); + return null; + } + + UserRecord record = create.insertInto( + Tables.USER, + Tables.USER.IDCLOUDSESSION).values(idCloudSession).returning().fetchOne(); if (record != null && record.getId() != null && record.getId() > 0) { Set roles = new HashSet<>(); @@ -52,7 +66,7 @@ public UserRecord create(Long idCloudSession) { } catch (UnauthorizedException ue) { // Can be dismissed because of hard coded user role // Print exception in case anything should change - log.error("Creating a user should have no problem with creating its role (only USER role)", ue); + LOG.error("Creating a user should have no problem with creating its role (only USER role)", ue); } } diff --git a/src/main/java/com/parallax/server/blocklyprop/jsp/Properties.java b/src/main/java/com/parallax/server/blocklyprop/jsp/Properties.java index 16d6fc68..220f1c3b 100644 --- a/src/main/java/com/parallax/server/blocklyprop/jsp/Properties.java +++ b/src/main/java/com/parallax/server/blocklyprop/jsp/Properties.java @@ -6,7 +6,10 @@ package com.parallax.server.blocklyprop.jsp; import com.google.inject.Inject; +import com.parallax.client.cloudsession.objects.User; +import com.parallax.server.blocklyprop.security.BlocklyPropSecurityUtils; import org.apache.commons.configuration.Configuration; +import org.slf4j.LoggerFactory; /** * @@ -77,4 +80,25 @@ public static boolean isExperimentalMenu(Boolean state) { return false; } + public static boolean isCoppaRestricted() { + LoggerFactory.getLogger(Properties.class).info("Checking for COPPA restrictions"); + + // Get the current user context + User user = BlocklyPropSecurityUtils.getUserInfo(); + LoggerFactory.getLogger(Properties.class).info("Completed call to getUserInfo()"); + + // Do not restrict is we do not have a valid user id + + if (user == null) { + LoggerFactory.getLogger(Properties.class).info("Anonymous user. No COPPA restrictions"); + return false; + } + +// LoggerFactory.getLogger(Properties.class).info("User screen name is: {}.", user.getScreenname()); +// LoggerFactory.getLogger(Properties.class).info("User COPPA requirement: {}.", user.isCoppaEligible()); +// LoggerFactory.getLogger(Properties.class).info("User COPPA month: {}.", user.getBirthMonth()); +// LoggerFactory.getLogger(Properties.class).info("User COPPA year: {}.", user.getBirthYear()); + + return user.isCoppaEligible(); + } } diff --git a/src/main/java/com/parallax/server/blocklyprop/rest/RestProject.java b/src/main/java/com/parallax/server/blocklyprop/rest/RestProject.java index d79cf275..429046c9 100644 --- a/src/main/java/com/parallax/server/blocklyprop/rest/RestProject.java +++ b/src/main/java/com/parallax/server/blocklyprop/rest/RestProject.java @@ -196,7 +196,7 @@ public Response saveProject( @FormParam("type") ProjectType type, @FormParam("board") String board) { - LOG.info("Saving project {}.", idProject); + LOG.info("Received POST REST call. Saving project {}.", idProject); try { boolean privateProject = false; diff --git a/src/main/java/com/parallax/server/blocklyprop/security/BlocklyPropSessionDao.java b/src/main/java/com/parallax/server/blocklyprop/security/BlocklyPropSessionDao.java index 6abb9e58..8deb424b 100644 --- a/src/main/java/com/parallax/server/blocklyprop/security/BlocklyPropSessionDao.java +++ b/src/main/java/com/parallax/server/blocklyprop/security/BlocklyPropSessionDao.java @@ -62,7 +62,7 @@ public Session readSession(Serializable sessionId) throws UnknownSessionExceptio if (sessionRecord != null) { return convert(sessionRecord); } else { - LOG.warn("Unable to fin session: {}", sessionId); + LOG.warn("Unable to find session: {}", sessionId); throw new UnknownSessionException(); } } catch (NullPointerException npe) { @@ -74,7 +74,14 @@ public Session readSession(Serializable sessionId) throws UnknownSessionExceptio @Override public void update(Session session) throws UnknownSessionException { LOG.debug("Update session: {}", session.getId()); - SessionServiceImpl.getSessionService().updateSession(convert(session)); + try { + // updateSession() can throw a NullPointerException if something goes wrong + SessionServiceImpl.getSessionService().updateSession(convert(session)); + } + catch (NullPointerException npe) { + LOG.error("Unable to update the session. Error message: {}", npe.getMessage()); + throw new UnknownSessionException("Unable to update the session"); + } } @Override diff --git a/src/main/java/com/parallax/server/blocklyprop/services/SecurityService.java b/src/main/java/com/parallax/server/blocklyprop/services/SecurityService.java index 9442a92c..f8238d1f 100644 --- a/src/main/java/com/parallax/server/blocklyprop/services/SecurityService.java +++ b/src/main/java/com/parallax/server/blocklyprop/services/SecurityService.java @@ -12,6 +12,7 @@ import com.parallax.client.cloudsession.exceptions.PasswordVerifyException; import com.parallax.client.cloudsession.exceptions.ScreennameUsedException; import com.parallax.client.cloudsession.exceptions.UnknownUserException; +import com.parallax.client.cloudsession.exceptions.UnknownUserIdException; import com.parallax.client.cloudsession.exceptions.UserBlockedException; import com.parallax.client.cloudsession.exceptions.WrongAuthenticationSourceException; import com.parallax.client.cloudsession.objects.User; @@ -19,19 +20,18 @@ /** * * @author Michel + * + * Interface to the Cloud Session service to create new user accounts and to + * authenticate existing accounts. */ public interface SecurityService { - Long register( - String screenname, - String email, - String password, - String passwordConfirm) throws - NonUniqueEmailException, - PasswordVerifyException, - PasswordComplexityException, - ScreennameUsedException; - + User authenticateLocalUser( + Long idUser) throws + UnknownUserIdException, + UserBlockedException, + EmailNotConfirmedException; + User authenticateLocalUser( String email, String password) throws @@ -41,4 +41,18 @@ User authenticateLocalUser( InsufficientBucketTokensException, WrongAuthenticationSourceException; -} + Long register( + String screenname, + String email, + String password, + String passwordConfirm, + int birthMonth, + int birthYear, + String parentEmail, + int parentEmailSource) throws + NonUniqueEmailException, + PasswordVerifyException, + PasswordComplexityException, + ScreennameUsedException, + IllegalStateException; +} \ No newline at end of file diff --git a/src/main/java/com/parallax/server/blocklyprop/services/impl/SecurityServiceImpl.java b/src/main/java/com/parallax/server/blocklyprop/services/impl/SecurityServiceImpl.java index 4857f8b2..a69ffa95 100644 --- a/src/main/java/com/parallax/server/blocklyprop/services/impl/SecurityServiceImpl.java +++ b/src/main/java/com/parallax/server/blocklyprop/services/impl/SecurityServiceImpl.java @@ -29,7 +29,9 @@ import com.parallax.server.blocklyprop.SessionData; import com.parallax.server.blocklyprop.db.dao.UserDao; import com.parallax.server.blocklyprop.services.SecurityService; +import java.util.Calendar; import org.apache.commons.configuration.Configuration; +import org.apache.commons.validator.routines.EmailValidator; import org.apache.shiro.SecurityUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -43,20 +45,51 @@ @Transactional public class SecurityServiceImpl implements SecurityService { - private static final Logger log = LoggerFactory.getLogger(SecurityServiceImpl.class); + /** + * + */ + private static final Logger LOG = LoggerFactory.getLogger(SecurityServiceImpl.class); + /** + * + */ private static SecurityServiceImpl instance; + /** + * + */ private Provider sessionData; + /** + * + */ private Configuration configuration; + + /** + * + */ + private EmailValidator emailValidator = EmailValidator.getInstance(); + /** + * + */ private CloudSessionRegisterService registerService; + + /** + * + */ private CloudSessionAuthenticateService authenticateService; + + /** + * + */ private CloudSessionUserService userService; + /** + * + */ private UserDao userDao; - + /** * Constructor * @@ -99,7 +132,7 @@ public void setUserDao(UserDao userDao) { */ @Inject public void setConfiguration(Configuration configuration) { - log.debug("Setting cloud session configuration"); + LOG.debug("Setting cloud session configuration"); this.configuration = configuration; // Set the source for the cloud session registration services @@ -119,51 +152,128 @@ public void setConfiguration(Configuration configuration) { } /** - * Register a user account + * Validate new user data and create a new user account * - * @param screenname - * @param email - * @param password - * @param passwordConfirm - * @return An ID for the new account or null if unsuccessful + * @param screenname String user screen name + * @param email String user email address + * @param password String user password + * @param passwordConfirm String user password confirmation + * @param birthMonth int Month component of user's birthday. COPPA field + * @param birthYear int Year component of the user's birthday. COPPA field + * @param parentEmail String Sponsor's email address. COPPA field + * @param parentEmailSource int Sponsor classification. COPPA + * @return * @throws NonUniqueEmailException * @throws PasswordVerifyException * @throws PasswordComplexityException - * @throws ScreennameUsedException + * @throws ScreennameUsedException + * @throws IllegalStateException */ @Override public Long register( String screenname, String email, String password, - String passwordConfirm) throws + String passwordConfirm, + int birthMonth, + int birthYear, + String parentEmail, + int parentEmailSource) throws NonUniqueEmailException, PasswordVerifyException, PasswordComplexityException, - ScreennameUsedException { + ScreennameUsedException, + IllegalStateException{ - log.info("Resgistering new user: {}", screenname); + User user = new User(); + assert(!user.isCoppaEligible(1,2017)); + + // Log a few things + LOG.info("In register: parameter screen name: {}", screenname); + LOG.info("In register: parameter email: {}", email); + LOG.info("In register: parameter month: {}", birthMonth); + LOG.info("In register: parameter year: {}", birthYear); + LOG.info("In register: parameter sponsor email: {}", parentEmail); + LOG.info("In register: parameter sponsor type selection: {}", parentEmailSource); // Perform basic sanity checks on inputs - Preconditions.checkNotNull(screenname, "Screenname cannot be null"); - Preconditions.checkNotNull(email, "Email cannot be null"); - Preconditions.checkNotNull(password, "Password cannot be null"); - Preconditions.checkNotNull(passwordConfirm, "PasswordConfirm cannot be null"); + // Throws NullPointerException if screenname is null + LOG.info("Resgistering new user: {}", screenname); + Preconditions.checkNotNull(screenname, "ScreenNameNull"); + + // User email address is required and must be reasonably valid + LOG.info("Verifying email address has been supplied"); + Preconditions.checkNotNull(email, "UserEmailNull"); + + LOG.info("Verifying email address is reasonable"); + Preconditions.checkState( + emailValidator.isValid(email), + "Email address format is incorrect"); + + LOG.info("Verifying that a password was provided"); + Preconditions.checkNotNull(password, "PasswordIsNull"); + + LOG.info("Verify that second copy of password was provided"); + Preconditions.checkNotNull(passwordConfirm, "PasswordConfirmIsNull"); + + // Verify that we have valid COPPA data before continuing + // Birth month + Preconditions.checkNotNull(birthMonth, "BirthMonthNull"); + LOG.info("Verify that month is provided: {}", birthMonth); + Preconditions.checkState((birthMonth != 0), "BirthMonthNotSet"); + + // Birth year + Preconditions.checkNotNull(birthYear, "BirthYearNull"); + LOG.info("Verify that year is provided: {}", birthYear); + Preconditions.checkState( + (Calendar.getInstance().get(Calendar.YEAR) != birthYear), + "BirthYearNotSet"); + + // Get additional information if the registrant is under 13 years old + if (user.isCoppaEligible(birthMonth, birthYear)) { + LOG.info("User is subject to COPPA regulations"); + + // We must have a sponsor email address for COPPA eligible users + Preconditions.checkNotNull( + parentEmail, + "SponsorEmailNull"); + + // Verify that the sponsor email address is reasonable + if (parentEmail != null && parentEmail.length() > 0) { + LOG.info("Verify that optional user email address is reasonable"); + Preconditions.checkState( + emailValidator.isValid(parentEmail), + "SponsorEmail"); + } + } try { - // Attempt to register the user account data + // Attempt to register the user account data with the cloud session + // service + LOG.info("Registering user account with cloud-service"); Long id = registerService.registerUser( - email, password, passwordConfirm, "en", screenname); - userDao.create(id); + email, password, passwordConfirm, "en", screenname, + birthMonth, birthYear, parentEmail, parentEmailSource); + + if (id > 0) { + userDao.create(id); + } + return id; - } catch (ServerException se) { - log.error("Server error detected"); - return null; + } + catch (ServerException se) { + LOG.error("Server error detected"); + return 0L; + } + catch (NullPointerException npe) { + LOG.error("New user registration failed with: {}", npe.getMessage() ); + return 0L; } } + /** - * Get instance of the authenticated user object + * Get instance of an authenticated user object * * @param email * @param password @@ -174,6 +284,7 @@ public Long register( * @throws InsufficientBucketTokensException * @throws WrongAuthenticationSourceException */ + @Inject public static User authenticateLocalUserStatic( String email, String password) throws @@ -183,7 +294,7 @@ public static User authenticateLocalUserStatic( InsufficientBucketTokensException, WrongAuthenticationSourceException { - log.info("Authenticating user from email address"); + LOG.info("Authenticating user from email address"); return instance.authenticateLocalUser(email, password); } @@ -197,12 +308,13 @@ public static User authenticateLocalUserStatic( * @throws UserBlockedException * @throws EmailNotConfirmedException */ + @Inject public static User authenticateLocalUserStatic(Long idUser) throws UnknownUserIdException, UserBlockedException, EmailNotConfirmedException { - log.info("Authenticating user from userID"); + LOG.info("Authenticating user from userID"); return instance.authenticateLocalUser(idUser); } @@ -226,15 +338,19 @@ public User authenticateLocalUser(String email, String password) throws WrongAuthenticationSourceException { try { + LOG.debug("Attempting to authenticate {}",email); + + // Query Cloud Session interface User user = authenticateService.authenticateLocalUser(email, password); - log.info("User authenticated"); + LOG.info("User authenticated"); return user; } catch (NullPointerException npe) { + LOG.debug("Authetication threw Null Pointer Exception"); throw npe; } catch (ServerException se) { - log.error("Server error encountered: {}", se.getMessage()); + LOG.error("Server error encountered: {}", se.getMessage()); return null; } } @@ -254,14 +370,14 @@ public User authenticateLocalUser(Long idUser) throws try { User user = userService.getUser(idUser); - log.info("User authenticated"); + LOG.info("User authenticated"); return user; } catch (NullPointerException npe) { throw npe; } catch (ServerException se) { - log.error("Server error detected. {}", se.getMessage()); + LOG.error("Server error detected. {}", se.getMessage()); return null; } } @@ -272,7 +388,7 @@ public User authenticateLocalUser(Long idUser) throws * @return SessionData object containing user session details or null */ public static SessionData getSessionData() { - log.debug("Getting user session data"); + LOG.debug("Getting user session data"); SessionData sessionData = instance.sessionData.get(); if (sessionData.getIdUser() == null) { @@ -292,7 +408,7 @@ public static SessionData getSessionData() { user = instance.userService.changeUserLocale( user.getId(), sessionData.getLocale()); } catch (UnknownUserIdException ex) { - log.error("UnknownUserId exception detected. {}", ex.getMessage()); + LOG.error("UnknownUserId exception detected. {}", ex.getMessage()); } } } @@ -306,13 +422,12 @@ public static SessionData getSessionData() { user.getScreenname()); } } catch (UnknownUserException ex) { - log.error("Unknown user ID. {}", ex); + LOG.error("Unknown user ID. {}", ex); } catch (ServerException se) { - log.error("Server error detected. {}", se.getMessage()); + LOG.error("Server error detected. {}", se.getMessage()); } } } return sessionData; } - } diff --git a/src/main/java/com/parallax/server/blocklyprop/services/impl/UserServiceImpl.java b/src/main/java/com/parallax/server/blocklyprop/services/impl/UserServiceImpl.java index f7510a87..ad41ce4e 100644 --- a/src/main/java/com/parallax/server/blocklyprop/services/impl/UserServiceImpl.java +++ b/src/main/java/com/parallax/server/blocklyprop/services/impl/UserServiceImpl.java @@ -18,6 +18,7 @@ import com.parallax.server.blocklyprop.services.UserService; import java.util.List; import org.apache.commons.configuration.Configuration; +import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** @@ -28,7 +29,7 @@ @Transactional public class UserServiceImpl implements UserService { - private static final org.slf4j.Logger LOG = LoggerFactory.getLogger(UserServiceImpl.class); + private static final Logger LOG = LoggerFactory.getLogger(UserServiceImpl.class); private static UserService USER_SERVICE; diff --git a/src/main/java/com/parallax/server/blocklyprop/servlets/AuthenticationServlet.java b/src/main/java/com/parallax/server/blocklyprop/servlets/AuthenticationServlet.java index 605fa93f..ca7dc917 100644 --- a/src/main/java/com/parallax/server/blocklyprop/servlets/AuthenticationServlet.java +++ b/src/main/java/com/parallax/server/blocklyprop/servlets/AuthenticationServlet.java @@ -18,6 +18,9 @@ import org.apache.commons.configuration.Configuration; import org.apache.shiro.web.util.SavedRequest; import org.apache.shiro.web.util.WebUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + /** * @@ -25,27 +28,53 @@ */ @Singleton public class AuthenticationServlet extends HttpServlet { + /** + * Handle for any logging activity + */ + private final Logger LOG = LoggerFactory.getLogger(AuthenticationServlet.class); + /** + * Application configuration settings + */ private Configuration configuration; + + /** + * An instance of this class + */ private AuthenticationService authenticationService; + /** + * Initialize the application configuration + * + * @param configuration + */ @Inject public void setConfiguration(Configuration configuration) { this.configuration = configuration; } + /** + * Initialize an instance of the Authentication service + * + * @param authenticationService + */ @Inject public void setAuthenticationService(AuthenticationService authenticationService) { this.authenticationService = authenticationService; } @Override - protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + protected void doPost(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + resp.setContentType("application/json"); String username = req.getParameter("username"); String password = req.getParameter("password"); + LOG.debug("Reveived user name: {}", username); + + LOG.debug("Authenticating user"); User user = authenticationService.authenticate(username, password); if (user != null) { @@ -60,6 +89,13 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws S userJson.addProperty("id-user", user.getId()); userJson.addProperty("screenname", user.getScreenname()); userJson.addProperty("email", user.getEmail()); + + // COPPA required fields + userJson.addProperty("bdmonth",user.getBirthMonth()); + userJson.addProperty("bdyear", user.getBirthYear()); + userJson.addProperty("parent-email", user.getCoachEmail()); + userJson.addProperty("sponsoremail", user.getCoachEmailSource()); + response.add("user", userJson); resp.getWriter().write(response.toString()); } diff --git a/src/main/java/com/parallax/server/blocklyprop/servlets/ConfirmServlet.java b/src/main/java/com/parallax/server/blocklyprop/servlets/ConfirmServlet.java index 1b68397b..1a73d7cf 100644 --- a/src/main/java/com/parallax/server/blocklyprop/servlets/ConfirmServlet.java +++ b/src/main/java/com/parallax/server/blocklyprop/servlets/ConfirmServlet.java @@ -26,7 +26,8 @@ import org.slf4j.LoggerFactory; /** - * + * Confirm account registration request + * * @author Michel */ @Singleton @@ -49,31 +50,43 @@ public void setUserDao(UserDao userDao) { @Inject public void setConfiguration(Configuration configuration) { this.configuration = configuration; - cloudSessionLocalUserService = new CloudSessionLocalUserService(configuration.getString("cloudsession.server"), configuration.getString("cloudsession.baseurl")); + cloudSessionLocalUserService = new CloudSessionLocalUserService( + configuration.getString("cloudsession.server"), + configuration.getString("cloudsession.baseurl")); } @Override - protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { confirmToken(req, resp); } @Override - protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + protected void doPost(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { confirmToken(req, resp); } - public void confirmToken(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + public void confirmToken(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + + // Retreive the registration token String token = req.getParameter("token"); - String email = req.getParameter("email"); req.setAttribute("token", token == null ? "" : token); + + // Retreive the requestor's email address + String email = req.getParameter("email"); req.setAttribute("email", email == null ? "" : email); + + // Return to the confirmation web page is we're missing data if (Strings.isNullOrEmpty(token) || Strings.isNullOrEmpty(email)) { - req.getRequestDispatcher("WEB-INF/servlet/confirm/confirm.jsp").forward(req, resp); + req.getRequestDispatcher("WEB-INF/servlet/confirm/confirm.jsp") + .forward(req, resp); } else { try { + // Validate the email and token with the Cloud Session server if (cloudSessionLocalUserService.doConfirm(email, token)) { // req.getRequestDispatcher("WEB-INF/servlet/confirm/confirmed.jsp").forward(req, resp); - showTextilePage(req, resp, ConfirmPage.CONFIRMED); } else { req.setAttribute("invalidToken", "Invalid token"); @@ -93,8 +106,16 @@ public void confirmToken(HttpServletRequest req, HttpServletResponse resp) throw } } - public void showTextilePage(HttpServletRequest req, HttpServletResponse resp, ConfirmPage confirmPage) throws ServletException, IOException { - String html = textileFileReader.readFile("confirm/" + confirmPage.getPage(), ServletUtils.getLocale(req), req.isSecure()); + public void showTextilePage( + HttpServletRequest req, + HttpServletResponse resp, + ConfirmPage confirmPage) throws ServletException, IOException { + + String html = textileFileReader.readFile( + "confirm/" + confirmPage.getPage(), + ServletUtils.getLocale(req), + req.isSecure()); + req.setAttribute("html", html); req.getRequestDispatcher("/WEB-INF/servlet/html.jsp").forward(req, resp); } diff --git a/src/main/java/com/parallax/server/blocklyprop/servlets/PrivacyPolicyServlet.java b/src/main/java/com/parallax/server/blocklyprop/servlets/PrivacyPolicyServlet.java new file mode 100644 index 00000000..7ca31a87 --- /dev/null +++ b/src/main/java/com/parallax/server/blocklyprop/servlets/PrivacyPolicyServlet.java @@ -0,0 +1,62 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.parallax.server.blocklyprop.servlets; + +import com.google.inject.Singleton; +import java.io.IOException; +import java.io.PrintWriter; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * + * @author developer + */ +@Singleton +public class PrivacyPolicyServlet extends HttpServlet { + + /** + * Application logging system access + */ + private static Logger LOG = LoggerFactory.getLogger(PrivacyPolicyServlet.class); + + + // + /** + * Handles the HTTP GET method. + * + * @param request servlet request + * @param response servlet response + * + * @throws ServletException if a servlet-specific error occurs + * @throws IOException if an I/O error occurs + */ + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + + LOG.debug("Requesting child privacy page"); + + request.getRequestDispatcher( + "WEB-INF/servlet/coppa/privacy-policy.jsp") + .forward(request, response); + } + + /** + * Returns a short description of the servlet. + * + * @return a String containing servlet description + */ + @Override + public String getServletInfo() { + return "Parallax COPPA policy page"; + }// + +} diff --git a/src/main/java/com/parallax/server/blocklyprop/servlets/ProjectServlet.java b/src/main/java/com/parallax/server/blocklyprop/servlets/ProjectServlet.java index d0c1d25a..d91493ac 100644 --- a/src/main/java/com/parallax/server/blocklyprop/servlets/ProjectServlet.java +++ b/src/main/java/com/parallax/server/blocklyprop/servlets/ProjectServlet.java @@ -18,7 +18,8 @@ import org.apache.shiro.authz.UnauthorizedException; /** - * + * Handler for the /project REST endpoint + * * @author Michel */ @Singleton @@ -32,7 +33,9 @@ public void setProjectService(ProjectService projectService) { } @Override - protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + String clone = req.getParameter("clone"); if (!Strings.isNullOrEmpty(clone)) { clone(Long.parseLong(clone), req, resp); diff --git a/src/main/java/com/parallax/server/blocklyprop/servlets/RegisterServlet.java b/src/main/java/com/parallax/server/blocklyprop/servlets/RegisterServlet.java index 636e2f68..0e0a8564 100644 --- a/src/main/java/com/parallax/server/blocklyprop/servlets/RegisterServlet.java +++ b/src/main/java/com/parallax/server/blocklyprop/servlets/RegisterServlet.java @@ -5,6 +5,7 @@ */ package com.parallax.server.blocklyprop.servlets; +import org.apache.commons.validator.routines.EmailValidator; import com.google.common.base.Strings; import com.google.inject.Inject; import com.google.inject.Singleton; @@ -18,6 +19,8 @@ import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * @@ -26,60 +29,253 @@ @Singleton public class RegisterServlet extends HttpServlet { - private SecurityService securityService; + /** + * + */ + private static final Logger LOG = LoggerFactory.getLogger(RegisterServlet.class); + + private EmailValidator emailValid = EmailValidator.getInstance(); + /** + * + */ + private SecurityService securityService; + + /** + * + * @param securityService + */ @Inject public void setSecurityService(SecurityService securityService) { this.securityService = securityService; } + /** + * Clear the user registration form data and present the form + * + * @param req + * @param resp + * @throws ServletException + * @throws IOException + */ @Override - protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + protected void doGet( + HttpServletRequest req, + HttpServletResponse resp) throws ServletException, IOException { + + // Initialize the form fields to reasonable defaults req.setAttribute("screenname", ""); req.setAttribute("email", ""); + req.setAttribute("sponsoremail", ""); + + // Send the new form to the client req.getRequestDispatcher("WEB-INF/servlet/register/register.jsp").forward(req, resp); } - /* - * Register + /** + * Process the user registration page + *

+ * This retrieves the form values and pass them on to another method + * that validates the information and creates the user account. + *

+ * The register() method will throw an exception if the data collected from + * the form is considered invalid. The exact exception depends on the nature + * of the error. For example, null values in required fields will throw a + * NullPointerException or a poorly formatted email address will cause a + * IllegalStateException to be thrown. + *

+ * This method traps those exceptions and resubmits the form with a helpful + * error message to guide the user to a successful registration experience. + * Each exception contains a mnemonic that is used here to identify the + * exact error. The exception handler in this method then sets an attribute + * on the form and resubmits it. The form is then presented to the user to + * provide him or her with an opportunity to correct the data and resubmit + * the registration. + *

+ * + * @param req + * @param resp + * @throws ServletException + * @throws IOException */ @Override - protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + protected void doPost( + HttpServletRequest req, + HttpServletResponse resp) throws ServletException, IOException { + resp.setContentType("application/json"); String screenname = Strings.emptyToNull(req.getParameter("screenname")); String email = Strings.emptyToNull(req.getParameter("email")); String password = Strings.emptyToNull(req.getParameter("password")); String confirmPassword = Strings.emptyToNull(req.getParameter("confirmpassword")); + String birthMonth = Strings.emptyToNull(req.getParameter("bdmonth")); + String birthYear = Strings.emptyToNull(req.getParameter("bdyear")); + String sponsorEmail = Strings.emptyToNull(req.getParameter("sponsoremail")); + String sponsorEmailType = Strings.emptyToNull(req.getParameter("sponsoremailtype")); + + LOG.info("Raw screen name: {}", screenname); + LOG.info("Raw sponsor email address is: {}", sponsorEmail); + // Clean up any possible null fields req.setAttribute("screenname", screenname == null ? "" : screenname); req.setAttribute("email", email == null ? "" : email); + req.setAttribute("bdmonth", birthMonth == null ? "" : birthMonth ); + req.setAttribute("bdyear", birthYear == null ? "" : birthYear ); + req.setAttribute("sponsoremail", sponsorEmail == null ? "" : sponsorEmail); + req.setAttribute("sponsoremailtype", sponsorEmailType == null ? "0" : sponsorEmailType); + + // Log a few things + LOG.info("Registering screen name: {}", screenname); + LOG.info("Registering email: {}", email); + LOG.info("Registering month: {}", birthMonth); + LOG.info("Registering year: {}", birthYear); + LOG.info("Registering sponsor email: {}", sponsorEmail); + LOG.info("Registering sponsor type selection: {}", sponsorEmailType); + LOG.info("Checking REQ Year setting: {}",req.getAttribute("bdmonth")); + Long idUser; + try { - idUser = securityService.register(screenname, email, password, confirmPassword); + LOG.info("Calling securityService.register()"); + // Validate the collected information + idUser = securityService.register( + screenname, email, password, confirmPassword, + Integer.parseInt(birthMonth), + Integer.parseInt(birthYear), + sponsorEmail, + Integer.parseInt(sponsorEmailType)); + + LOG.info("Returning from register(). ID assigned is: {}", idUser); + if (idUser != null && idUser > 0) { + LOG.info("Transfering to registered.jsp"); req.getRequestDispatcher("WEB-INF/servlet/register/registered.jsp").forward(req, resp); } else { + LOG.info("Returning to the user registration page"); req.setAttribute("error", true); req.getRequestDispatcher("WEB-INF/servlet/register/register.jsp").forward(req, resp); } } catch (NonUniqueEmailException ex) { + LOG.warn("Attempt to register already assigned email: {}", email); req.setAttribute("emailAlreadyUsed", true); req.getRequestDispatcher("WEB-INF/servlet/register/register.jsp").forward(req, resp); } catch (PasswordVerifyException ex) { + LOG.info("Paswword mismatch"); req.setAttribute("passwordsDontMatch", true); req.getRequestDispatcher("WEB-INF/servlet/register/register.jsp").forward(req, resp); } catch (NullPointerException npe) { - req.setAttribute("missingFields", true); + LOG.warn("Null Pointer Exception. Data is: {}", npe.getMessage()); + + // Figure out which data field triggered the exception + switch(npe.getMessage()) { + case "ScreenNameNull": + LOG.warn("Null screen name trigger exception"); + req.setAttribute("ScreenNameNull", true); + break; + + case "PasswordIsNull": + LOG.warn("Null password trigger exception"); + req.setAttribute("PasswordIsNull", true); + break; + + case "PasswordConfirmIsNull": + LOG.warn("Null confirmation password trigger exception"); + req.setAttribute("PasswordConfirmIsNull", true); + break; + + case "BirthMonthNull": + LOG.warn("Null birth month trigger exception"); + req.setAttribute("BirthMonthNull", true); + break; + + case "BirthYearNull": + LOG.warn("Null birth yeartrigger exception"); + req.setAttribute("BirthYearNull", true); + break; + + case "UserEmailNull": + LOG.warn("Null user email"); + req.setAttribute("UserEmailNull", true); + break; + + case "SponsorEmailNull": + LOG.warn("Null sponsor email"); + req.setAttribute("SponsorEmailNull", true); + break; + + default: + LOG.warn("Unknown exception trigger"); + req.setAttribute("missingFields", true); + } req.getRequestDispatcher("WEB-INF/servlet/register/register.jsp").forward(req, resp); + } catch (PasswordComplexityException pce) { + LOG.info("Paswword is not complex enough"); req.setAttribute("passwordComplexity", true); req.getRequestDispatcher("WEB-INF/servlet/register/register.jsp").forward(req, resp); } catch (ScreennameUsedException sue) { + LOG.info("Attempt to use already assigned screen name: {}", screenname); req.setAttribute("screennameUsed", true); req.getRequestDispatcher("WEB-INF/servlet/register/register.jsp").forward(req, resp); + } catch (IllegalStateException ex) { + LOG.warn("Exception message is: {}", ex.getMessage()); + + if ("BirthMonthNotSet".equals(ex.getMessage())) { + req.setAttribute("BirthMonthNotSet", true); + } + + // Birth year is still at the default. Infants probably are not users + else if ("BirthYearNotSet".equals(ex.getMessage())) { + req.setAttribute("BirthYearNotSet", true); + } + + // Set the on-screen error message numonic. Code in the + // register.jsp page will map the numonic to a language sting and + // display that to the user + else if ("SponsorEmail".equals(ex.getMessage())) { + req.setAttribute("sponsorEmailMalformed", true); + } + + else // User email issue + { + req.setAttribute("emailMalformed", true); + } + req.getRequestDispatcher("WEB-INF/servlet/register/register.jsp").forward(req, resp); + } + } + + private String getMonthFromDate(String date) { + String[] dates = date.split("/"); + int month = 0; + + try { + if (dates.length == 2) { + month = Integer.parseInt(dates[0]); + } + } + catch (NumberFormatException ex) { + month = 0; + } + + return Integer.toString(month); + } + + private String getYearFromDate(String date) { + String[] dates = date.split("/"); + int year = 0; + + try { + if (dates.length == 2) { + year = Integer.parseInt(dates[1]); + } + } + catch (NumberFormatException ex) { + year = 0; } + + return Integer.toString(year); } } diff --git a/src/main/resources/com/parallax/server/blocklyprop/internationalization/translations.properties b/src/main/resources/com/parallax/server/blocklyprop/internationalization/translations.properties index e371b14c..fc0adec9 100644 --- a/src/main/resources/com/parallax/server/blocklyprop/internationalization/translations.properties +++ b/src/main/resources/com/parallax/server/blocklyprop/internationalization/translations.properties @@ -6,9 +6,6 @@ logout = Logout cancel = Cancel back = Back -password.complexity = The password should be at least 8 characters long -password.complexity.error = Password is not complex enough - error.generic = A problem occurred error.unknownemail = Unknown email @@ -21,6 +18,7 @@ menu.help = Help menu.newproject.title = New project menu.newproject.spin = Scribbler Robot menu.newproject.c = Propeller C +menu.privacy = Privacy Policy footer.licenselink = License footer.changelog = Change log @@ -29,8 +27,8 @@ footer.clientdownloadlink = BlocklyProp-client # Application version numbers. application.major = 0 -application.minor = 96 -application.build = 410 +application.minor = 97 +application.build = 419 html.content_missing = Content missing @@ -39,6 +37,8 @@ clientdownload.showall = Show clients for all operating systems clientdownload.client.macos.installer = MacOS client installer clientdownload.client.windows32.installer = Windows 7/8/8.1/10 (32-bit) client installer clientdownload.client.windows64.installer = Windows 7/8/8.1/10 (64-bit) client installer +clientdownload.client.chromeos.installer = Add to Chrome +clientdownload.client.chromeos.alreadyinstalled = BlocklyProp Launcher is already installed. Make sure it is open and running. help.title = Help help.not-found = Help file not found @@ -58,9 +58,9 @@ home.spin_project.title = S3 Robot Project home.spin_project.newlink = New oauth.new-user = New user -oauth.new-user.screenname = Screenname +oauth.new-user.screenname = Screen Name oauth.new-user.do.submit = Save -oauth.new-user.error.screenname = Screenname already in use +oauth.new-user.error.screenname = Screen Name already in use oauth.success = User logged in not_loggedin.title = You are not logged in @@ -70,16 +70,16 @@ not_loggedin.try.trylink = Try editor not_loggedin.try.viewprojectlink = View project not_loggedin.login.title = Log in not_loggedin.login.loginlink = Login -not_loggedin.login.registerlink = Register +not_loggedin.login.registerlink = Register a new account my_project.list.title = My projects project.list.title = Community projects project.title = Project project.details_title = Project details -project.name = Name -project.user = User -project.board = Board +project.name = Project Name +project.user = User Screen Name +project.board = Board Type project.created = Created On project.modified = Last Modified project.description = Description @@ -87,12 +87,13 @@ project.share-link = Share project using link project.sharing = Sharing project.sharing.private = Private project.sharing.shared = Shared -# TODO change translation to "Friends" once implemented -project.sharing.friends = Default +project.sharing.tooltip.shared = Make project visible to other users +project.sharing.tooltip.private = Hide project from other users #project.openlink = Open project project.viewcode = View project code project.clonelink = Clone project.savelink = Save +project.saveaslink = Save As project.deletelink = Delete project.delete.confirm.title = Confirm delete project.delete.confirm = Are you sure you want to delete this project? @@ -105,7 +106,7 @@ project.changed = Project changes have been saved project.created = Created project.modified = Modified -project.create.title = Your project +project.create.title = New project project.create.basic = Basic info project.create.basic.title = Basic project info project.create.project_name = Project name @@ -128,14 +129,19 @@ confirm.request.title = Email confirm request confirm.request.email = Email: confirm.request.submit = Request confirm.requested = Please check your email + confirm.do.title = Please confirm confirm.do.error.invalid_combination = Invalid token/email combination confirm.do.email = Email: confirm.do.token = Token: confirm.do.submit = Confirm + confirm.error.requested_too_often = Confirm requested too often confirm.error.wrong_authentication_source = Email confirm is not for users using third party authentication sources. +password.complexity = The password should be at least 8 characters long +password.complexity.error = Password is not complex enough + password_reset.request.title = Request password reset password_reset.request.email = Email: password_reset.request.submit = Confirm @@ -151,17 +157,35 @@ password_reset.error.requested_too_often = Reset requested too often password_reset.error.wrong_authentication_source = Password reset is not for users using third party authentication sources. register.do.title = Register -register.do.screenname = Screenname: +register.do.screenname = Screen name: register.do.email = Email: register.do.password = Password: register.do.confirm_password = Confirm password: +register.do.birth.month = Birth Month: +register.do.birth.year = Birth Year: +register.do.sponsor.email = Parent (or Teacher) email: +register.do.sponsor.emailtype = Select one: +register.do.coppa.msg0 = Why are we asking? +register.do.coppa.msg1 = To protect your privacy, especially if you are one of our younger users, US COPPA regulations require that we determine your age. More information is available +register.do.coppa.msg2 = here register.do.submit = Register register.done.title = Registration successful register.done.text = Please check your email to confirm your email account. + +register.error.generic = Something has gone wrong while registering your account. Support has been notified. +register.error.birth_month_not_selected = Please select the month of your birthday +register.error.birth_year_not_selected = Please select the year of your birthday +register.error.user_email_empty = Please enter your email address register.error.email_already_used = Email already used +register.error.email_format_error = The email address is not formatted correctly +register.error.sponsor_email_empty = Please enter a sponsor email address. Ask a parent or teacher if you can use their email address +register.error.sponsor_email_format_error = The sponsor email address is not formatted correctly +register.error.password_empty = Please provide a password to secure your account +register.error.password_confirm_empty = Please enter your password twice to ensure it matches register.error.passwords_dont_match = Passwords don't match register.error.missing_fields = All fields must be completed -register.error.screenname_used = Screenname already in use +register.error.screenname_empty = Please enter a screen name +register.error.screenname_used = Screen name already in use profile.title = Profile profile.unlock.error = Could not unlock your profile @@ -188,7 +212,7 @@ public-profile.nav.projects = Projects public-profile.friends = Friends public-profile.projects = Projects -login.registerlink = Register now! +login.registerlink = Register a new account login.title = Please Log in login.email = Email: login.password = Password: @@ -217,11 +241,13 @@ editor.project = Project editor.edit-details = Edit Project details editor.save = Save editor.save-as = Save project as +editor.save-check = Save project reminder +editor.save-check.warning = It has been 20 minutes since you last saved your project. Save now? editor.client.title = BlocklyPropClient -editor.client.checking = Looking for BlocklyPropClient -editor.client.available = Select the correct port, then click or . -editor.client.available.short = Select the correct port, then click . -editor.client.not-available = BlocklyPropClient is not running +editor.client.checking = \u2139 Looking for BlocklyPropClient +editor.client.available = Select the correct port, then click or . +editor.client.available.short = Select the correct port, then click . +editor.client.not-available = \u26a0 BlocklyPropClient is not running editor.download = Download blocks file editor.upload = Upload blocks file editor.upload.selectfile = Select File diff --git a/src/main/resources/documents/client-instructions.textile b/src/main/resources/documents/client-instructions.textile index fe152933..77b0c047 100644 --- a/src/main/resources/documents/client-instructions.textile +++ b/src/main/resources/documents/client-instructions.textile @@ -1,4 +1,4 @@ To download code to your Propeller, BlocklyProp requires that the BlocklyPropClient software is installed and connected on your computer. * If BlocklyPropClient is already installed, run it and click the "Connect" button, then return to this editor -* If BlocklyPropClient is not installed, download and install it using the link for your computer type below \ No newline at end of file +* If BlocklyPropClient is not installed, download and install it using the link for your computer below \ No newline at end of file diff --git a/src/main/resources/documents/confirm/already-confirmed.textile b/src/main/resources/documents/confirm/already-confirmed.textile index 47d74be8..393a1930 100644 --- a/src/main/resources/documents/confirm/already-confirmed.textile +++ b/src/main/resources/documents/confirm/already-confirmed.textile @@ -1,3 +1,4 @@ -h2. Email already confirmed +h3. Email already confirmed +This account has already been activated. "Go to home":/blockly/index diff --git a/src/main/resources/documents/confirm/confirm-requested.textile b/src/main/resources/documents/confirm/confirm-requested.textile index c256160e..6ae60e6e 100644 --- a/src/main/resources/documents/confirm/confirm-requested.textile +++ b/src/main/resources/documents/confirm/confirm-requested.textile @@ -1,3 +1,3 @@ -h2. Email confirm requested +h3. Email confirm requested "Go to home":/blockly/index diff --git a/src/main/resources/documents/confirm/confirmed.textile b/src/main/resources/documents/confirm/confirmed.textile index f67f82b7..4a66b225 100644 --- a/src/main/resources/documents/confirm/confirmed.textile +++ b/src/main/resources/documents/confirm/confirmed.textile @@ -1,3 +1,5 @@ -h2. Email confirmed +h3. Registration Complete +Congratulations! your account has been activated. You are ready to build +your own projects. "Go to home":/blockly/index diff --git a/src/main/resources/shiro.ini b/src/main/resources/shiro.ini index 7cf8c1b8..0ffd72b6 100644 --- a/src/main/resources/shiro.ini +++ b/src/main/resources/shiro.ini @@ -29,6 +29,9 @@ shiro.loginUrl = /login.jsp [urls] +# +# A list of accessable URLs +# # CDN (data, local during development) (maybe add a hotlink protection?) /cdn/** = anon @@ -55,6 +58,7 @@ shiro.loginUrl = /login.jsp /demo/** = anon, ssl /frame/** = anon, ssl /projectlink = anon, ssl +/privacy-policy = anon, ssl # REST api and api documentation /apidoc = anon diff --git a/src/main/webapp/WEB-INF/includes/pageparts/clientdownload.jsp b/src/main/webapp/WEB-INF/includes/pageparts/clientdownload.jsp index 81c0a9a5..81f5728f 100644 --- a/src/main/webapp/WEB-INF/includes/pageparts/clientdownload.jsp +++ b/src/main/webapp/WEB-INF/includes/pageparts/clientdownload.jsp @@ -34,6 +34,13 @@ + +

+ "/> + + +
+ <%--
Windows service diff --git a/src/main/webapp/WEB-INF/includes/pageparts/editor-menu.jsp b/src/main/webapp/WEB-INF/includes/pageparts/editor-menu.jsp index 5bb28dbb..a5733796 100644 --- a/src/main/webapp/WEB-INF/includes/pageparts/editor-menu.jsp +++ b/src/main/webapp/WEB-INF/includes/pageparts/editor-menu.jsp @@ -43,11 +43,11 @@
- + - + - + @@ -62,18 +62,17 @@ - Code + Code - Code + Code - + <% if ("1".equals(request.getParameter("debug"))) {%> <% }%> - +