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 String TMP_DIR;
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        TMP_DIR = m_engine.getWikiProperties().getProperty( "jspwiki.workDir" );
090    }
091    
092    /**
093     * Returns <code>true</code> if the administrative user had been created previously.
094     *
095     * @return the result
096     */
097    public boolean adminExists() {
098        // See if the admin user exists already
099        final UserManager userMgr = m_engine.getManager( UserManager.class );
100        final UserDatabase userDb = userMgr.getUserDatabase();
101        try {
102            userDb.findByLoginName( ADMIN_ID );
103            return true;
104        } catch ( final NoSuchPrincipalException e ) {
105            return false;
106        }
107    }
108    
109    /**
110     * Creates an administrative user and returns the new password. If the admin user exists, the password will be <code>null</code>.
111     *
112     * @return the password
113     */
114    public String createAdministrator() throws WikiSecurityException {
115        if ( !m_validated ) {
116            throw new WikiSecurityException( "Cannot create administrator because one or more of the installation settings are invalid." );
117        }
118        
119        if ( adminExists() ) {
120            return null;
121        }
122        
123        // See if the admin user exists already
124        final UserManager userMgr = m_engine.getManager( UserManager.class );
125        final UserDatabase userDb = userMgr.getUserDatabase();
126        String password = null;
127        
128        try {
129            userDb.findByLoginName( ADMIN_ID );
130        } catch( final NoSuchPrincipalException e ) {
131            // Create a random 12-character password
132            password = TextUtil.generateRandomPassword();
133            final UserProfile profile = userDb.newProfile();
134            profile.setLoginName( ADMIN_ID );
135            profile.setFullname( ADMIN_NAME );
136            profile.setPassword( password );
137            userDb.save( profile );
138        }
139        
140        // Create a new admin group
141        final GroupManager groupMgr = m_engine.getManager( GroupManager.class );
142        Group group;
143        try {
144            group = groupMgr.getGroup( ADMIN_GROUP );
145            group.add( new WikiPrincipal( ADMIN_NAME ) );
146        } catch( final NoSuchPrincipalException e ) {
147            group = groupMgr.parseGroup( ADMIN_GROUP, ADMIN_NAME, true );
148        }
149        groupMgr.setGroup( m_session, group );
150        
151        return password;
152    }
153    
154    /**
155     * Returns the properties as a "key=value" string separated by newlines
156     * @return the string
157     */
158    public String getPropertiesList() {
159        final Set< String > keys = m_props.stringPropertyNames();
160        return keys.stream().map( key -> key + " = " + m_props.getProperty( key ) + "\n" ).collect( Collectors.joining() );
161    }
162
163    public String getPropertiesPath() {
164        return m_propertyFile.getAbsolutePath();
165    }
166
167    /**
168     * Returns a property from the Engine's properties.
169     * @param key the property key
170     * @return the property value
171     */
172    public String getProperty( final String key ) {
173        return m_props.getProperty( key );
174    }
175    
176    public void parseProperties () {
177        final ResourceBundle rb = ResourceBundle.getBundle( InternationalizationManager.CORE_BUNDLE, m_session.getLocale() );
178        m_validated = false;
179
180        // Get application name
181        String nullValue = m_props.getProperty( APP_NAME, rb.getString( "install.installer.default.appname" ) );
182        parseProperty( APP_NAME, nullValue );
183
184        // Get work directory
185        nullValue = m_props.getProperty( WORK_DIR, TMP_DIR );
186        parseProperty( WORK_DIR, nullValue );
187
188        // Get page directory
189        nullValue = m_props.getProperty( PAGE_DIR, m_props.getProperty( WORK_DIR, TMP_DIR ) + File.separatorChar + "data" );
190        parseProperty( PAGE_DIR, nullValue );
191
192        // Set a few more default properties, for easy setup
193        m_props.setProperty( STORAGE_DIR, m_props.getProperty( PAGE_DIR ) );
194        m_props.setProperty( PageManager.PROP_PAGEPROVIDER, "VersioningFileProvider" );
195    }
196    
197    public void saveProperties() {
198        final ResourceBundle rb = ResourceBundle.getBundle( InternationalizationManager.CORE_BUNDLE, m_session.getLocale() );
199        // Write the file back to disk
200        try {
201            try( final OutputStream out = Files.newOutputStream( m_propertyFile.toPath() ) ) {
202                m_props.store( out, null );
203            }
204            m_session.addMessage( INSTALL_INFO, MessageFormat.format(rb.getString("install.installer.props.saved"), m_propertyFile) );
205        } catch( final IOException e ) {
206            final Object[] args = { e.getMessage(), m_props.toString() };
207            m_session.addMessage( INSTALL_ERROR, MessageFormat.format( rb.getString( "install.installer.props.notsaved" ), args ) );
208        }
209    }
210    
211    public boolean validateProperties() {
212        final ResourceBundle rb = ResourceBundle.getBundle( InternationalizationManager.CORE_BUNDLE, m_session.getLocale() );
213        m_session.clearMessages( INSTALL_ERROR );
214        parseProperties();
215        // sanitize pages, attachments and work directories
216        sanitizePath( PAGE_DIR );
217        sanitizePath( STORAGE_DIR );
218        sanitizePath( WORK_DIR );
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    public void restoreUserValues() {
256        desanitizePath( PAGE_DIR );
257        desanitizePath( STORAGE_DIR );
258        desanitizePath( WORK_DIR );
259    }
260
261    /**
262     * Simply removes sanitizations so values can be shown back to the user as they were entered
263     *
264     * @param key the key of the property to sanitize
265     */
266    private void desanitizePath( final String key ) {
267        String s = m_props.getProperty( key );
268        s = TextUtil.replaceString(s, "\\\\", "\\" );
269        s = s.trim();
270        m_props.put( key, s );
271    }
272    
273    private void validateNotNull( final String key, final String message ) {
274        final String value = m_props.getProperty( key );
275        if ( value == null || value.isEmpty() ) {
276            m_session.addMessage( INSTALL_ERROR, message );
277        }
278    }
279    
280}