001/*
002 * (C) Copyright 2010-2015 Nuxeo SA (http://nuxeo.com/) and contributors.
003 *
004 * All rights reserved. This program and the accompanying materials
005 * are made available under the terms of the GNU Lesser General Public License
006 * (LGPL) version 2.1 which accompanies this distribution, and is available at
007 * http://www.gnu.org/licenses/lgpl-2.1.html
008 *
009 * This library is distributed in the hope that it will be useful,
010 * but WITHOUT ANY WARRANTY; without even the implied warranty of
011 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
012 * Lesser General Public License for more details.
013 *
014 * Contributors:
015 *     Thierry Delprat
016 *     Julien Carsique
017 *
018 */
019
020package org.nuxeo.ecm.admin.setup;
021
022import static org.nuxeo.launcher.config.ConfigurationGenerator.PARAM_TEMPLATE_DBNAME;
023
024import java.io.FileNotFoundException;
025import java.io.IOException;
026import java.io.Serializable;
027import java.math.BigDecimal;
028import java.sql.SQLException;
029import java.util.HashMap;
030import java.util.Iterator;
031import java.util.Map;
032import java.util.Map.Entry;
033import java.util.Properties;
034import java.util.TreeMap;
035
036import javax.faces.application.FacesMessage;
037import javax.faces.component.UIComponent;
038import javax.faces.component.UIInput;
039import javax.faces.component.ValueHolder;
040import javax.faces.context.FacesContext;
041import javax.faces.event.AbortProcessingException;
042import javax.faces.event.AjaxBehaviorEvent;
043import javax.faces.validator.ValidatorException;
044import javax.naming.AuthenticationException;
045import javax.naming.NamingException;
046
047import org.apache.commons.lang.StringUtils;
048import org.apache.commons.logging.Log;
049import org.apache.commons.logging.LogFactory;
050import org.jboss.seam.ScopeType;
051import org.jboss.seam.annotations.Factory;
052import org.jboss.seam.annotations.In;
053import org.jboss.seam.annotations.Name;
054import org.jboss.seam.annotations.Scope;
055import org.jboss.seam.contexts.Contexts;
056import org.jboss.seam.faces.FacesMessages;
057import org.jboss.seam.international.StatusMessage;
058
059import org.nuxeo.common.Environment;
060import org.nuxeo.ecm.platform.ui.web.util.ComponentUtils;
061import org.nuxeo.launcher.commons.DatabaseDriverException;
062import org.nuxeo.launcher.config.ConfigurationException;
063import org.nuxeo.launcher.config.ConfigurationGenerator;
064
065/**
066 * Serves UI for the setup screen, handling properties that can be saved on the bin/nuxeo.conf file on server.
067 * <p>
068 * Manages some important parameters to perform validation on them, and accepts custom parameters that would be present
069 * in the server nuxeo.conf file, and moves to advanced mode any property that would not be in that list.
070 *
071 * @since 5.5
072 */
073@Scope(ScopeType.SESSION)
074@Name("setupWizardAction")
075public class SetupWizardActionBean implements Serializable {
076
077    private static final long serialVersionUID = 1L;
078
079    protected static final Log log = LogFactory.getLog(SetupWizardActionBean.class);
080
081    /**
082     * The list of important parameters that need to be presented first to the user
083     */
084    private static final String[] managedKeyParameters = { "nuxeo.bind.address", "nuxeo.url",
085            Environment.NUXEO_DATA_DIR, Environment.NUXEO_LOG_DIR, Environment.PRODUCT_NAME,
086            Environment.PRODUCT_VERSION, "nuxeo.conf", PARAM_TEMPLATE_DBNAME, "nuxeo.db.name", "nuxeo.db.user",
087            "nuxeo.db.password", "nuxeo.db.host", "nuxeo.db.port", "nuxeo.db.min-pool-size", "nuxeo.db.min-pool-size",
088            "nuxeo.db.max-pool-size", "nuxeo.vcs.min-pool-size", "nuxeo.vcs.max-pool-size",
089            "nuxeo.notification.eMailSubjectPrefix", "mailservice.user", "mailservice.password", "mail.store.protocol",
090            "mail.transport.protocol", "mail.store.host", "mail.store.port", "mail.store.user", "mail.store.password",
091            "mail.debug", "mail.transport.host", "mail.transport.port", "mail.transport.auth", "mail.transport.user",
092            "mail.transport.password", "mail.from", "mail.user", "mail.transport.usetls", "nuxeo.http.proxy.host",
093            "nuxeo.http.proxy.port", "nuxeo.http.proxy.login", "nuxeo.http.proxy.password", "org.nuxeo.dev",
094            "nuxeo.directory.type", "nuxeo.user.group.storage", "nuxeo.ldap.url", "nuxeo.ldap.binddn",
095            "nuxeo.ldap.bindpassword", "nuxeo.ldap.retries", "nuxeo.ldap.user.searchBaseDn",
096            "nuxeo.ldap.user.searchClass", "nuxeo.ldap.user.searchFilter", "nuxeo.ldap.user.searchScope",
097            "nuxeo.ldap.user.readonly", "nuxeo.ldap.user.mapping.rdn", "nuxeo.ldap.user.mapping.username",
098            "nuxeo.ldap.user.mapping.password", "nuxeo.ldap.user.mapping.firstname",
099            "nuxeo.ldap.user.mapping.lastname", "nuxeo.ldap.user.mapping.email", "nuxeo.ldap.user.mapping.company",
100            "nuxeo.ldap.group.searchBaseDn", "nuxeo.ldap.group.searchFilter", "nuxeo.ldap.group.searchScope",
101            "nuxeo.ldap.group.readonly", "nuxeo.ldap.group.mapping.rdn", "nuxeo.ldap.group.mapping.name",
102            "nuxeo.ldap.group.mapping.label", "nuxeo.ldap.group.mapping.members.staticAttributeId",
103            "nuxeo.ldap.group.mapping.members.dynamicAttributeId", "nuxeo.ldap.defaultAdministratorId",
104            "nuxeo.ldap.defaultMembersGroup", "nuxeo.user.anonymous.enable", "nuxeo.user.emergency.enable",
105            "nuxeo.user.emergency.username", "nuxeo.user.emergency.password", "nuxeo.user.emergency.firstname",
106            "nuxeo.user.emergency.lastname" };
107
108    protected Map<String, String> parameters;
109
110    protected Map<String, String> advancedParameters;
111
112    protected static final String PROXY_NONE = "none";
113
114    protected static final String PROXY_ANONYMOUS = "anonymous";
115
116    protected static final String PROXY_AUTHENTICATED = "authenticated";
117
118    protected static final String DIRECTORY_DEFAULT = "default";
119
120    protected static final String DIRECTORY_LDAP = "ldap";
121
122    protected static final String DIRECTORY_MULTI = "multi";
123
124    private static final String ERROR_DB_DRIVER = "error.db.driver.notfound";
125
126    private static final String ERROR_DB_CONNECTION = "error.db.connection";
127
128    private static final String ERROR_LDAP_CONNECTION = "error.ldap.connection";
129
130    private static final String ERROR_LDAP_AUTHENTICATION = "error.ldap.authentication";
131
132    private static final String ERROR_DB_FS = "error.db.fs";
133
134    protected String proxyType = PROXY_NONE;
135
136    protected String directoryType = DIRECTORY_DEFAULT;
137
138    protected boolean needsRestart = false;
139
140    @In(create = true)
141    private transient ConfigurationGenerator setupConfigGenerator;
142
143    protected Properties userConfig;
144
145    @In(create = true, required = false)
146    protected FacesMessages facesMessages;
147
148    @In(create = true)
149    protected Map<String, String> messages;
150
151    private Boolean needGroupConfiguration;
152
153    @Factory(value = "setupRequiresRestart", scope = ScopeType.EVENT)
154    public boolean isNeedsRestart() {
155        return needsRestart;
156    }
157
158    public void setNeedsRestart(boolean needsRestart) {
159        this.needsRestart = needsRestart;
160    }
161
162    @Factory(value = "setupConfigGenerator", scope = ScopeType.PAGE)
163    public ConfigurationGenerator getConfigurationGenerator() {
164        if (setupConfigGenerator == null) {
165            setupConfigGenerator = new ConfigurationGenerator();
166            if (setupConfigGenerator.init()) {
167                setParameters();
168            }
169        }
170        return setupConfigGenerator;
171    }
172
173    @Factory(value = "setupConfigurable", scope = ScopeType.APPLICATION)
174    public boolean isConfigurable() {
175        return setupConfigGenerator.isConfigurable();
176    }
177
178    @Factory(value = "advancedParams", scope = ScopeType.EVENT)
179    public Map<String, String> getAdvancedParameters() {
180        return advancedParameters;
181    }
182
183    @Factory(value = "setupParams", scope = ScopeType.EVENT)
184    public Map<String, String> getParameters() {
185        return parameters;
186    }
187
188    /**
189     * Fill {@link #parameters} and {@link #advancedParameters} with properties from #
190     * {@link ConfigurationGenerator#getUserConfig()}
191     *
192     * @since 5.6
193     */
194    protected void setParameters() {
195        userConfig = setupConfigGenerator.getUserConfig();
196        parameters = new HashMap<>();
197        advancedParameters = new TreeMap<>();
198        // will remove managed parameters later in setParameter()
199        for (String key : userConfig.stringPropertyNames()) {
200            if (System.getProperty(key) == null
201                    || key.matches("^(nuxeo|org\\.nuxeo|catalina|derby|h2|java\\.home|"
202                            + "java\\.io\\.tmpdir|tomcat|sun\\.rmi\\.dgc).*")) {
203                advancedParameters.put(key, userConfig.getProperty(key).trim());
204            }
205        }
206        for (String keyParam : managedKeyParameters) {
207            String parameter = userConfig.getProperty(keyParam);
208            setParameter(keyParam, parameter);
209        }
210
211        proxyType = PROXY_NONE;
212        if (parameters.get("nuxeo.http.proxy.host") != null) {
213            proxyType = PROXY_ANONYMOUS;
214            if (parameters.get("nuxeo.http.proxy.login") != null) {
215                proxyType = PROXY_AUTHENTICATED;
216            }
217        }
218
219        if (parameters.get("nuxeo.directory.type") != null) {
220            directoryType = parameters.get("nuxeo.directory.type");
221        }
222    }
223
224    /**
225     * Adds parameter value to the
226     *
227     * @param key parameter key such as used in templates and nuxeo.conf
228     */
229    private void setParameter(String key, String value) {
230        if (value != null) {
231            parameters.put(key, value.trim());
232            advancedParameters.remove(key);
233        }
234    }
235
236    public void save() {
237        saveParameters();
238        setNeedsRestart(true);
239        resetParameters();
240        // initialize setupConfigurator again, as it's in scope page
241        getConfigurationGenerator();
242        facesMessages.add(StatusMessage.Severity.INFO, messages.get("label.parameters.saved"));
243    }
244
245    @SuppressWarnings("unchecked")
246    protected void saveParameters() {
247        // Fix types (replace Long and Boolean with String)
248        for (Iterator<?> entries = parameters.entrySet().iterator(); entries.hasNext();) {
249            @SuppressWarnings("rawtypes")
250            Entry entry = (Entry<String, ?>) entries.next();
251            if (entry.getValue() instanceof Long) {
252                entry.setValue(((Long) entry.getValue()).toString());
253            }
254            if (entry.getValue() instanceof Boolean) {
255                entry.setValue(((Boolean) entry.getValue()).toString());
256            }
257            if (entry.getValue() instanceof BigDecimal) {
258                entry.setValue(entry.getValue().toString());
259            }
260        }
261
262        // manage httpProxy settings (setting null is not accepted)
263        if (!PROXY_AUTHENTICATED.equals(proxyType)) {
264            parameters.put("nuxeo.http.proxy.login", "");
265            parameters.put("nuxeo.http.proxy.password", "");
266        }
267        if (PROXY_NONE.equals(proxyType)) {
268            parameters.put("nuxeo.http.proxy.host", "");
269            parameters.put("nuxeo.http.proxy.port", "");
270        }
271
272        // Remove empty values for password keys
273        for (String pwdKey : new String[] { "nuxeo.db.password", "mailservice.password", "mail.transport.password",
274                "nuxeo.http.proxy.password" }) {
275            if (StringUtils.isEmpty(parameters.get(pwdKey))) {
276                parameters.remove(pwdKey);
277            }
278        }
279
280        Map<String, String> customParameters = new HashMap<>();
281        customParameters.putAll(parameters);
282        customParameters.putAll(advancedParameters);
283        try {
284            setupConfigGenerator.saveFilteredConfiguration(customParameters);
285        } catch (ConfigurationException e) {
286            log.error(e, e);
287        }
288    }
289
290    public void resetParameters() {
291        setupConfigGenerator = null;
292        parameters = null;
293        advancedParameters = null;
294        Contexts.getPageContext().remove("setupConfigGenerator");
295    }
296
297    /**
298     * @since 5.6
299     */
300    public void checkDatabaseParameters(FacesContext context, UIComponent component, Object value) {
301        Map<String, Object> attributes = component.getAttributes();
302        String dbNameInputId = (String) attributes.get("dbNameInputId");
303        String dbUserInputId = (String) attributes.get("dbUserInputId");
304        String dbPwdInputId = (String) attributes.get("dbPwdInputId");
305        String dbHostInputId = (String) attributes.get("dbHostInputId");
306        String dbPortInputId = (String) attributes.get("dbPortInputId");
307
308        if (dbNameInputId == null || dbUserInputId == null || dbPwdInputId == null || dbHostInputId == null
309                || dbPortInputId == null) {
310            log.error("Cannot validate database parameters: missing inputIds");
311            return;
312        }
313
314        UIInput dbNameComp = (UIInput) component.findComponent(dbNameInputId);
315        UIInput dbUserComp = (UIInput) component.findComponent(dbUserInputId);
316        UIInput dbPwdComp = (UIInput) component.findComponent(dbPwdInputId);
317        UIInput dbHostComp = (UIInput) component.findComponent(dbHostInputId);
318        UIInput dbPortComp = (UIInput) component.findComponent(dbPortInputId);
319        if (dbNameComp == null || dbUserComp == null || dbPwdComp == null || dbHostComp == null || dbPortComp == null) {
320            log.error("Cannot validate inputs: not found");
321            return;
322        }
323
324        String dbName = (String) dbNameComp.getLocalValue();
325        String dbUser = (String) dbUserComp.getLocalValue();
326        String dbPwd = (String) dbPwdComp.getLocalValue();
327        String dbHost = (String) dbHostComp.getLocalValue();
328        Long dbPortLong = ((BigDecimal) dbPortComp.getLocalValue()).longValue();
329        String dbPort = dbPortLong.toString();
330
331        if (StringUtils.isEmpty(dbPwd)) {
332            dbPwd = parameters.get("nuxeo.db.password");
333        }
334
335        String errorLabel = null;
336        Exception error = null;
337        try {
338            setupConfigGenerator.checkDatabaseConnection(parameters.get(ConfigurationGenerator.PARAM_TEMPLATE_DBNAME),
339                    dbName, dbUser, dbPwd, dbHost, dbPort);
340        } catch (FileNotFoundException e) {
341            errorLabel = ERROR_DB_FS;
342            error = e;
343        } catch (IOException e) {
344            errorLabel = ERROR_DB_FS;
345            error = e;
346        } catch (DatabaseDriverException e) {
347            errorLabel = ERROR_DB_DRIVER;
348            error = e;
349        } catch (SQLException e) {
350            errorLabel = ERROR_DB_CONNECTION;
351            error = e;
352        }
353        if (error != null) {
354            log.error(error, error);
355            FacesMessage message = new FacesMessage(FacesMessage.SEVERITY_ERROR, ComponentUtils.translate(context,
356                    errorLabel), null);
357            throw new ValidatorException(message);
358        }
359
360        FacesMessage message = new FacesMessage(FacesMessage.SEVERITY_INFO, ComponentUtils.translate(context,
361                "error.db.none"), null);
362        message.setSeverity(FacesMessage.SEVERITY_INFO);
363        context.addMessage(component.getClientId(context), message);
364    }
365
366    public void templateChange(AjaxBehaviorEvent event) {
367        String dbTemplate;
368        UIComponent select = event.getComponent();
369        if (select instanceof ValueHolder) {
370            dbTemplate = (String) ((ValueHolder) select).getValue();
371        } else {
372            log.error("Bad component returned " + select);
373            throw new AbortProcessingException("Bad component returned " + select);
374        }
375        setupConfigGenerator.changeDBTemplate(dbTemplate);
376        setParameters();
377        Contexts.getEventContext().remove("setupParams");
378        Contexts.getEventContext().remove("advancedParams");
379        FacesContext context = FacesContext.getCurrentInstance();
380        context.renderResponse();
381    }
382
383    public void proxyChange(AjaxBehaviorEvent event) {
384        UIComponent select = event.getComponent();
385        if (select instanceof ValueHolder) {
386            proxyType = (String) ((ValueHolder) select).getValue();
387        } else {
388            log.error("Bad component returned " + select);
389            throw new AbortProcessingException("Bad component returned " + select);
390        }
391        Contexts.getEventContext().remove("setupParams");
392        FacesContext context = FacesContext.getCurrentInstance();
393        context.renderResponse();
394    }
395
396    /**
397     * Initialized by {@link #getParameters()}
398     */
399    public String getProxyType() {
400        return proxyType;
401    }
402
403    public void setProxyType(String proxyType) {
404        this.proxyType = proxyType;
405    }
406
407    public String getDirectoryType() {
408        return directoryType;
409    }
410
411    public void setDirectoryType(String directoryType) {
412        parameters.put("nuxeo.directory.type", directoryType);
413        this.directoryType = directoryType;
414    }
415
416    public void setDirectoryStorage(String directoryStorage) {
417        parameters.put("nuxeo.user.group.storage", directoryStorage);
418    }
419
420    public void ldapStorageChange() {
421        needGroupConfiguration = null;
422    }
423
424    public boolean getNeedGroupConfiguration() {
425        if (needGroupConfiguration == null) {
426            String storageType = parameters.get("nuxeo.user.group.storage");
427            if ("userLdapOnly".equals(storageType) || "multiUserSqlGroup".equals(storageType)) {
428                needGroupConfiguration = Boolean.FALSE;
429            } else {
430                needGroupConfiguration = Boolean.TRUE;
431            }
432        }
433        return needGroupConfiguration.booleanValue();
434    }
435
436    public void directoryChange(AjaxBehaviorEvent event) {
437        UIComponent select = event.getComponent();
438        if (select instanceof ValueHolder) {
439            directoryType = (String) ((ValueHolder) select).getValue();
440        } else {
441            log.error("Bad component returned " + select);
442            throw new AbortProcessingException("Bad component returned " + select);
443        }
444        if ("multi".equals(directoryType)) {
445            setDirectoryStorage("multiUserGroup");
446        } else {
447            setDirectoryStorage("default");
448        }
449        needGroupConfiguration = null;
450        Contexts.getEventContext().remove("setupParams");
451        FacesContext context = FacesContext.getCurrentInstance();
452        context.renderResponse();
453    }
454
455    public void checkLdapNetworkParameters(FacesContext context, UIComponent component, Object value) {
456        Map<String, Object> attributes = component.getAttributes();
457        String ldapUrlId = (String) attributes.get("directoryLdapUrl");
458
459        if (ldapUrlId == null) {
460            log.error("Cannot validate LDAP parameters: missing inputIds");
461            return;
462        }
463
464        UIInput ldapUrlComp = (UIInput) component.findComponent(ldapUrlId);
465        if (ldapUrlComp == null) {
466            log.error("Cannot validate LDAP inputs: not found");
467            return;
468        }
469
470        String ldapUrl = (String) ldapUrlComp.getLocalValue();
471
472        String errorLabel = null;
473        Exception error = null;
474        try {
475            setupConfigGenerator.checkLdapConnection(ldapUrl, null, null, false);
476        } catch (NamingException e) {
477            errorLabel = ERROR_LDAP_CONNECTION;
478            error = e;
479        }
480        if (error != null) {
481            log.error(error, error);
482            FacesMessage message = new FacesMessage(FacesMessage.SEVERITY_ERROR, ComponentUtils.translate(context,
483                    errorLabel), null);
484            throw new ValidatorException(message);
485        }
486
487        FacesMessage message = new FacesMessage(FacesMessage.SEVERITY_INFO, ComponentUtils.translate(context,
488                "error.ldap.network.none"), null);
489        message.setSeverity(FacesMessage.SEVERITY_INFO);
490        context.addMessage(component.getClientId(context), message);
491    }
492
493    public void checkLdapAuthenticationParameters(FacesContext context, UIComponent component, Object value) {
494
495        Map<String, Object> attributes = component.getAttributes();
496        String ldapUrlId = (String) attributes.get("ldapUrl");
497        String ldapBinddnId = (String) attributes.get("ldapBindDn");
498        String ldapBindpwdId = (String) attributes.get("ldapBindPwd");
499
500        if (ldapUrlId == null || ldapBinddnId == null || ldapBindpwdId == null) {
501            log.error("Cannot validate LDAP parameters: missing inputIds");
502            return;
503        }
504
505        UIInput ldapUrlComp = (UIInput) component.findComponent(ldapUrlId);
506        UIInput ldapBinddnComp = (UIInput) component.findComponent(ldapBinddnId);
507        UIInput ldapBindpwdComp = (UIInput) component.findComponent(ldapBindpwdId);
508        if (ldapUrlComp == null || ldapBinddnComp == null || ldapBindpwdComp == null) {
509            log.error("Cannot validate LDAP inputs: not found");
510            return;
511        }
512
513        String ldapUrl = (String) ldapUrlComp.getLocalValue();
514        String ldapBindDn = (String) ldapBinddnComp.getLocalValue();
515        String ldapBindPwd = (String) ldapBindpwdComp.getLocalValue();
516
517        String errorLabel = null;
518        Exception error = null;
519        try {
520            setupConfigGenerator.checkLdapConnection(ldapUrl, ldapBindDn, ldapBindPwd, true);
521        } catch (NamingException e) {
522            if (e instanceof AuthenticationException) {
523                errorLabel = ERROR_LDAP_AUTHENTICATION;
524            } else {
525                errorLabel = ERROR_LDAP_CONNECTION;
526            }
527            error = e;
528        }
529        if (error != null) {
530            log.error(error, error);
531            FacesMessage message = new FacesMessage(FacesMessage.SEVERITY_ERROR, ComponentUtils.translate(context,
532                    errorLabel), null);
533            throw new ValidatorException(message);
534        }
535
536        FacesMessage message = new FacesMessage(FacesMessage.SEVERITY_INFO, ComponentUtils.translate(context,
537                "error.ldap.auth.none"), null);
538        message.setSeverity(FacesMessage.SEVERITY_INFO);
539        context.addMessage(component.getClientId(context), message);
540    }
541
542}