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 jakarta.servlet.ServletConfig;
039import jakarta.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            LOG.debug(e.getMessage(), e);
108            return false;
109        }
110    }
111    
112    /**
113     * Creates an administrative user and returns the new password. If the admin user exists, the password will be <code>null</code>.
114     *
115     * @return the password
116     */
117    public String createAdministrator() throws WikiSecurityException {
118        if ( !m_validated ) {
119            throw new WikiSecurityException( "Cannot create administrator because one or more of the installation settings are invalid." );
120        }
121        
122        if ( adminExists() ) {
123            return null;
124        }
125        
126        // See if the admin user exists already
127        final UserManager userMgr = m_engine.getManager( UserManager.class );
128        final UserDatabase userDb = userMgr.getUserDatabase();
129        String password = null;
130        
131        try {
132            userDb.findByLoginName( ADMIN_ID );
133        } catch( final NoSuchPrincipalException e ) {
134            // Create a random 12-character password
135            password = TextUtil.generateRandomPassword();
136            final UserProfile profile = userDb.newProfile();
137            profile.setLoginName( ADMIN_ID );
138            profile.setFullname( ADMIN_NAME );
139            profile.setPassword( password );
140            userDb.save( profile );
141        }
142        
143        // Create a new admin group
144        final GroupManager groupMgr = m_engine.getManager( GroupManager.class );
145        Group group;
146        try {
147            group = groupMgr.getGroup( ADMIN_GROUP );
148            group.add( new WikiPrincipal( ADMIN_NAME ) );
149        } catch( final NoSuchPrincipalException e ) {
150            group = groupMgr.parseGroup( ADMIN_GROUP, ADMIN_NAME, true );
151        }
152        groupMgr.setGroup( m_session, group );
153        
154        return password;
155    }
156    
157    /**
158     * Returns the properties as a "key=value" string separated by newlines
159     * @return the string
160     */
161    public String getPropertiesList() {
162        final Set< String > keys = m_props.stringPropertyNames();
163        return keys.stream().map( key -> key + " = " + m_props.getProperty( key ) + "\n" ).collect( Collectors.joining() );
164    }
165
166    public String getPropertiesPath() {
167        return m_propertyFile.getAbsolutePath();
168    }
169
170    /**
171     * Returns a property from the Engine's properties.
172     * @param key the property key
173     * @return the property value
174     */
175    public String getProperty( final String key ) {
176        return m_props.getProperty( key );
177    }
178    
179    public void parseProperties () {
180        final ResourceBundle rb = ResourceBundle.getBundle( InternationalizationManager.CORE_BUNDLE, m_session.getLocale() );
181        m_validated = false;
182
183        // Get application name
184        String nullValue = m_props.getProperty( APP_NAME, rb.getString( "install.installer.default.appname" ) );
185        parseProperty( APP_NAME, nullValue );
186
187        // Get work directory
188        nullValue = m_props.getProperty( WORK_DIR, TMP_DIR );
189        parseProperty( WORK_DIR, nullValue );
190
191        // Get page directory
192        nullValue = m_props.getProperty( PAGE_DIR, m_props.getProperty( WORK_DIR, TMP_DIR ) + File.separatorChar + "data" );
193        parseProperty( PAGE_DIR, nullValue );
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            LOG.warn("save properties failed", e);
210            final Object[] args = {  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        // sanitize pages, attachments and work directories
220        sanitizePath( PAGE_DIR );
221        sanitizePath( STORAGE_DIR );
222        sanitizePath( WORK_DIR );
223        validateNotNull( PAGE_DIR, rb.getString( "install.installer.validate.pagedir" ) );
224        validateNotNull( APP_NAME, rb.getString( "install.installer.validate.appname" ) );
225        validateNotNull( WORK_DIR, rb.getString( "install.installer.validate.workdir" ) );
226
227        if( m_session.getMessages( INSTALL_ERROR ).length == 0 ) {
228            m_validated = true;
229        }
230        return m_validated;
231    }
232        
233    /**
234     * Sets a property based on the value of an HTTP request parameter. If the parameter is not found, a default value is used instead.
235     *
236     * @param param the parameter containing the value we will extract
237     * @param defaultValue the default to use if the parameter was not passed in the request
238     */
239    private void parseProperty( final String param, final String defaultValue ) {
240        String value = m_request.getParameter( param );
241        if( value == null ) {
242            value = defaultValue;
243        }
244        m_props.put( param, value );
245    }
246    
247    /**
248     * Simply sanitizes any path which contains backslashes (sometimes Windows users may have them) by expanding them to double-backslashes
249     *
250     * @param key the key of the property to sanitize
251     */
252    private void sanitizePath( final String key ) {
253        String s = m_props.getProperty( key );
254        s = TextUtil.replaceString(s, "\\", "\\\\" );
255        s = s.trim();
256        m_props.put( key, s );
257    }
258
259    public void restoreUserValues() {
260        desanitizePath( PAGE_DIR );
261        desanitizePath( STORAGE_DIR );
262        desanitizePath( WORK_DIR );
263    }
264
265    /**
266     * Simply removes sanitizations so values can be shown back to the user as they were entered
267     *
268     * @param key the key of the property to sanitize
269     */
270    private void desanitizePath( final String key ) {
271        String s = m_props.getProperty( key );
272        s = TextUtil.replaceString(s, "\\\\", "\\" );
273        s = s.trim();
274        m_props.put( key, s );
275    }
276    
277    private void validateNotNull( final String key, final String message ) {
278        final String value = m_props.getProperty( key );
279        if ( value == null || value.isEmpty() ) {
280            m_session.addMessage( INSTALL_ERROR, message );
281        }
282    }
283    
284}