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}