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