This is an automated email from the git hooks/post-receive script. New commit to branch develop in repository pollen. See https://gitlab.nuiton.org/chorem/pollen.git commit 91bfe7286ca9a7d63be3865748b5fbe3bfcfb566 Author: Sylvain Bavencoff <bavencoff@codelutin.com> Date: Thu Oct 19 14:51:38 2017 +0200 Sppression de token de session par un batch et non a la vérification du token + correction lié a l'expiration d'un token de séssion --- .../rest/api/PollenRestApiApplicationListener.java | 15 +++++++ pollen-services/src/main/config/PollenServices.ini | 8 +++- .../pollen/services/job/AbstractPollenJob.java | 27 ++++++++++- .../job/DeleteObsoleteSessionTokensJob.java | 28 ++++++++++++ .../pollen/services/job/SendEmailInErrorsJob.java | 20 +++------ .../services/job/SendPollEndReminderJob.java | 52 +++++++++------------- .../services/service/NotificationService.java | 5 ++- .../pollen/services/service/mail/EmailService.java | 6 ++- .../services/service/security/SecurityService.java | 20 +++++---- .../i18n/pollen-services_en_GB.properties | 1 + .../i18n/pollen-services_fr_FR.properties | 1 + pollen-ui-riot-js/src/main/web/js/Session.js | 8 ++-- pollen-ui-riot-js/src/main/web/tag/Pollen.tag.html | 7 ++- .../{SigninAction.tag.js => SignInAction.tag.js} | 0 .../src/main/web/tag/admin/UserEditModal.tag.html | 4 +- .../src/main/web/tag/admin/Users.tag.html | 6 +-- .../src/main/web/tag/components/LazyLoad.tag.html | 6 +-- 17 files changed, 141 insertions(+), 73 deletions(-) diff --git a/pollen-rest-api/src/main/java/org/chorem/pollen/rest/api/PollenRestApiApplicationListener.java b/pollen-rest-api/src/main/java/org/chorem/pollen/rest/api/PollenRestApiApplicationListener.java index d4e34508..5bd9595e 100644 --- a/pollen-rest-api/src/main/java/org/chorem/pollen/rest/api/PollenRestApiApplicationListener.java +++ b/pollen-rest-api/src/main/java/org/chorem/pollen/rest/api/PollenRestApiApplicationListener.java @@ -41,6 +41,7 @@ import org.chorem.pollen.services.bean.VoterListBean; import org.chorem.pollen.services.bean.VoterListMemberBean; import org.chorem.pollen.services.config.PollenServicesConfig; import org.chorem.pollen.services.job.AbstractPollenJob; +import org.chorem.pollen.services.job.DeleteObsoleteSessionTokensJob; import org.chorem.pollen.services.job.SendEmailInErrorsJob; import org.chorem.pollen.services.job.SendPollEndReminderJob; import org.quartz.CronScheduleBuilder; @@ -115,6 +116,11 @@ public class PollenRestApiApplicationListener implements ServletContextListener .withIdentity("resendEmailsJob", "pollenJobs") .build(); + JobDetail deleteObsoleteSessionTokensJob = JobBuilder.newJob(DeleteObsoleteSessionTokensJob.class) + .usingJobData(data) + .withIdentity("deleteObsoleteSessionTokensJob", "pollenJobs") + .build(); + try { scheduler = new StdSchedulerFactory().getScheduler(); @@ -139,6 +145,15 @@ public class PollenRestApiApplicationListener implements ServletContextListener scheduler.scheduleJob(resendEmailsJob, resendEmailsTrigger); + // schedule send poll end reminders + Trigger deleteObsoleteSessionTokensTrigger = TriggerBuilder + .newTrigger() + .withIdentity("deleteObsoleteSessionTokensTrigger", "pollenTriggers") + .withSchedule(CronScheduleBuilder.cronSchedule(applicationConfig.getDeleteObsoleteSessionTokensCronSchedule())) + .build(); + + scheduler.scheduleJob(deleteObsoleteSessionTokensJob, deleteObsoleteSessionTokensTrigger); + scheduler.start(); if (log.isDebugEnabled()) { diff --git a/pollen-services/src/main/config/PollenServices.ini b/pollen-services/src/main/config/PollenServices.ini index 7f9aa169..465b7a15 100644 --- a/pollen-services/src/main/config/PollenServices.ini +++ b/pollen-services/src/main/config/PollenServices.ini @@ -221,4 +221,10 @@ defaultValue = false description = pollen.configuration.usersCanCreatePoll key = pollen.default.usersCanCreatePoll type = org.chorem.pollen.services.bean.UsersRight -defaultValue = ALL_USERS \ No newline at end of file +defaultValue = ALL_USERS + +[option deleteObsoleteSessionTokensCronSchedule] +description = pollen.configuration.deleteObsoleteSessionTokensCronSchedule +key = pollen.deleteObsoleteSessionTokensCronSchedule +type = string +defaultValue = "0 0/5 * * * ?" \ No newline at end of file diff --git a/pollen-services/src/main/java/org/chorem/pollen/services/job/AbstractPollenJob.java b/pollen-services/src/main/java/org/chorem/pollen/services/job/AbstractPollenJob.java index b0fb3885..a2ba060b 100644 --- a/pollen-services/src/main/java/org/chorem/pollen/services/job/AbstractPollenJob.java +++ b/pollen-services/src/main/java/org/chorem/pollen/services/job/AbstractPollenJob.java @@ -21,9 +21,15 @@ package org.chorem.pollen.services.job; * #L% */ +import org.chorem.pollen.persistence.PollenTopiaPersistenceContext; import org.chorem.pollen.services.PollenApplicationContext; +import org.chorem.pollen.services.PollenService; +import org.chorem.pollen.services.PollenServiceContext; import org.quartz.Job; import org.quartz.JobExecutionContext; +import org.quartz.JobExecutionException; + +import java.util.Locale; /** * @author Kevin Morin (Code Lutin) @@ -33,7 +39,24 @@ public abstract class AbstractPollenJob implements Job { public static final String APPLICATION_CONTEXT = "applicationContext"; - protected PollenApplicationContext getApplicationContext(JobExecutionContext jobExecutionContext) { - return (PollenApplicationContext) jobExecutionContext.getMergedJobDataMap().get(APPLICATION_CONTEXT); + protected PollenApplicationContext applicationContext; + + protected PollenServiceContext serviceContext; + + public <E extends PollenService> E newService(Class<E> serviceClass) { + return serviceContext.newService(serviceClass); + } + + @Override + public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException { + applicationContext = (PollenApplicationContext) jobExecutionContext.getMergedJobDataMap().get(APPLICATION_CONTEXT); + try (PollenTopiaPersistenceContext persistenceContext = applicationContext.newPersistenceContext()) { + serviceContext = applicationContext.newServiceContext(persistenceContext, Locale.getDefault()); + execute(); + + } } + + protected abstract void execute() throws JobExecutionException; + } diff --git a/pollen-services/src/main/java/org/chorem/pollen/services/job/DeleteObsoleteSessionTokensJob.java b/pollen-services/src/main/java/org/chorem/pollen/services/job/DeleteObsoleteSessionTokensJob.java new file mode 100644 index 00000000..e1fe108f --- /dev/null +++ b/pollen-services/src/main/java/org/chorem/pollen/services/job/DeleteObsoleteSessionTokensJob.java @@ -0,0 +1,28 @@ +package org.chorem.pollen.services.job; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.chorem.pollen.services.service.security.SecurityService; +import org.quartz.DisallowConcurrentExecution; +import org.quartz.JobExecutionException; + +/** + * @author Sylvain Bavencoff - bavencoff@codelutin.com + */ +@DisallowConcurrentExecution +public class DeleteObsoleteSessionTokensJob extends AbstractPollenJob { + + private static final Log log = LogFactory.getLog(DeleteObsoleteSessionTokensJob.class); + + @Override + public void execute() throws JobExecutionException { + + if (log.isDebugEnabled()) { + log.debug("Start job to delete obsolete Session Tokens"); + } + + SecurityService securityService = newService(SecurityService.class); + securityService.deleteObsoleteSessionTokens(); + + } +} diff --git a/pollen-services/src/main/java/org/chorem/pollen/services/job/SendEmailInErrorsJob.java b/pollen-services/src/main/java/org/chorem/pollen/services/job/SendEmailInErrorsJob.java index ce587b23..a423b659 100644 --- a/pollen-services/src/main/java/org/chorem/pollen/services/job/SendEmailInErrorsJob.java +++ b/pollen-services/src/main/java/org/chorem/pollen/services/job/SendEmailInErrorsJob.java @@ -23,16 +23,10 @@ package org.chorem.pollen.services.job; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; -import org.chorem.pollen.persistence.PollenTopiaPersistenceContext; -import org.chorem.pollen.services.PollenApplicationContext; -import org.chorem.pollen.services.PollenServiceContext; import org.chorem.pollen.services.service.mail.EmailService; import org.quartz.DisallowConcurrentExecution; -import org.quartz.JobExecutionContext; import org.quartz.JobExecutionException; -import java.util.Locale; - /** * @author Kevin Morin (Code Lutin) */ @@ -42,19 +36,15 @@ public class SendEmailInErrorsJob extends AbstractPollenJob { /** Logger. */ private static final Log log = LogFactory.getLog(SendEmailInErrorsJob.class); - protected PollenApplicationContext applicationContext; - @Override - public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException { + protected void execute() throws JobExecutionException { + if (log.isDebugEnabled()) { log.debug("Start job to resend emails in errors"); } - applicationContext = getApplicationContext(jobExecutionContext); - try (PollenTopiaPersistenceContext persistenceContext = applicationContext.newPersistenceContext()) { - PollenServiceContext serviceContext = applicationContext.newServiceContext(persistenceContext, Locale.getDefault()); - EmailService emailService = serviceContext.newService(EmailService.class); - emailService.resendEmails(); - } + EmailService emailService = newService(EmailService.class); + emailService.resendEmails(); + } } diff --git a/pollen-services/src/main/java/org/chorem/pollen/services/job/SendPollEndReminderJob.java b/pollen-services/src/main/java/org/chorem/pollen/services/job/SendPollEndReminderJob.java index d109bbc9..9eeebc59 100644 --- a/pollen-services/src/main/java/org/chorem/pollen/services/job/SendPollEndReminderJob.java +++ b/pollen-services/src/main/java/org/chorem/pollen/services/job/SendPollEndReminderJob.java @@ -23,14 +23,10 @@ package org.chorem.pollen.services.job; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; -import org.chorem.pollen.persistence.PollenTopiaPersistenceContext; import org.chorem.pollen.persistence.entity.Poll; -import org.chorem.pollen.services.PollenApplicationContext; -import org.chorem.pollen.services.PollenServiceContext; import org.chorem.pollen.services.service.NotificationService; import org.chorem.pollen.services.service.PollService; import org.quartz.DisallowConcurrentExecution; -import org.quartz.JobExecutionContext; import org.quartz.JobExecutionException; import java.util.Collection; @@ -45,42 +41,36 @@ public class SendPollEndReminderJob extends AbstractPollenJob { /** Logger. */ private static final Log log = LogFactory.getLog(SendPollEndReminderJob.class); - protected PollenApplicationContext applicationContext; - @Override - public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException { + protected void execute() throws JobExecutionException { + if (log.isDebugEnabled()) { log.debug("Start job to send reminder emails to the poll owners"); } - applicationContext = getApplicationContext(jobExecutionContext); - try (PollenTopiaPersistenceContext persistenceContext = applicationContext.newPersistenceContext()) { - PollenServiceContext serviceContext = applicationContext.newServiceContext(persistenceContext, Locale.getDefault()); + PollService pollService = newService(PollService.class); + Collection<Poll> pollsWithVoteReminderNeeded = pollService.getPollsWithReminderNeeded(); - PollService pollService = serviceContext.newService(PollService.class); - Collection<Poll> pollsWithVoteReminderNeeded = pollService.getPollsWithReminderNeeded(); - - for (Poll poll : pollsWithVoteReminderNeeded) { - if (log.isDebugEnabled()) { - log.debug("Send reminder for poll " + poll.getTitle()); + for (Poll poll : pollsWithVoteReminderNeeded) { + if (log.isDebugEnabled()) { + log.debug("Send reminder for poll " + poll.getTitle()); + } + try { + Locale locale; + if (poll.getNotificationLocale() != null) { + locale = Locale.forLanguageTag(poll.getNotificationLocale()); + } else { + locale = Locale.getDefault(); } - try { - Locale locale; - if (poll.getNotificationLocale() != null) { - locale = Locale.forLanguageTag(poll.getNotificationLocale()); - } else { - locale = Locale.getDefault(); - } - PollenServiceContext localizedServiceContext = applicationContext.newServiceContext(persistenceContext, locale); - localizedServiceContext.newService(NotificationService.class).sendPollEndReminder(poll); + NotificationService notificationService = newService(NotificationService.class); + notificationService.sendPollEndReminder(poll, locale); - poll.setPollEndReminderSent(true); - pollService.commit(); + poll.setPollEndReminderSent(true); + pollService.commit(); - } catch (Exception e) { - if (log.isErrorEnabled()) { - log.error("Error while sending end reminder for poll " + poll.getTopiaId(), e); - } + } catch (Exception e) { + if (log.isErrorEnabled()) { + log.error("Error while sending end reminder for poll " + poll.getTopiaId(), e); } } } diff --git a/pollen-services/src/main/java/org/chorem/pollen/services/service/NotificationService.java b/pollen-services/src/main/java/org/chorem/pollen/services/service/NotificationService.java index 0b26b609..938cc677 100644 --- a/pollen-services/src/main/java/org/chorem/pollen/services/service/NotificationService.java +++ b/pollen-services/src/main/java/org/chorem/pollen/services/service/NotificationService.java @@ -67,6 +67,7 @@ import org.nuiton.topia.persistence.TopiaEntity; import java.util.Collections; import java.util.List; +import java.util.Locale; import java.util.Set; /** @@ -328,9 +329,9 @@ public class NotificationService extends PollenServiceSupport { } - public void sendPollEndReminder(Poll poll) { + public void sendPollEndReminder(Poll poll, Locale locale) { EmailService emailService = getEmailService(); - PollEndReminderEmail pollEndReminderEmail = emailService.newPollEndReminderEmail(poll); + PollEndReminderEmail pollEndReminderEmail = emailService.newPollEndReminderEmail(poll, locale); pollEndReminderEmail.addTo(poll.getCreator().getEmail()); emailService.send(pollEndReminderEmail); diff --git a/pollen-services/src/main/java/org/chorem/pollen/services/service/mail/EmailService.java b/pollen-services/src/main/java/org/chorem/pollen/services/service/mail/EmailService.java index 389ea4a7..af7a11dc 100644 --- a/pollen-services/src/main/java/org/chorem/pollen/services/service/mail/EmailService.java +++ b/pollen-services/src/main/java/org/chorem/pollen/services/service/mail/EmailService.java @@ -309,7 +309,11 @@ public class EmailService extends PollenServiceSupport { } public PollEndReminderEmail newPollEndReminderEmail(Poll poll) { - PollEndReminderEmail email = new PollEndReminderEmail(getLocale(), getTimeZone()); + return newPollEndReminderEmail(poll, getLocale()); + } + + public PollEndReminderEmail newPollEndReminderEmail(Poll poll, Locale locale) { + PollEndReminderEmail email = new PollEndReminderEmail(locale, getTimeZone()); email.setPoll(poll); return email; } diff --git a/pollen-services/src/main/java/org/chorem/pollen/services/service/security/SecurityService.java b/pollen-services/src/main/java/org/chorem/pollen/services/service/security/SecurityService.java index 3dfb9a6d..5d34f418 100644 --- a/pollen-services/src/main/java/org/chorem/pollen/services/service/security/SecurityService.java +++ b/pollen-services/src/main/java/org/chorem/pollen/services/service/security/SecurityService.java @@ -239,7 +239,16 @@ public class SecurityService extends PollenServiceSupport { public void deleteObsoleteSessionTokens() { - Set<SessionToken> sessionTokens = getSessionTokenDao().findAllBeforeEndDate(serviceContext.getNow()); + Set<SessionToken> sessionTokens = getSessionTokenDao().findAllBeforeEndDate(getNow()); + + if (log.isDebugEnabled()) { + sessionTokens.forEach(sessionToken -> + log.debug(String.format("SessionToken %s delete expired : %s", + sessionToken.getPollenToken().getToken(), + sessionToken.getPollenToken().getEndDate())) + ); + } + getSessionTokenDao().deleteAll(sessionTokens); commit(); @@ -288,17 +297,10 @@ public class SecurityService extends PollenServiceSupport { // check that token is still valid Date endDate = sessionToken.getPollenToken().getEndDate(); - Date now = serviceContext.getNow(); + Date now = getNow(); if (endDate.before(now)) { - // expired token - if (log.isDebugEnabled()) { - log.debug(String.format("SessionToken %s delete expired : %s", sessionToken.getPollenToken().getToken(), endDate)); - } - getSessionTokenDao().delete(sessionToken); - commit(); - throw new PollenInvalidSessionTokenException(); } diff --git a/pollen-services/src/main/resources/i18n/pollen-services_en_GB.properties b/pollen-services/src/main/resources/i18n/pollen-services_en_GB.properties index 0c8abba1..7440e08f 100644 --- a/pollen-services/src/main/resources/i18n/pollen-services_en_GB.properties +++ b/pollen-services/src/main/resources/i18n/pollen-services_en_GB.properties @@ -22,6 +22,7 @@ pollen.configuration.defaultVoteCountingType=Default vote counting type used whe pollen.configuration.defaultVoteNotification=Default notification type for the votes of a poll pollen.configuration.defaultVotePageSize=Default number of vote per page pollen.configuration.defaultVoteVisibility=Default vote visiblity +pollen.configuration.deleteObsoleteSessionTokensCronSchedule=Time between two cron jobs of delete obsolete session tokens pollen.configuration.devMode=Dev mode pollen.configuration.feedback.locale=locale to send feedback pollen.configuration.feedback.mails=mails to send feedback diff --git a/pollen-services/src/main/resources/i18n/pollen-services_fr_FR.properties b/pollen-services/src/main/resources/i18n/pollen-services_fr_FR.properties index 07d85331..374861dc 100644 --- a/pollen-services/src/main/resources/i18n/pollen-services_fr_FR.properties +++ b/pollen-services/src/main/resources/i18n/pollen-services_fr_FR.properties @@ -22,6 +22,7 @@ pollen.configuration.defaultVoteCountingType=Type de dépouillement par défaut pollen.configuration.defaultVoteNotification=Type de notification par défaut pour les votes pollen.configuration.defaultVotePageSize=Nombre de votes par page pollen.configuration.defaultVoteVisibility=Visibilité des votes par défaut +pollen.configuration.deleteObsoleteSessionTokensCronSchedule=Intervalle entre deux lancements de la supression des tokens de session obsoletes pollen.configuration.devMode=Mode développement pollen.configuration.feedback.locale=La locale pour envoyer les retours utlisateur pollen.configuration.feedback.mails=Courriel destinataires des retours utilisateur diff --git a/pollen-ui-riot-js/src/main/web/js/Session.js b/pollen-ui-riot-js/src/main/web/js/Session.js index 31067807..3410c698 100644 --- a/pollen-ui-riot-js/src/main/web/js/Session.js +++ b/pollen-ui-riot-js/src/main/web/js/Session.js @@ -83,9 +83,11 @@ class Session { this.datetimeInputSupported = (input.value !== notADateValue); bus.on("unauthorize", () => { - var oldUser = this.user; - this.user = null; - bus.trigger("user", this.user, oldUser); + if (this.user !== null) { + var oldUser = this.user; + this.user = null; + bus.trigger("user", this.user, oldUser); + } }); this.isGtu = false; } diff --git a/pollen-ui-riot-js/src/main/web/tag/Pollen.tag.html b/pollen-ui-riot-js/src/main/web/tag/Pollen.tag.html index 80d439f2..1f97f3ed 100644 --- a/pollen-ui-riot-js/src/main/web/tag/Pollen.tag.html +++ b/pollen-ui-riot-js/src/main/web/tag/Pollen.tag.html @@ -26,6 +26,7 @@ require("./PollenHeader.tag.html"); require("./PollenFooter.tag.html"); require("./PollenMessageManager.tag.html"); require("./SignIn.tag.html"); +require("./SignInAction.tag.js"); require("./SignUp.tag.html"); require("./SignCheck.tag.html"); require("./Home.tag.html"); @@ -264,7 +265,11 @@ require("./popup/GtuChangeModal.tag.html"); this.on("mount", () => { this.listen("locale", this.onLocaleChange); - this.listen("unauthorize", this.refs.signIn.open); + this.listen("unauthorize", () => { + if (!location.hash.startsWith("#signin?hash=")) { + route("signin?hash=" + (location.hash && location.hash.substring(1))); + } + }); this.listen("signIn", this.refs.signIn.open); this.listen("closeSignIn", this.refs.signIn.close); this.listen("messageManagerReady", () => { diff --git a/pollen-ui-riot-js/src/main/web/tag/SigninAction.tag.js b/pollen-ui-riot-js/src/main/web/tag/SignInAction.tag.js similarity index 100% rename from pollen-ui-riot-js/src/main/web/tag/SigninAction.tag.js rename to pollen-ui-riot-js/src/main/web/tag/SignInAction.tag.js diff --git a/pollen-ui-riot-js/src/main/web/tag/admin/UserEditModal.tag.html b/pollen-ui-riot-js/src/main/web/tag/admin/UserEditModal.tag.html index 9cc803dc..05a2109b 100644 --- a/pollen-ui-riot-js/src/main/web/tag/admin/UserEditModal.tag.html +++ b/pollen-ui-riot-js/src/main/web/tag/admin/UserEditModal.tag.html @@ -136,13 +136,13 @@ require("../components/UserEmailAddressList.tag.html"); }); }; - this.deleteAvatar = e => { + this.deleteAvatar = () => { this.confirm(this.__.deleteAvatarMessage).then((confirm) => { if (!confirm) { return Promise.reject(); } return userService.deleteAvatar(this.opts.user.id); - }).then(result => { + }).then(() => { this.opts.user.avatar = null; this.update(); }); diff --git a/pollen-ui-riot-js/src/main/web/tag/admin/Users.tag.html b/pollen-ui-riot-js/src/main/web/tag/admin/Users.tag.html index bed59afc..9951917a 100644 --- a/pollen-ui-riot-js/src/main/web/tag/admin/Users.tag.html +++ b/pollen-ui-riot-js/src/main/web/tag/admin/Users.tag.html @@ -8,12 +8,12 @@ it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. - + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. - + You should have received a copy of the GNU Affero General Public License along with this program. If not, see <http://www.gnu.org/licenses/>. #L% @@ -73,7 +73,7 @@ require("./UserCard.tag.html"); } else { label = this._l("many", this.count); } - if (this.filteredCount != this.count) { + if (this.filteredCount !== this.count) { if (this.filteredCount === 0) { label = this._l("noFound") + label; } else if (this.filteredCount === 1) { diff --git a/pollen-ui-riot-js/src/main/web/tag/components/LazyLoad.tag.html b/pollen-ui-riot-js/src/main/web/tag/components/LazyLoad.tag.html index a8c342c7..8a2e2eeb 100644 --- a/pollen-ui-riot-js/src/main/web/tag/components/LazyLoad.tag.html +++ b/pollen-ui-riot-js/src/main/web/tag/components/LazyLoad.tag.html @@ -8,12 +8,12 @@ it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. - + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. - + You should have received a copy of the GNU Affero General Public License along with this program. If not, see <http://www.gnu.org/licenses/>. #L% @@ -78,7 +78,7 @@ (this.opts.scrollTarget || window).addEventListener("scroll", this.onscroll); if (!this.opts.notLoadOnStart) { - this.reload(); + this.reload().catch(() => {}); } this.on("unmount", () => { -- To stop receiving notification emails like this one, please contact chorem.org SCM administrator <admin+scm@chorem.org>.