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 String result;
160        final Set< String > keys = m_props.stringPropertyNames();
161        result = keys.stream().map(key -> key + " = " + m_props.getProperty(key) + "\n").collect(Collectors.joining());
162        return result;
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}