001package org.deepamehta.plugins.signup;
002
003import com.sun.jersey.api.view.Viewable;
004import de.deepamehta.core.Association;
005import de.deepamehta.core.ChildTopics;
006import de.deepamehta.core.RelatedTopic;
007import de.deepamehta.core.Topic;
008import de.deepamehta.core.model.*;
009import de.deepamehta.core.service.DeepaMehtaEvent;
010import de.deepamehta.core.service.DeepaMehtaService;
011import de.deepamehta.core.service.EventListener;
012import de.deepamehta.core.service.Inject;
013import de.deepamehta.core.service.ResultList;
014import de.deepamehta.core.service.accesscontrol.AccessControl;
015import de.deepamehta.core.service.accesscontrol.Credentials;
016import de.deepamehta.core.service.event.PostUpdateTopicListener;
017import de.deepamehta.core.storage.spi.DeepaMehtaTransaction;
018import de.deepamehta.plugins.accesscontrol.AccessControlService;
019import de.deepamehta.plugins.webactivator.WebActivatorPlugin;
020import de.deepamehta.plugins.workspaces.WorkspacesService;
021import java.net.MalformedURLException;
022import java.net.URL;
023import java.util.ArrayList;
024import java.util.Collection;
025import java.util.Date;
026import java.util.HashMap;
027import java.util.Iterator;
028import java.util.UUID;
029import java.util.concurrent.Callable;
030import java.util.logging.Level;
031import java.util.logging.Logger;
032import javax.mail.internet.InternetAddress;
033import javax.ws.rs.*;
034import javax.ws.rs.core.Context;
035import javax.ws.rs.core.MediaType;
036import javax.ws.rs.core.UriInfo;
037import org.apache.commons.mail.HtmlEmail;
038import org.codehaus.jettison.json.JSONException;
039import org.codehaus.jettison.json.JSONObject;
040import org.deepamehta.plugins.signup.service.SignupPluginService;
041
042
043/**
044 * Routes registerd by this plugin are:
045 *
046 * /        => login-form
047 * /sign-up => signup-form
048 * /ok      => info-page after account creation
049 *
050 * @name dm4-sign-up
051 * @website https://github.com/mukil/dm4-sign-up
052 * @version 1.1-SNAPSHOT
053 * @author <a href="mailto:malte@mikromedia.de">Malte Reissig</a>;
054 */
055
056@Path("/")
057public class SignupPlugin extends WebActivatorPlugin implements SignupPluginService,
058                                                                PostUpdateTopicListener {
059
060    private static Logger log = Logger.getLogger(SignupPlugin.class.getName());
061
062    // --- DeepaMehta 4 related type URIs
063
064    public static final String MAILBOX_TYPE_URI = "dm4.contacts.email_address";
065    public final static String WS_DM_DEFAULT_URI = "de.workspaces.deepamehta";
066
067    // --- Sign-up related type URIs (Configuration, Template Data)
068
069    private final String SIGN_UP_PLUGIN_TOPIC_URI = "org.deepamehta.sign-up";
070    private final String SIGN_UP_CONFIG_TYPE_URI = "org.deepamehta.signup.configuration";
071    private final String CONFIG_PROJECT_TITLE = "org.deepamehta.signup.config_project_title";
072    private final String CONFIG_WEBAPP_TITLE = "org.deepamehta.signup.config_webapp_title";
073    private final String CONFIG_LOGO_PATH = "org.deepamehta.signup.config_webapp_logo_path";
074    private final String CONFIG_CSS_PATH = "org.deepamehta.signup.config_custom_css_path";
075    private final String CONFIG_READ_MORE_URL = "org.deepamehta.signup.config_read_more_url";
076    private final String CONFIG_PAGES_FOOTER = "org.deepamehta.signup.config_pages_footer";
077    private final String CONFIG_TOS_LABEL = "org.deepamehta.signup.config_tos_label";
078    private final String CONFIG_TOS_DETAILS = "org.deepamehta.signup.config_tos_detail";
079    private final String CONFIG_PD_LABEL = "org.deepamehta.signup.config_pd_label";
080    private final String CONFIG_PD_DETAILS = "org.deepamehta.signup.config_pd_detail";
081
082    private Topic currentModuleConfiguration = null;
083
084    @Inject
085    private AccessControlService acService;
086
087    @Inject /*** Used in migration */
088    private WorkspacesService wsService;
089        
090        @Context UriInfo uri;
091
092    HashMap<String, JSONObject> token = new HashMap<String, JSONObject>();
093
094
095    @Override
096    public void init() {
097        initTemplateEngine();
098        reloadConfiguration();
099    }
100
101    static DeepaMehtaEvent USER_ACCOUNT_CREATE_LISTENER = new DeepaMehtaEvent(UserAccountCreateListener.class) {
102        @Override
103        public void deliver(EventListener listener, Object... params) {
104            ((UserAccountCreateListener) listener).userAccountCreated((Topic) params[0]);
105        }
106    };
107
108    /** Plugin Service Implementation */
109    @GET
110    @Path("/sign-up/check/{username}")
111    @Produces(MediaType.APPLICATION_JSON)
112    public String getUsernameAvailability(@PathParam("username") String username) {
113        JSONObject response = new JSONObject();
114        try {
115            response.put("isAvailable", true);
116            if (isUsernameTaken(username)) response.put("isAvailable", false);
117            return response.toString();
118        } catch (Exception e) {
119            throw new RuntimeException(e);
120        }
121    }
122
123    @GET
124    @Path("/sign-up/check/mailbox/{email}")
125    @Produces(MediaType.APPLICATION_JSON)
126    public String getMailboxAvailability(@PathParam("email") String email) {
127        JSONObject response = new JSONObject();
128        try {
129            response.put("isAvailable", true);
130            if (isMailboxTaken(email)) response.put("isAvailable", false);
131            return response.toString();
132        } catch (Exception e) {
133            throw new RuntimeException(e);
134        }
135    }
136
137    private String createSimpleUserAccount(@PathParam("username") String username, @PathParam("pass-one") String password,
138            @PathParam("mailbox") String mailbox) {
139                DeepaMehtaTransaction tx = dms.beginTx();
140                try {
141                        if (isUsernameTaken(username)) throw new RuntimeException("Username is unavailable!");
142                        Credentials creds = new Credentials(new JSONObject()
143                                        .put("username", username)
144                                        .put("password", password));
145                        // 1) Create new user (in which workspace), just within the private one, no?
146                        final Topic usernameTopic = acService.createUserAccount(creds);
147                        final String eMailAddressValue = mailbox;
148                        // 2) create and associate e-mail address topic
149                        dms.getAccessControl().runWithoutWorkspaceAssignment(new Callable<Topic>() {
150                                @Override
151                                public Topic call() {
152                                        Topic eMailAddress = dms.createTopic(new TopicModel(MAILBOX_TYPE_URI, new SimpleValue(eMailAddressValue)));
153                                        // 3) fire custom event ### this is useless since fired by "anonymous" (this request scope)
154                                        // dms.fireEvent(USER_ACCOUNT_CREATE_LISTENER, user);
155                                        AccessControl acCore = dms.getAccessControl();
156                                        // 4) assign new e-mail address topic to admins "Private workspace"
157                                        Topic adminWorkspace = dms.getAccessControl().getPrivateWorkspace("admin");
158                                        acCore.assignToWorkspace(eMailAddress, adminWorkspace.getId());
159                                        // 5) associate email address to "username" topic too
160                                        Association assoc = dms.createAssociation(new AssociationModel("dm4.core.association",
161                                                new TopicRoleModel(eMailAddress.getId(), "dm4.core.child"),
162                                                new TopicRoleModel(usernameTopic.getId(), "dm4.core.parent")));
163                                        // 6) assign that association also to admins "Private Workspace"
164                                        acCore.assignToWorkspace(assoc, adminWorkspace.getId());
165                                        return eMailAddress;
166                                }
167                        });
168                        log.info("Created new user account for user \"" + username + "\" and " + eMailAddressValue);
169                        // 6) Inform administrations about successfull account creation
170                        sendNotificationMail(username, mailbox.trim());
171                        tx.success();
172                        return username;
173                } catch (Exception e) {
174                        throw new RuntimeException("Creating simple user account FAILED!", e);
175                } finally {
176                        tx.finish();
177                }
178    }
179
180    @GET
181    @Path("/sign-up/send/{username}/{pass-one}/{mailbox}")
182    public String createUserValidationToken(@PathParam("username") String username,
183                @PathParam("pass-one") String password, @PathParam("mailbox") String mailbox) {
184                //
185                String response = null;
186                try {
187                        String key = UUID.randomUUID().toString();
188                        long valid = new Date().getTime() + 3600000; // Token is valid fo 60 min
189                        JSONObject value = new JSONObject();
190                        value.put("username", username.trim());
191                        value.put("mailbox", mailbox.trim());
192                        value.put("password", password);
193                        value.put("expiration", valid);
194                        token.put(key, value);
195                        log.info("Set up key " +key+ " for "+mailbox+" sending confirmation mail valid till "
196                                + new Date(valid).toString());
197                        sendConfirmationMail(key, username, mailbox.trim());
198                } catch (JSONException ex) {
199                        Logger.getLogger(SignupPlugin.class.getName()).log(Level.SEVERE, null, ex);
200                        throw new RuntimeException(ex);
201                }
202                return response;
203    }
204        
205        @GET
206    @Path("/sign-up/confirm/{token}")
207    public Viewable handleTokenRequest(@PathParam("token") String key) {
208                String username;
209                if (token.isEmpty()) throw new RuntimeException("Sorry, we lost all the tokens!");
210                try {
211                        JSONObject input = token.get(key);
212                        token.remove(key);
213                        username = input.getString("username");
214                        if (input.getLong("expiration") > new Date().getTime()) {
215                                log.info("Trying to create user account for " + input.getString("mailbox"));
216                                createSimpleUserAccount(username, input.getString("password"), input.getString("mailbox"));
217                        } else {
218                                viewData("username", null);
219                                viewData("message", "Timeout");
220                                return view("failure");
221                        }
222                } catch (JSONException ex) {
223                        Logger.getLogger(SignupPlugin.class.getName()).log(Level.SEVERE, null, ex);
224                        viewData("message", "An error occured processing your request");
225                        log.severe("Account creation failed due to " + ex.getMessage() + " caused by " + ex.getCause().toString());
226                        return view("failure");
227                }
228                log.info("Account confirmed & succesfully created, username: " + username);
229                viewData("username", username);
230                viewData("message", "User account created successfully");
231                return getAccountCreationOKView();
232    }
233
234
235
236    /** --- Private Helpers --- */
237        
238    /**
239     * Loads the next sign-up configuration topic for this plugin.
240     *
241     * @see init()
242     * @see postUpdateTopic()
243     */
244    private Topic reloadConfiguration() {
245        log.info("Sign-up: Reloading sign-up plugin configuration.");
246        currentModuleConfiguration = getCurrentSignupConfiguration();
247        currentModuleConfiguration.loadChildTopics();
248        log.info("Sign-up: Loaded sign-up configuration => \"" + currentModuleConfiguration.getUri()
249                + "\", \"" + currentModuleConfiguration.getSimpleValue() + "\"");
250        return currentModuleConfiguration;
251    }
252
253        private void sendConfirmationMail(String key, String username, String mailbox) {
254                try {
255                        String webAppTitle = currentModuleConfiguration.getChildTopics()
256                                .getString("org.deepamehta.signup.config_webapp_title");
257                        URL url = uri.getBaseUri().toURL();
258                        log.info("The confirmation mails token request URL should be:"
259                                + "\n" + url + "sign-up/confirm/" + key);
260                        sendSystemMail("Your account on " + webAppTitle,
261                                "Hi "+username+",\n\nyou can complete the account registration process for " + webAppTitle
262                                        + " through visiting the following webpage:\n" + url + "sign-up/confirm/" + key
263                                        + "\n\nCheers!\n\n", mailbox);
264                } catch (MalformedURLException ex) {
265                        throw new RuntimeException(ex);
266                }
267        }
268
269    private void sendNotificationMail(String username, String mailbox) {
270                String webAppTitle = currentModuleConfiguration.getChildTopics().getString("org.deepamehta.signup.config_webapp_title");
271        sendSystemMail("Account registration on " + webAppTitle,
272                        "\nA user has registered.\n\nUsername: " + username + "\nEmail: "+mailbox+"\n\n", null);
273    }
274
275        private void sendSystemMail(String subject, String message, String recipient) {
276                // Fix: Classloader issue we have in OSGi since using Pax web
277        Thread.currentThread().setContextClassLoader(SignupPlugin.class.getClassLoader());
278        log.info("BeforeSend: Set classloader to " + Thread.currentThread().getContextClassLoader().toString());
279        HtmlEmail email = new HtmlEmail();
280        email.setDebug(true); // => System.out.println(SMTP communication);
281        email.setHostName("localhost"); // ### use getBaseUri() from HTTP Context?
282        try {
283            // ..) Set Senders of Mail
284                        String projectName = currentModuleConfiguration.getChildTopics().getString("org.deepamehta.signup.config_project_title");
285                        String sender = currentModuleConfiguration.getChildTopics().getString("org.deepamehta.signup.config_from_mailbox");
286            email.setFrom(sender.trim(), projectName.trim());
287                        // ..) Set Subject of Mail
288            email.setSubject(subject);
289            // ..) Set Message Body
290            email.setTextMsg(message);
291                        // ..) Set recipient of notification mail
292                        String recipientValue;
293                        if (recipient != null) {
294                                recipientValue = recipient.trim();
295                        } else {
296                                recipientValue = currentModuleConfiguration.getChildTopics()
297                                        .getString("org.deepamehta.signup.config_admin_mailbox").trim();
298                        }
299                        log.info("Loaded current configuration topic, sending notification mail to " + recipientValue);
300                        Collection<InternetAddress> recipients = new ArrayList<InternetAddress>();
301            recipients.add(new InternetAddress(recipientValue));
302            email.setTo(recipients);
303                        email.send();
304            log.info("Mail was SUCCESSFULLY sent to " + email.getToAddresses() + " mail addresses");
305        } catch (Exception ex) {
306            throw new RuntimeException("Sending notification mail FAILED", ex);
307        } finally {
308                        // Fix: Classloader issue we have in OSGi since using Pax web
309                        Thread.currentThread().setContextClassLoader(DeepaMehtaService.class.getClassLoader());
310                        log.info("AfterSend: Set Classloader back " + Thread.currentThread().getContextClassLoader().toString());
311                }
312        }
313
314    private boolean isUsernameTaken(String username) {
315                String value = username.trim();
316        Topic userNameTopic = acService.getUsernameTopic(value);
317        return (userNameTopic != null);
318    }
319
320    private boolean isMailboxTaken(String email) {
321        String value = email.toLowerCase().trim();
322        return dms.getAccessControl().emailAddressExists(value);
323    }
324
325    /**
326     * The sign-up configuration object is loaded once when this bundle/plugin is
327     * initialized by the framework and as soon as one configuration was edited.
328     *
329     * @see reloadConfiguration()
330     */
331    private Topic getCurrentSignupConfiguration() {
332        Topic pluginTopic = dms.getTopic("uri", new SimpleValue(SIGN_UP_PLUGIN_TOPIC_URI));
333        return pluginTopic.getRelatedTopic("dm4.core.association", "dm4.core.default", "dm4.core.default",
334                SIGN_UP_CONFIG_TYPE_URI);
335    }
336
337
338
339    /** --- Sign-up Routes --- */
340
341    @GET
342    @Produces(MediaType.TEXT_HTML)
343    public Viewable getLoginFormView() {
344        // fixme: use acl service to check if a session already exists and if so, redirect to dm-webclient directly
345        prepareSignupPage();
346        return view("login");
347    }
348
349    @GET
350    @Path("/sign-up")
351    @Produces(MediaType.TEXT_HTML)
352    public Viewable getSignupFormView() {
353        // fixme: use acl service to check if a session already exists and if so, redirect to dm-webclient directly
354        prepareSignupPage();
355        return view("sign-up");
356    }
357
358    @GET
359    @Path("/ok")
360    @Produces(MediaType.TEXT_HTML)
361    public Viewable getAccountCreationOKView() {
362        prepareSignupPage();
363        return view("ok");
364    }
365        
366        @GET
367    @Path("/error")
368    @Produces(MediaType.TEXT_HTML)
369    public Viewable getFailureView() {
370        prepareSignupPage();
371        return view("failure");
372    }
373
374    @GET
375    @Path("/token-info")
376    @Produces(MediaType.TEXT_HTML)
377    public Viewable getConfirmationInfoView() {
378        prepareSignupPage();
379        return view("confirmation");
380    }
381        
382        
383
384    private void prepareSignupPage() {
385        if (currentModuleConfiguration != null) {
386            log.info("Preparing views according to current module configuration.");
387            ChildTopics configuration = currentModuleConfiguration.getChildTopics();
388            viewData("title", configuration.getTopic(CONFIG_WEBAPP_TITLE).getSimpleValue().toString());
389            viewData("logo_path", configuration.getTopic(CONFIG_LOGO_PATH).getSimpleValue().toString());
390            viewData("css_path", configuration.getTopic(CONFIG_CSS_PATH).getSimpleValue().toString());
391            viewData("project_name", configuration.getTopic(CONFIG_PROJECT_TITLE).getSimpleValue().toString());
392            viewData("read_more_url", configuration.getTopic(CONFIG_READ_MORE_URL).getSimpleValue().toString());
393            viewData("tos_label", configuration.getTopic(CONFIG_TOS_LABEL).getSimpleValue().toString());
394            viewData("tos_details", configuration.getTopic(CONFIG_TOS_DETAILS).getSimpleValue().toString());
395            viewData("pd_label", configuration.getTopic(CONFIG_PD_LABEL).getSimpleValue().toString());
396            viewData("pd_details", configuration.getTopic(CONFIG_PD_DETAILS).getSimpleValue().toString());
397            viewData("footer", configuration.getTopic(CONFIG_PAGES_FOOTER).getSimpleValue().toString());
398        } else {
399            log.warning("Could not load module configuration!");
400        }
401
402    }
403
404    public void postUpdateTopic(Topic topic, TopicModel tm, TopicModel tm1) {
405        if (topic.getTypeUri().equals(SIGN_UP_CONFIG_TYPE_URI)) {
406            reloadConfiguration();
407        }
408    }
409
410}