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}