001/*
002    Licensed to the Apache Software Foundation (ASF) under one
003    or more contributor license agreements.  See the NOTICE file
004    distributed with this work for additional information
005    regarding copyright ownership.  The ASF licenses this file
006    to you under the Apache License, Version 2.0 (the
007    "License"); you may not use this file except in compliance
008    with the License.  You may obtain a copy of the License at
009
010       http://www.apache.org/licenses/LICENSE-2.0
011
012    Unless required by applicable law or agreed to in writing,
013    software distributed under the License is distributed on an
014    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015    KIND, either express or implied.  See the License for the
016    specific language governing permissions and limitations
017    under the License.  
018 */
019package org.apache.wiki.ui;
020
021import org.apache.wiki.api.core.Engine;
022import org.apache.wiki.api.core.Session;
023import org.apache.wiki.api.providers.AttachmentProvider;
024import org.apache.wiki.api.spi.Wiki;
025import org.apache.wiki.auth.NoSuchPrincipalException;
026import org.apache.wiki.auth.UserManager;
027import org.apache.wiki.auth.WikiPrincipal;
028import org.apache.wiki.auth.WikiSecurityException;
029import org.apache.wiki.auth.authorize.Group;
030import org.apache.wiki.auth.authorize.GroupManager;
031import org.apache.wiki.auth.user.UserDatabase;
032import org.apache.wiki.auth.user.UserProfile;
033import org.apache.wiki.i18n.InternationalizationManager;
034import org.apache.wiki.pages.PageManager;
035import org.apache.wiki.providers.FileSystemProvider;
036import org.apache.wiki.util.TextUtil;
037
038import javax.servlet.ServletConfig;
039import javax.servlet.http.HttpServletRequest;
040import java.io.File;
041import java.io.IOException;
042import java.io.OutputStream;
043import java.nio.file.Files;
044import java.text.MessageFormat;
045import java.util.Properties;
046import java.util.ResourceBundle;
047import java.util.Set;
048import java.util.stream.Collectors;
049import org.apache.log4j.Logger;
050
051/**
052 * Manages JSPWiki installation on behalf of <code>admin/Install.jsp</code>. The contents of this class were previously part of
053 * <code>Install.jsp</code>.
054 *
055 * @since 2.4.20
056 */
057public class Installer {
058    private static final Logger LOG = Logger.getLogger(Installer.class);
059
060    public static final String ADMIN_ID = "admin";
061    public static final String ADMIN_NAME = "Administrator";
062    public static final String INSTALL_INFO = "Installer.Info";
063    public static final String INSTALL_ERROR = "Installer.Error";
064    public static final String INSTALL_WARNING = "Installer.Warning";
065    public static final String APP_NAME = Engine.PROP_APPNAME;
066    public static final String STORAGE_DIR = AttachmentProvider.PROP_STORAGEDIR;
067    public static final String PAGE_DIR = FileSystemProvider.PROP_PAGEDIR;
068    public static final String WORK_DIR = Engine.PROP_WORKDIR;
069    public static final String ADMIN_GROUP = "Admin";
070    public static final String PROPFILENAME = "jspwiki-custom.properties" ;
071    public static String TMP_DIR;
072    private final Session m_session;
073    private final File m_propertyFile;
074    private final Properties m_props;
075    private final Engine m_engine;
076    private final HttpServletRequest m_request;
077    private boolean m_validated;
078    
079    public Installer( final HttpServletRequest request, final ServletConfig config ) {
080        // Get wiki session for this user
081        m_engine = Wiki.engine().find( config );
082        m_session = Wiki.session().find( m_engine, request );
083        
084        // Get the file for properties
085        m_propertyFile = new File(TMP_DIR, PROPFILENAME);
086        m_props = new Properties();
087        
088        // Stash the request
089        m_request = request;
090        m_validated = false;
091        TMP_DIR = m_engine.getWikiProperties().getProperty( "jspwiki.workDir" );
092    }
093    
094    /**
095     * Returns <code>true</code> if the administrative user had been created previously.
096     *
097     * @return the result
098     */
099    public boolean adminExists() {
100        // See if the admin user exists already
101        final UserManager userMgr = m_engine.getManager( UserManager.class );
102        final UserDatabase userDb = userMgr.getUserDatabase();
103        try {
104            userDb.findByLoginName( ADMIN_ID );
105            return true;
106        } catch ( final NoSuchPrincipalException e ) {
107            return false;
108        }
109    }
110    
111    /**
112     * Creates an administrative user and returns the new password. If the admin user exists, the password will be <code>null</code>.
113     *
114     * @return the password
115     */
116    public String createAdministrator() throws WikiSecurityException {
117        if ( !m_validated ) {
118            throw new WikiSecurityException( "Cannot create administrator because one or more of the installation settings are invalid." );
119        }
120        
121        if ( adminExists() ) {
122            return null;
123        }
124        
125        // See if the admin user exists already
126        final UserManager userMgr = m_engine.getManager( UserManager.class );
127        final UserDatabase userDb = userMgr.getUserDatabase();
128        String password = null;
129        
130        try {
131            userDb.findByLoginName( ADMIN_ID );
132        } catch( final NoSuchPrincipalException e ) {
133            // Create a random 12-character password
134            password = TextUtil.generateRandomPassword();
135            final UserProfile profile = userDb.newProfile();
136            profile.setLoginName( ADMIN_ID );
137            profile.setFullname( ADMIN_NAME );
138            profile.setPassword( password );
139            userDb.save( profile );
140        }
141        
142        // Create a new admin group
143        final GroupManager groupMgr = m_engine.getManager( GroupManager.class );
144        Group group;
145        try {
146            group = groupMgr.getGroup( ADMIN_GROUP );
147            group.add( new WikiPrincipal( ADMIN_NAME ) );
148        } catch( final NoSuchPrincipalException e ) {
149            group = groupMgr.parseGroup( ADMIN_GROUP, ADMIN_NAME, true );
150        }
151        groupMgr.setGroup( m_session, group );
152        
153        return password;
154    }
155    
156    /**
157     * Returns the properties as a "key=value" string separated by newlines
158     * @return the string
159     */
160    public String getPropertiesList() {
161        final Set< String > keys = m_props.stringPropertyNames();
162        return keys.stream().map( key -> key + " = " + m_props.getProperty( key ) + "\n" ).collect( Collectors.joining() );
163    }
164
165    public String getPropertiesPath() {
166        return m_propertyFile.getAbsolutePath();
167    }
168
169    /**
170     * Returns a property from the Engine's properties.
171     * @param key the property key
172     * @return the property value
173     */
174    public String getProperty( final String key ) {
175        return m_props.getProperty( key );
176    }
177    
178    public void parseProperties () {
179        final ResourceBundle rb = ResourceBundle.getBundle( InternationalizationManager.CORE_BUNDLE, m_session.getLocale() );
180        m_validated = false;
181
182        // Get application name
183        String nullValue = m_props.getProperty( APP_NAME, rb.getString( "install.installer.default.appname" ) );
184        parseProperty( APP_NAME, nullValue );
185
186        // Get work directory
187        nullValue = m_props.getProperty( WORK_DIR, TMP_DIR );
188        parseProperty( WORK_DIR, nullValue );
189
190        // Get page directory
191        nullValue = m_props.getProperty( PAGE_DIR, m_props.getProperty( WORK_DIR, TMP_DIR ) + File.separatorChar + "data" );
192        parseProperty( PAGE_DIR, nullValue );
193
194        // Set a few more default properties, for easy setup
195        m_props.setProperty( STORAGE_DIR, m_props.getProperty( PAGE_DIR ) );
196        m_props.setProperty( PageManager.PROP_PAGEPROVIDER, "VersioningFileProvider" );
197    }
198    
199    public void saveProperties() {
200        final ResourceBundle rb = ResourceBundle.getBundle( InternationalizationManager.CORE_BUNDLE, m_session.getLocale() );
201        // Write the file back to disk
202        try {
203            try( final OutputStream out = Files.newOutputStream( m_propertyFile.toPath() ) ) {
204                m_props.store( out, null );
205            }
206            m_session.addMessage( INSTALL_INFO, MessageFormat.format(rb.getString("install.installer.props.saved"), m_propertyFile) );
207        } catch( final IOException e ) {
208            LOG.warn("save properties failed", e);
209            final Object[] args = {  m_props.toString() };
210            m_session.addMessage( INSTALL_ERROR, MessageFormat.format( rb.getString( "install.installer.props.notsaved" ), args ) );
211        }
212    }
213    
214    public boolean validateProperties() {
215        final ResourceBundle rb = ResourceBundle.getBundle( InternationalizationManager.CORE_BUNDLE, m_session.getLocale() );
216        m_session.clearMessages( INSTALL_ERROR );
217        parseProperties();
218        // sanitize pages, attachments and work directories
219        sanitizePath( PAGE_DIR );
220        sanitizePath( STORAGE_DIR );
221        sanitizePath( WORK_DIR );
222        validateNotNull( PAGE_DIR, rb.getString( "install.installer.validate.pagedir" ) );
223        validateNotNull( APP_NAME, rb.getString( "install.installer.validate.appname" ) );
224        validateNotNull( WORK_DIR, rb.getString( "install.installer.validate.workdir" ) );
225
226        if( m_session.getMessages( INSTALL_ERROR ).length == 0 ) {
227            m_validated = true;
228        }
229        return m_validated;
230    }
231        
232    /**
233     * Sets a property based on the value of an HTTP request parameter. If the parameter is not found, a default value is used instead.
234     *
235     * @param param the parameter containing the value we will extract
236     * @param defaultValue the default to use if the parameter was not passed in the request
237     */
238    private void parseProperty( final String param, final String defaultValue ) {
239        String value = m_request.getParameter( param );
240        if( value == null ) {
241            value = defaultValue;
242        }
243        m_props.put( param, value );
244    }
245    
246    /**
247     * Simply sanitizes any path which contains backslashes (sometimes Windows users may have them) by expanding them to double-backslashes
248     *
249     * @param key the key of the property to sanitize
250     */
251    private void sanitizePath( final String key ) {
252        String s = m_props.getProperty( key );
253        s = TextUtil.replaceString(s, "\\", "\\\\" );
254        s = s.trim();
255        m_props.put( key, s );
256    }
257
258    public void restoreUserValues() {
259        desanitizePath( PAGE_DIR );
260        desanitizePath( STORAGE_DIR );
261        desanitizePath( WORK_DIR );
262    }
263
264    /**
265     * Simply removes sanitizations so values can be shown back to the user as they were entered
266     *
267     * @param key the key of the property to sanitize
268     */
269    private void desanitizePath( final String key ) {
270        String s = m_props.getProperty( key );
271        s = TextUtil.replaceString(s, "\\\\", "\\" );
272        s = s.trim();
273        m_props.put( key, s );
274    }
275    
276    private void validateNotNull( final String key, final String message ) {
277        final String value = m_props.getProperty( key );
278        if ( value == null || value.isEmpty() ) {
279            m_session.addMessage( INSTALL_ERROR, message );
280        }
281    }
282    
283}