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