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