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;
048
049/**
050 * Manages JSPWiki installation on behalf of <code>admin/Install.jsp</code>. The contents of this class were previously part of
051 * <code>Install.jsp</code>.
052 *
053 * @since 2.4.20
054 */
055public class Installer {
056
057    public static final String ADMIN_ID = "admin";
058    public static final String ADMIN_NAME = "Administrator";
059    public static final String INSTALL_INFO = "Installer.Info";
060    public static final String INSTALL_ERROR = "Installer.Error";
061    public static final String INSTALL_WARNING = "Installer.Warning";
062    public static final String APP_NAME = Engine.PROP_APPNAME;
063    public static final String STORAGE_DIR = AttachmentProvider.PROP_STORAGEDIR;
064    public static final String PAGE_DIR = FileSystemProvider.PROP_PAGEDIR;
065    public static final String WORK_DIR = Engine.PROP_WORKDIR;
066    public static final String ADMIN_GROUP = "Admin";
067    public static final String PROPFILENAME = "jspwiki-custom.properties" ;
068    public static final String TMP_DIR = System.getProperty("java.io.tmpdir");
069    private final Session m_session;
070    private final File m_propertyFile;
071    private final Properties m_props;
072    private final Engine m_engine;
073    private final HttpServletRequest m_request;
074    private boolean m_validated;
075    
076    public Installer( final HttpServletRequest request, final ServletConfig config ) {
077        // Get wiki session for this user
078        m_engine = Wiki.engine().find( config );
079        m_session = Wiki.session().find( m_engine, request );
080        
081        // Get the file for properties
082        m_propertyFile = new File(TMP_DIR, PROPFILENAME);
083        m_props = new Properties();
084        
085        // Stash the request
086        m_request = request;
087        m_validated = false;
088    }
089    
090    /**
091     * Returns <code>true</code> if the administrative user had been created previously.
092     *
093     * @return the result
094     */
095    public boolean adminExists() {
096        // See if the admin user exists already
097        final UserManager userMgr = m_engine.getManager( UserManager.class );
098        final UserDatabase userDb = userMgr.getUserDatabase();
099        try {
100            userDb.findByLoginName( ADMIN_ID );
101            return true;
102        } catch ( final NoSuchPrincipalException e ) {
103            return false;
104        }
105    }
106    
107    /**
108     * Creates an administrative user and returns the new password. If the admin user exists, the password will be <code>null</code>.
109     *
110     * @return the password
111     */
112    public String createAdministrator() throws WikiSecurityException {
113        if ( !m_validated ) {
114            throw new WikiSecurityException( "Cannot create administrator because one or more of the installation settings are invalid." );
115        }
116        
117        if ( adminExists() ) {
118            return null;
119        }
120        
121        // See if the admin user exists already
122        final UserManager userMgr = m_engine.getManager( UserManager.class );
123        final UserDatabase userDb = userMgr.getUserDatabase();
124        String password = null;
125        
126        try {
127            userDb.findByLoginName( ADMIN_ID );
128        } catch( final NoSuchPrincipalException e ) {
129            // Create a random 12-character password
130            password = TextUtil.generateRandomPassword();
131            final UserProfile profile = userDb.newProfile();
132            profile.setLoginName( ADMIN_ID );
133            profile.setFullname( ADMIN_NAME );
134            profile.setPassword( password );
135            userDb.save( profile );
136        }
137        
138        // Create a new admin group
139        final GroupManager groupMgr = m_engine.getManager( GroupManager.class );
140        Group group;
141        try {
142            group = groupMgr.getGroup( ADMIN_GROUP );
143            group.add( new WikiPrincipal( ADMIN_NAME ) );
144        } catch( final NoSuchPrincipalException e ) {
145            group = groupMgr.parseGroup( ADMIN_GROUP, ADMIN_NAME, true );
146        }
147        groupMgr.setGroup( m_session, group );
148        
149        return password;
150    }
151    
152    /**
153     * Returns the properties as a "key=value" string separated by newlines
154     * @return the string
155     */
156    public String getPropertiesList() {
157        final StringBuilder result = new StringBuilder();
158        final Set< String > keys = m_props.stringPropertyNames();
159        for( final String key : keys ) {
160            result.append(key ).append( " = " ).append( m_props.getProperty( key ) ).append( "\n" );
161        }
162        return result.toString();
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/sanitize page directory
187        nullValue = m_props.getProperty( PAGE_DIR, rb.getString( "install.installer.default.pagedir" ) );
188        parseProperty( PAGE_DIR, nullValue );
189        sanitizePath( PAGE_DIR );
190
191        // Get/sanitize work directory
192        nullValue = m_props.getProperty( WORK_DIR, TMP_DIR );
193        parseProperty( WORK_DIR, nullValue );
194        sanitizePath( WORK_DIR );
195        
196        // Set a few more default properties, for easy setup
197        m_props.setProperty( STORAGE_DIR, m_props.getProperty( PAGE_DIR ) );
198        m_props.setProperty( PageManager.PROP_PAGEPROVIDER, "VersioningFileProvider" );
199    }
200    
201    public void saveProperties() {
202        final ResourceBundle rb = ResourceBundle.getBundle( InternationalizationManager.CORE_BUNDLE, m_session.getLocale() );
203        // Write the file back to disk
204        try {
205            try( final OutputStream out = Files.newOutputStream( m_propertyFile.toPath() ) ) {
206                m_props.store( out, null );
207            }
208            m_session.addMessage( INSTALL_INFO, MessageFormat.format(rb.getString("install.installer.props.saved"), m_propertyFile) );
209        } catch( final IOException e ) {
210            final Object[] args = { e.getMessage(), m_props.toString() };
211            m_session.addMessage( INSTALL_ERROR, MessageFormat.format( rb.getString( "install.installer.props.notsaved" ), args ) );
212        }
213    }
214    
215    public boolean validateProperties() {
216        final ResourceBundle rb = ResourceBundle.getBundle( InternationalizationManager.CORE_BUNDLE, m_session.getLocale() );
217        m_session.clearMessages( INSTALL_ERROR );
218        parseProperties();
219        validateNotNull( PAGE_DIR, rb.getString( "install.installer.validate.pagedir" ) );
220        validateNotNull( APP_NAME, rb.getString( "install.installer.validate.appname" ) );
221        validateNotNull( WORK_DIR, rb.getString( "install.installer.validate.workdir" ) );
222
223        if ( m_session.getMessages( INSTALL_ERROR ).length == 0 ) {
224            m_validated = true;
225        }
226        return m_validated;
227    }
228        
229    /**
230     * Sets a property based on the value of an HTTP request parameter. If the parameter is not found, a default value is used instead.
231     *
232     * @param param the parameter containing the value we will extract
233     * @param defaultValue the default to use if the parameter was not passed in the request
234     */
235    private void parseProperty( final String param, final String defaultValue ) {
236        String value = m_request.getParameter( param );
237        if( value == null ) {
238            value = defaultValue;
239        }
240        m_props.put( param, value );
241    }
242    
243    /**
244     * Simply sanitizes any path which contains backslashes (sometimes Windows users may have them) by expanding them to double-backslashes
245     *
246     * @param key the key of the property to sanitize
247     */
248    private void sanitizePath( final String key ) {
249        String s = m_props.getProperty( key );
250        s = TextUtil.replaceString(s, "\\", "\\\\" );
251        s = s.trim();
252        m_props.put( key, s );
253    }
254    
255    private void validateNotNull( final String key, final String message ) {
256        final String value = m_props.getProperty( key );
257        if ( value == null || value.isEmpty() ) {
258            m_session.addMessage( INSTALL_ERROR, message );
259        }
260    }
261    
262}