001/*
002 * (C) Copyright 2011-2018 Nuxeo (http://nuxeo.com/) and others.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *     http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 * Contributors:
016 * Nuxeo - initial API and implementation
017 */
018
019package org.nuxeo.ecm.user.invite;
020
021import static org.apache.commons.lang3.StringUtils.isBlank;
022import static org.nuxeo.ecm.user.invite.RegistrationRules.FACET_REGISTRATION_CONFIGURATION;
023import static org.nuxeo.ecm.user.invite.RegistrationRules.FIELD_CONFIGURATION_NAME;
024import static org.nuxeo.ecm.user.invite.UserInvitationService.ValidationMethod.EMAIL;
025import static org.nuxeo.ecm.user.invite.UserRegistrationConfiguration.DEFAULT_CONFIGURATION_NAME;
026
027import java.io.IOException;
028import java.io.Serializable;
029import java.io.StringReader;
030import java.io.StringWriter;
031import java.io.Writer;
032import java.util.Date;
033import java.util.HashMap;
034import java.util.List;
035import java.util.Map;
036import java.util.Set;
037
038import javax.mail.Message;
039import javax.mail.MessagingException;
040import javax.mail.Session;
041import javax.mail.Transport;
042import javax.mail.internet.InternetAddress;
043import javax.mail.internet.MimeMessage;
044import javax.naming.InitialContext;
045import javax.naming.NamingException;
046
047import org.apache.commons.lang3.StringUtils;
048import org.apache.commons.logging.Log;
049import org.apache.commons.logging.LogFactory;
050import org.nuxeo.ecm.core.api.CoreSession;
051import org.nuxeo.ecm.core.api.DocumentModel;
052import org.nuxeo.ecm.core.api.DocumentModelList;
053import org.nuxeo.ecm.core.api.DocumentRef;
054import org.nuxeo.ecm.core.api.IdRef;
055import org.nuxeo.ecm.core.api.NuxeoException;
056import org.nuxeo.ecm.core.api.NuxeoPrincipal;
057import org.nuxeo.ecm.core.api.PathRef;
058import org.nuxeo.ecm.core.api.PropertyException;
059import org.nuxeo.ecm.core.api.UnrestrictedSessionRunner;
060import org.nuxeo.ecm.core.api.impl.DocumentModelListImpl;
061import org.nuxeo.ecm.core.api.pathsegment.PathSegmentService;
062import org.nuxeo.ecm.core.api.repository.RepositoryManager;
063import org.nuxeo.ecm.core.api.security.ACE;
064import org.nuxeo.ecm.core.api.security.ACL;
065import org.nuxeo.ecm.core.api.security.ACP;
066import org.nuxeo.ecm.core.event.Event;
067import org.nuxeo.ecm.core.event.EventContext;
068import org.nuxeo.ecm.core.event.EventService;
069import org.nuxeo.ecm.core.event.impl.DocumentEventContext;
070import org.nuxeo.ecm.platform.rendering.api.RenderingException;
071import org.nuxeo.ecm.platform.usermanager.NuxeoPrincipalImpl;
072import org.nuxeo.ecm.platform.usermanager.UserConfig;
073import org.nuxeo.ecm.platform.usermanager.UserManager;
074import org.nuxeo.ecm.platform.usermanager.exceptions.UserAlreadyExistsException;
075import org.nuxeo.runtime.api.Framework;
076import org.nuxeo.runtime.model.ComponentInstance;
077import org.nuxeo.runtime.model.DefaultComponent;
078
079import freemarker.template.Configuration;
080import freemarker.template.Template;
081import freemarker.template.TemplateException;
082
083public class UserInvitationComponent extends DefaultComponent implements UserInvitationService {
084
085    protected static Log log = LogFactory.getLog(UserInvitationService.class);
086
087    public static final String NUXEO_URL_KEY = "nuxeo.url";
088
089    protected String repoName = null;
090
091    protected String testRendering = null;
092
093    protected RenderingHelper rh = new RenderingHelper();
094
095    protected Map<String, UserRegistrationConfiguration> configurations = new HashMap<>();
096
097    private static final String INVITATION_SUBMITTED_EVENT = "invitationSubmitted";
098
099    private static final String INVITATION_ACCEPTED_EVENT = "invitationAccepted";
100
101    private static final String INVITATION_REJECTED_EVENT = "invitationRejected";
102
103    private static final String INVITATION_VALIDATED_EVENT = "invitationValidated";
104
105    public String getTestedRendering() {
106        return testRendering;
107    }
108
109    protected String getTargetRepositoryName() {
110        if (repoName == null) {
111            RepositoryManager rm = Framework.getService(RepositoryManager.class);
112            repoName = rm.getDefaultRepositoryName();
113        }
114        return repoName;
115    }
116
117    protected boolean userAlreadyExists(UserRegistrationInfo userRegistrationInfo) {
118        DocumentModel user = Framework.getService(UserManager.class).getUserModel(userRegistrationInfo.getLogin());
119        return user != null;
120    }
121
122    protected String getJavaMailJndiName() {
123        return Framework.getProperty("jndi.java.mail", "java:/Mail");
124    }
125
126    @Override
127    public DocumentModel getUserRegistrationModel(String configurationName) {
128        // Test if the configuration is defined
129        if (StringUtils.isEmpty(configurationName)) {
130            configurationName = DEFAULT_CONFIGURATION_NAME;
131        }
132        // Get the DocumentModel for the doctype defined in the configuration
133        UserRegistrationModelCreator creator = new UserRegistrationModelCreator(configurationName);
134        creator.runUnrestricted();
135        return creator.getUserRegistrationModel();
136    }
137
138    @Override
139    public DocumentModel getRegistrationRulesDocument(CoreSession session, String configurationName) {
140        // By default, configuration is hold by the root request document
141        return getOrCreateRootDocument(session, configurationName);
142    }
143
144    public DocumentModel getOrCreateRootDocument(CoreSession session, String configurationName) {
145        UserRegistrationConfiguration configuration = getConfiguration(configurationName);
146
147        String targetPath = configuration.getContainerParentPath() + configuration.getContainerName();
148        DocumentRef targetRef = new PathRef(targetPath);
149        DocumentModel root;
150
151        if (!session.exists(targetRef)) {
152            root = session.createDocumentModel(configuration.getContainerDocType());
153            root.setPathInfo(configuration.getContainerParentPath(), configuration.getContainerName());
154            root.setPropertyValue("dc:title", configuration.getContainerTitle());
155            // XXX ACLs ?!!!
156            root = session.createDocument(root);
157        } else {
158            root = session.getDocument(targetRef);
159        }
160
161        // Add configuration facet
162        if (!root.hasFacet(FACET_REGISTRATION_CONFIGURATION)) {
163            root.addFacet(FACET_REGISTRATION_CONFIGURATION);
164            root.setPropertyValue(FIELD_CONFIGURATION_NAME, configuration.getName());
165            root = session.saveDocument(root);
166        }
167        return root;
168    }
169
170    protected class UserRegistrationModelCreator extends UnrestrictedSessionRunner {
171
172        DocumentModel userRegistrationModel;
173
174        protected UserRegistrationConfiguration configuration;
175
176        public UserRegistrationModelCreator(String configurationName) {
177            super(getTargetRepositoryName());
178            configuration = getConfiguration(configurationName);
179        }
180
181        @Override
182        public void run() {
183            userRegistrationModel = session.createDocumentModel(configuration.getRequestDocType());
184        }
185
186        public DocumentModel getUserRegistrationModel() {
187            return userRegistrationModel;
188        }
189    }
190
191    protected class RegistrationCreator extends UnrestrictedSessionRunner {
192
193        protected Map<String, Serializable> additionnalInfo;
194
195        protected String registrationUuid;
196
197        protected ValidationMethod validationMethod;
198
199        protected DocumentModel userRegistrationModel;
200
201        protected UserRegistrationConfiguration configuration;
202
203        public String getRegistrationUuid() {
204            return registrationUuid;
205        }
206
207        public RegistrationCreator(String configurationName, DocumentModel userRegistrationModel,
208                Map<String, Serializable> additionnalInfo, ValidationMethod validationMethod) {
209            super(getTargetRepositoryName());
210            this.userRegistrationModel = userRegistrationModel;
211            this.additionnalInfo = additionnalInfo;
212            this.validationMethod = validationMethod;
213            configuration = getConfiguration(configurationName);
214        }
215
216        @Override
217        public void run() {
218
219            String title = "registration request for "
220                    + userRegistrationModel.getPropertyValue(configuration.getUserInfoUsernameField()) + " ("
221                    + userRegistrationModel.getPropertyValue(configuration.getUserInfoEmailField()) + " "
222                    + userRegistrationModel.getPropertyValue(configuration.getUserInfoCompanyField()) + ") ";
223            PathSegmentService pss = Framework.getService(PathSegmentService.class);
224            String name = pss.generatePathSegment(title + "-" + System.currentTimeMillis());
225
226            String targetPath = getOrCreateRootDocument(session, configuration.getName()).getPathAsString();
227
228            userRegistrationModel.setPathInfo(targetPath, name);
229            userRegistrationModel.setPropertyValue("dc:title", title);
230
231            // validation method
232            userRegistrationModel.setPropertyValue("registration:validationMethod", validationMethod.toString());
233
234            // additionnal infos
235            if (additionnalInfo != null && !additionnalInfo.isEmpty()) {
236                for (String key : additionnalInfo.keySet()) {
237                    try {
238                        userRegistrationModel.setPropertyValue(key, additionnalInfo.get(key));
239                    } catch (PropertyException e) {
240                        // skip silently
241                    }
242                }
243            }
244
245            userRegistrationModel = session.createDocument(userRegistrationModel);
246
247            registrationUuid = userRegistrationModel.getId();
248
249            sendEvent(session, userRegistrationModel, getNameEventRegistrationSubmitted());
250
251            session.save();
252        }
253
254    }
255
256    protected class RegistrationApprover extends UnrestrictedSessionRunner {
257
258        private final UserManager userManager;
259
260        protected String uuid;
261
262        protected Map<String, Serializable> additionnalInfo;
263
264        public RegistrationApprover(String registrationUuid, Map<String, Serializable> additionnalInfo) {
265            super(getTargetRepositoryName());
266            uuid = registrationUuid;
267            this.additionnalInfo = additionnalInfo;
268            this.userManager = Framework.getService(UserManager.class);
269        }
270
271        @Override
272        public void run() {
273
274            DocumentModel doc = session.getDocument(new IdRef(uuid));
275            String validationMethod = (String) doc.getPropertyValue("registration:validationMethod");
276
277            NuxeoPrincipal targetPrincipal = userManager.getPrincipal((String) doc.getPropertyValue("userinfo:login"));
278            if (targetPrincipal != null) {
279                throw new UserAlreadyExistsException();
280            }
281
282            targetPrincipal = userManager.getPrincipal((String) doc.getPropertyValue("userinfo:email"));
283            if (targetPrincipal != null) {
284                DocumentModel target = session.getDocument(
285                        new IdRef((String) doc.getPropertyValue("docinfo:documentId")));
286                ACP acp = target.getACP();
287                Map<String, Serializable> contextData = new HashMap<>();
288                contextData.put("notify", true);
289                contextData.put("comment", doc.getPropertyValue("registration:comment"));
290                acp.addACE(ACL.LOCAL_ACL,
291                        ACE.builder(targetPrincipal.getName(), (String) doc.getPropertyValue("docinfo:permission"))
292                           .creator((String) doc.getPropertyValue("docinfo:creator"))
293                           .contextData(contextData)
294                           .build());
295                target.setACP(acp, true);
296                // test Validation Method
297            } else if (StringUtils.equals(EMAIL.toString(), validationMethod)) {
298                sendValidationEmail(additionnalInfo, doc);
299            }
300
301            doc.setPropertyValue("registration:accepted", true);
302            if (doc.getAllowedStateTransitions().contains("approve")) {
303                doc.followTransition("approve");
304            }
305            doc = session.saveDocument(doc);
306            session.save();
307
308            sendEvent(session, doc, getNameEventRegistrationAccepted());
309        }
310    }
311
312    protected class RegistrationRejector extends UnrestrictedSessionRunner {
313
314        protected String uuid;
315
316        protected Map<String, Serializable> additionnalInfo;
317
318        public RegistrationRejector(String registrationUuid, Map<String, Serializable> additionnalInfo) {
319            super(getTargetRepositoryName());
320            uuid = registrationUuid;
321            this.additionnalInfo = additionnalInfo;
322        }
323
324        @Override
325        public void run() {
326
327            DocumentModel doc = session.getDocument(new IdRef(uuid));
328
329            doc.setPropertyValue("registration:accepted", false);
330            if (doc.getAllowedStateTransitions().contains("reject")) {
331                doc.followTransition("reject");
332            }
333            doc = session.saveDocument(doc);
334            session.save();
335
336            sendEvent(session, doc, getNameEventRegistrationRejected());
337        }
338    }
339
340    protected class RegistrationAcceptator extends UnrestrictedSessionRunner {
341
342        protected String uuid;
343
344        protected Map<String, Serializable> registrationData = new HashMap<>();
345
346        protected Map<String, Serializable> additionnalInfo;
347
348        public RegistrationAcceptator(String uuid, Map<String, Serializable> additionnalInfo) {
349            super(getTargetRepositoryName());
350            this.uuid = uuid;
351            this.additionnalInfo = additionnalInfo;
352        }
353
354        public Map<String, Serializable> getRegistrationData() {
355            return registrationData;
356        }
357
358        @Override
359        public void run() {
360            DocumentRef idRef = new IdRef(uuid);
361
362            DocumentModel registrationDoc = session.getDocument(idRef);
363
364            // additionnal infos
365            for (String key : additionnalInfo.keySet()) {
366                try {
367                    if (DefaultInvitationUserFactory.PASSWORD_KEY.equals(key)) {
368                        // add the password as a transient context data
369                        registrationDoc.putContextData(DefaultInvitationUserFactory.PASSWORD_KEY,
370                                additionnalInfo.get(key));
371                    } else {
372                        registrationDoc.setPropertyValue(key, additionnalInfo.get(key));
373                    }
374                } catch (PropertyException e) {
375                    // skip silently
376                }
377            }
378
379            NuxeoPrincipal principal = null;
380            if (registrationDoc.getLifeCyclePolicy().equals("registrationRequest")) {
381                if (registrationDoc.getCurrentLifeCycleState().equals("approved")) {
382                    try {
383                        UserInvitationService userRegistrationService = Framework.getService(UserInvitationService.class);
384                        UserRegistrationConfiguration config = userRegistrationService.getConfiguration(registrationDoc);
385                        RegistrationRules rules = userRegistrationService.getRegistrationRules(config.getName());
386                        if (rules.allowUserCreation()) {
387                            principal = userRegistrationService.createUser(session, registrationDoc);
388                        }
389                        registrationDoc.followTransition("accept");
390                    } catch (NuxeoException e) {
391                        e.addInfo("Unable to complete registration");
392                        throw e;
393                    }
394                } else {
395                    if (registrationDoc.getCurrentLifeCycleState().equals("accepted")) {
396                        throw new AlreadyProcessedRegistrationException(
397                                "Registration request has already been processed");
398                    } else {
399                        throw new UserRegistrationException("Registration request has not been accepted yet");
400                    }
401                }
402            }
403
404            session.saveDocument(registrationDoc);
405            session.save();
406            sendEvent(session, registrationDoc, getNameEventRegistrationValidated());
407            registrationDoc.detach(sessionIsAlreadyUnrestricted);
408            registrationData.put(REGISTRATION_DATA_DOC, registrationDoc);
409            registrationData.put(REGISTRATION_DATA_USER, principal);
410        }
411
412    }
413
414    protected class RequestIdValidator extends UnrestrictedSessionRunner {
415
416        protected String uuid;
417
418        public RequestIdValidator(String uuid) {
419            super(getTargetRepositoryName());
420            this.uuid = uuid;
421        }
422
423        @Override
424        public void run() {
425            DocumentRef idRef = new IdRef(uuid);
426            // Check if the id matches an existing document
427            if (!session.exists(idRef)) {
428                throw new UserRegistrationException("There is no existing registration request with id " + uuid);
429            }
430
431            // Check if the request has not been already validated
432            DocumentModel registrationDoc = session.getDocument(idRef);
433            if (registrationDoc.getCurrentLifeCycleState().equals("accepted")) {
434                throw new AlreadyProcessedRegistrationException("Registration request has already been processed");
435            }
436        }
437    }
438
439    protected EventContext sendEvent(CoreSession session, DocumentModel source, String evName)
440            throws UserRegistrationException {
441        try {
442            EventService evService = Framework.getService(EventService.class);
443            EventContext evContext = new DocumentEventContext(session, session.getPrincipal(), source);
444
445            Event event = evContext.newEvent(evName);
446
447            evService.fireEvent(event);
448
449            return evContext;
450        } catch (UserRegistrationException ue) {
451            log.warn("Error during event processing", ue);
452            throw ue;
453        }
454
455    }
456
457    protected void sendValidationEmail(Map<String, Serializable> additionnalInfo, DocumentModel registrationDoc) {
458        UserRegistrationConfiguration configuration = getConfiguration(registrationDoc);
459        sendEmail(additionnalInfo, registrationDoc, configuration.getValidationEmailTemplate(),
460                configuration.getValidationEmailTitle());
461    }
462
463    protected void sendEmail(Map<String, Serializable> additionnalInfo, DocumentModel registrationDoc,
464            String emailTemplatePath, String emailTitle) {
465        UserRegistrationConfiguration configuration = getConfiguration(registrationDoc);
466
467        String emailAdress = (String) registrationDoc.getPropertyValue(configuration.getUserInfoEmailField());
468
469        Map<String, Serializable> input = new HashMap<>();
470        Map<String, Serializable> userinfo = new HashMap<>();
471        userinfo.put("firstName", registrationDoc.getPropertyValue(configuration.getUserInfoFirstnameField()));
472        userinfo.put("lastName", registrationDoc.getPropertyValue(configuration.getUserInfoLastnameField()));
473        userinfo.put("login", registrationDoc.getPropertyValue(configuration.getUserInfoUsernameField()));
474        userinfo.put("id", registrationDoc.getId());
475
476        String documentTitle = "";
477
478        if (registrationDoc.hasSchema("docinfo")) {
479            documentTitle = (String) registrationDoc.getPropertyValue("docinfo:documentTitle");
480        }
481        input.put("documentTitle", documentTitle);
482        input.put("configurationName", configuration.getName());
483        input.put("comment", registrationDoc.getPropertyValue("registration:comment"));
484        input.put(UserInvitationService.REGISTRATION_CONFIGURATION_NAME, configuration.getName());
485        input.put("userinfo", (Serializable) userinfo);
486        input.put("info", (Serializable) additionnalInfo);
487        input.put("userAlreadyExists", checkUserFromRegistrationExistence(registrationDoc));
488        input.put("productName", Framework.getProperty("org.nuxeo.ecm.product.name"));
489        StringWriter writer = new StringWriter();
490
491        try {
492            rh.getRenderingEngine().render(emailTemplatePath, input, writer);
493        } catch (RenderingException e) {
494            throw new NuxeoException("Error during rendering email", e);
495        }
496
497        // render custom email subject
498        emailTitle = renderSubjectTemplate(emailTitle, input);
499
500        String body = writer.getBuffer().toString();
501        String copyTo = (String) registrationDoc.getPropertyValue("registration:copyTo");
502        if (!isTestModeSet()) {
503            try {
504                generateMail(emailAdress, copyTo, emailTitle, body);
505            } catch (NamingException | MessagingException e) {
506                throw new NuxeoException("Error while sending mail: ", e);
507            }
508        } else {
509            testRendering = body;
510        }
511    }
512
513    private String renderSubjectTemplate(String emailTitle, Map<String, Serializable> input) {
514        Configuration stringCfg = rh.getEngineConfiguration();
515        Writer out;
516        try {
517            Template templ = new Template("subjectTemplate", new StringReader(emailTitle), stringCfg);
518            out = new StringWriter();
519            templ.process(input, out);
520            out.flush();
521        } catch (IOException | TemplateException e) {
522            throw new NuxeoException("Error while rendering email subject: ", e);
523        }
524        return out.toString();
525    }
526
527    protected static boolean isTestModeSet() {
528        return Framework.isTestModeSet() || !isBlank(Framework.getProperty("org.nuxeo.ecm.tester.name"));
529    }
530
531    protected boolean checkUserFromRegistrationExistence(DocumentModel registrationDoc) {
532        UserRegistrationConfiguration configuration = getConfiguration(registrationDoc);
533        return null != Framework.getService(UserManager.class).getPrincipal(
534                (String) registrationDoc.getPropertyValue(configuration.getUserInfoUsernameField()));
535    }
536
537    protected void generateMail(String destination, String copy, String title, String content)
538            throws NamingException, MessagingException {
539
540        InitialContext ic = new InitialContext();
541        Session session = (Session) ic.lookup(getJavaMailJndiName());
542
543        MimeMessage msg = new MimeMessage(session);
544        msg.setFrom(new InternetAddress(session.getProperty("mail.from")));
545        msg.setRecipients(Message.RecipientType.TO, InternetAddress.parse(destination, false));
546        if (!isBlank(copy)) {
547            msg.addRecipient(Message.RecipientType.CC, new InternetAddress(copy, false));
548        }
549
550        msg.setSubject(title, "UTF-8");
551        msg.setSentDate(new Date());
552        msg.setContent(content, "text/html; charset=utf-8");
553
554        Transport.send(msg);
555    }
556
557    @Override
558    public String submitRegistrationRequest(DocumentModel userRegistrationModel,
559            Map<String, Serializable> additionnalInfo, ValidationMethod validationMethod, boolean autoAccept) {
560        return submitRegistrationRequest(DEFAULT_CONFIGURATION_NAME, userRegistrationModel, additionnalInfo,
561                validationMethod, autoAccept);
562    }
563
564    @Override
565    public DocumentModelList getRegistrationsForUser(final String docId, final String username,
566            final String configurationName) {
567        final DocumentModelList registrationDocs = new DocumentModelListImpl();
568        new UnrestrictedSessionRunner(getTargetRepositoryName()) {
569            @Override
570            public void run() {
571                String query = "SELECT * FROM Document WHERE ecm:currentLifeCycleState != 'validated' AND"
572                        + " ecm:mixinType = '" + getConfiguration(configurationName).getRequestDocType()
573                        + "' AND docinfo:documentId = '%s' AND"
574                        + getConfiguration(configurationName).getUserInfoUsernameField()
575                        + " = '%s' AND ecm:isVersion = 0";
576                query = String.format(query, docId, username);
577                registrationDocs.addAll(session.query(query));
578            }
579        }.runUnrestricted();
580        return registrationDocs;
581    }
582
583    protected static boolean isEmailExist(UserRegistrationConfiguration configuration, DocumentModel userRegistration) {
584        String email = (String) userRegistration.getPropertyValue(configuration.getUserInfoEmailField());
585        if (isBlank(email)) {
586            return false;
587        }
588
589        Map<String, Serializable> filter = new HashMap<>(1);
590        filter.put(UserConfig.EMAIL_COLUMN, email);
591
592        DocumentModelList users = Framework.getService(UserManager.class).searchUsers(filter, null);
593        return !users.isEmpty();
594    }
595
596    @Override
597    public String submitRegistrationRequest(String configurationName, DocumentModel userRegistrationModel,
598            Map<String, Serializable> additionnalInfo, ValidationMethod validationMethod, boolean autoAccept) {
599        RegistrationCreator creator = new RegistrationCreator(configurationName, userRegistrationModel, additionnalInfo,
600                validationMethod);
601        creator.runUnrestricted();
602        String registrationUuid = creator.getRegistrationUuid();
603
604        UserRegistrationConfiguration currentConfig = getConfiguration(configurationName);
605        boolean userAlreadyExists = null != Framework.getService(UserManager.class).getPrincipal(
606                (String) userRegistrationModel.getPropertyValue(currentConfig.getUserInfoUsernameField()));
607
608        if (!userAlreadyExists && isEmailExist(currentConfig, userRegistrationModel)) {
609            log.info("Trying to submit a registration from an existing email with a different username.");
610            throw new UserAlreadyExistsException();
611        }
612
613        // Directly accept registration if the configuration allow it and the
614        // user already exists
615        RegistrationRules registrationRules = getRegistrationRules(configurationName);
616        boolean byPassAdminValidation = autoAccept;
617        byPassAdminValidation |= userAlreadyExists && registrationRules.allowDirectValidationForExistingUser();
618        byPassAdminValidation |= !userAlreadyExists && registrationRules.allowDirectValidationForNonExistingUser();
619        if (byPassAdminValidation) {
620            // Build validationBaseUrl with nuxeo.url property as request is not
621            // accessible.
622            if (!additionnalInfo.containsKey("enterPasswordUrl")) {
623                additionnalInfo.put("enterPasswordUrl", buildEnterPasswordUrl(currentConfig));
624            }
625            acceptRegistrationRequest(registrationUuid, additionnalInfo);
626        }
627        return registrationUuid;
628    }
629
630    protected String buildEnterPasswordUrl(UserRegistrationConfiguration configuration) {
631        String baseUrl = Framework.getProperty(NUXEO_URL_KEY);
632
633        baseUrl = isBlank(baseUrl) ? "/" : baseUrl;
634        if (!baseUrl.endsWith("/")) {
635            baseUrl += "/";
636        }
637        return baseUrl.concat(configuration.getEnterPasswordUrl());
638    }
639
640    @Override
641    public void acceptRegistrationRequest(String requestId, Map<String, Serializable> additionnalInfo)
642            throws UserRegistrationException {
643        RegistrationApprover acceptor = new RegistrationApprover(requestId, additionnalInfo);
644        acceptor.runUnrestricted();
645
646    }
647
648    @Override
649    public void rejectRegistrationRequest(String requestId, Map<String, Serializable> additionnalInfo)
650            throws UserRegistrationException {
651
652        RegistrationRejector rejector = new RegistrationRejector(requestId, additionnalInfo);
653        rejector.runUnrestricted();
654
655    }
656
657    @Override
658    public Map<String, Serializable> validateRegistration(String requestId, Map<String, Serializable> additionnalInfo)
659            throws UserRegistrationException {
660        RegistrationAcceptator validator = new RegistrationAcceptator(requestId, additionnalInfo);
661        validator.runUnrestricted();
662        return validator.getRegistrationData();
663    }
664
665    @Override
666    public Map<String, Serializable> validateRegistrationAndSendEmail(String requestId,
667            Map<String, Serializable> additionnalInfo) throws UserRegistrationException {
668
669        Map<String, Serializable> registrationInfo = validateRegistration(requestId, additionnalInfo);
670
671        Map<String, Serializable> input = new HashMap<>();
672        input.putAll(registrationInfo);
673        input.put("info", (Serializable) additionnalInfo);
674        StringWriter writer = new StringWriter();
675
676        UserRegistrationConfiguration configuration = getConfiguration(
677                (DocumentModel) registrationInfo.get(REGISTRATION_DATA_DOC));
678        try {
679            rh.getRenderingEngine().render(configuration.getSuccessEmailTemplate(), input, writer);
680        } catch (RenderingException e) {
681            throw new NuxeoException("Error during rendering email", e);
682        }
683
684        String emailAdress = ((NuxeoPrincipalImpl) registrationInfo.get("registeredUser")).getEmail();
685        String body = writer.getBuffer().toString();
686        String title = configuration.getValidationEmailTitle();
687        if (!Framework.isTestModeSet()) {
688            try {
689                generateMail(emailAdress, null, title, body);
690            } catch (NamingException | MessagingException e) {
691                throw new NuxeoException("Error while sending mail : ", e);
692            }
693        } else {
694            testRendering = body;
695        }
696
697        return registrationInfo;
698    }
699
700    @Override
701    public void registerContribution(Object contribution, String extensionPoint, ComponentInstance contributor) {
702        if ("configuration".equals(extensionPoint)) {
703            UserRegistrationConfiguration newConfig = (UserRegistrationConfiguration) contribution;
704
705            if (configurations.containsKey(newConfig.getName())) {
706                if (newConfig.isMerge()) {
707                    configurations.get(newConfig.getName()).mergeWith(newConfig);
708                } else if (newConfig.isRemove()) {
709                    configurations.remove(newConfig.getName());
710                } else {
711                    log.warn(
712                            "Trying to register an existing userRegistration configuration without removing or merging it, in: "
713                                    + contributor.getName());
714                }
715            } else {
716                configurations.put(newConfig.getName(), newConfig);
717            }
718        }
719    }
720
721    protected InvitationUserFactory getRegistrationUserFactory(UserRegistrationConfiguration configuration) {
722        InvitationUserFactory factory = null;
723        Class<? extends InvitationUserFactory> factoryClass = configuration.getRegistrationUserFactory();
724        if (factoryClass != null) {
725            try {
726                factory = factoryClass.getConstructor().newInstance();
727            } catch (ReflectiveOperationException e) {
728                log.warn("Failed to instanciate RegistrationUserFactory", e);
729            }
730        }
731        if (factory == null) {
732            factory = new DefaultInvitationUserFactory();
733        }
734        return factory;
735    }
736
737    @Override
738    public NuxeoPrincipal createUser(CoreSession session, DocumentModel registrationDoc)
739            throws UserRegistrationException {
740        UserRegistrationConfiguration configuration = getConfiguration(registrationDoc);
741        return getRegistrationUserFactory(configuration).doCreateUser(session, registrationDoc, configuration);
742    }
743
744    protected class RootDocumentGetter extends UnrestrictedSessionRunner {
745
746        protected DocumentModel doc;
747
748        protected String configurationName;
749
750        protected RootDocumentGetter(String configurationName) {
751            super(getTargetRepositoryName());
752            this.configurationName = configurationName;
753        }
754
755        @Override
756        public void run() {
757            doc = getOrCreateRootDocument(session, configurationName);
758            doc.detach(true);
759        }
760
761        public DocumentModel getDoc() {
762            return doc;
763        }
764    }
765
766    @Override
767    public UserRegistrationConfiguration getConfiguration() {
768        return getConfiguration(DEFAULT_CONFIGURATION_NAME);
769    }
770
771    @Override
772    public UserRegistrationConfiguration getConfiguration(DocumentModel requestDoc) {
773        try {
774            DocumentModel parent = requestDoc.getCoreSession().getDocument(requestDoc.getParentRef());
775            String configurationName = DEFAULT_CONFIGURATION_NAME;
776            if (parent.hasFacet(FACET_REGISTRATION_CONFIGURATION)) {
777                configurationName = (String) parent.getPropertyValue(FIELD_CONFIGURATION_NAME);
778            } else if (requestDoc.hasFacet(FACET_REGISTRATION_CONFIGURATION)) {
779                configurationName = (String) requestDoc.getPropertyValue(FIELD_CONFIGURATION_NAME);
780            }
781
782            if (!configurations.containsKey(configurationName)) {
783                throw new NuxeoException("Configuration " + configurationName + " is not registered");
784            }
785            return configurations.get(configurationName);
786        } catch (NuxeoException e) {
787            log.info("Unable to get request parent document: " + e.getMessage());
788            throw e;
789        }
790    }
791
792    @Override
793    public UserRegistrationConfiguration getConfiguration(String name) {
794        if (!configurations.containsKey(name)) {
795            throw new NuxeoException("Trying to get unknown user registration configuration.");
796        }
797        return configurations.get(name);
798    }
799
800    @Override
801    public RegistrationRules getRegistrationRules(String configurationName) {
802        RootDocumentGetter rdg = new RootDocumentGetter(configurationName);
803        rdg.runUnrestricted();
804        return rdg.getDoc().getAdapter(RegistrationRules.class);
805    }
806
807    @Override
808    public void reviveRegistrationRequests(CoreSession session, List<DocumentModel> registrationDocs) {
809        for (DocumentModel registrationDoc : registrationDocs) {
810            reviveRegistrationRequest(session, registrationDoc, new HashMap<>());
811        }
812    }
813
814    protected void reviveRegistrationRequest(CoreSession session, DocumentModel registrationDoc,
815            Map<String, Serializable> additionalInfos) {
816        UserRegistrationConfiguration configuration = getConfiguration(registrationDoc);
817        // Build validationBaseUrl with nuxeo.url property as request is not
818        // accessible.
819        if (!additionalInfos.containsKey("enterPasswordUrl")) {
820            additionalInfos.put("enterPasswordUrl", buildEnterPasswordUrl(configuration));
821        }
822        sendEmail(additionalInfos, registrationDoc, configuration.getReviveEmailTemplate(),
823                configuration.getReviveEmailTitle());
824    }
825
826    @Override
827    public void deleteRegistrationRequests(CoreSession session, List<DocumentModel> registrationDocs) {
828        for (DocumentModel registration : registrationDocs) {
829            UserRegistrationConfiguration configuration = getConfiguration(registration);
830            if (!registration.hasSchema(configuration.getUserInfoSchemaName())) {
831                throw new NuxeoException("Registration document do not contains needed schema");
832            }
833
834            session.removeDocument(registration.getRef());
835        }
836    }
837
838    @Override
839    public Set<String> getConfigurationsName() {
840        return configurations.keySet();
841    }
842
843    @Override
844    public void checkRequestId(final String requestId) throws UserRegistrationException {
845        RequestIdValidator runner = new RequestIdValidator(requestId);
846        runner.runUnrestricted();
847    }
848
849    @Override
850    public String getNameEventRegistrationSubmitted() {
851        return INVITATION_SUBMITTED_EVENT;
852    }
853
854    @Override
855    public String getNameEventRegistrationAccepted() {
856        return INVITATION_ACCEPTED_EVENT;
857    }
858
859    @Override
860    public String getNameEventRegistrationRejected() {
861        return INVITATION_REJECTED_EVENT;
862    }
863
864    @Override
865    public String getNameEventRegistrationValidated() {
866        return INVITATION_VALIDATED_EVENT;
867    }
868
869}