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; 048 049/** 050 * Manages JSPWiki installation on behalf of <code>admin/Install.jsp</code>. The contents of this class were previously part of 051 * <code>Install.jsp</code>. 052 * 053 * @since 2.4.20 054 */ 055public class Installer { 056 057 public static final String ADMIN_ID = "admin"; 058 public static final String ADMIN_NAME = "Administrator"; 059 public static final String INSTALL_INFO = "Installer.Info"; 060 public static final String INSTALL_ERROR = "Installer.Error"; 061 public static final String INSTALL_WARNING = "Installer.Warning"; 062 public static final String APP_NAME = Engine.PROP_APPNAME; 063 public static final String STORAGE_DIR = AttachmentProvider.PROP_STORAGEDIR; 064 public static final String PAGE_DIR = FileSystemProvider.PROP_PAGEDIR; 065 public static final String WORK_DIR = Engine.PROP_WORKDIR; 066 public static final String ADMIN_GROUP = "Admin"; 067 public static final String PROPFILENAME = "jspwiki-custom.properties" ; 068 public static final String TMP_DIR = System.getProperty("java.io.tmpdir"); 069 private final Session m_session; 070 private final File m_propertyFile; 071 private final Properties m_props; 072 private final Engine m_engine; 073 private final HttpServletRequest m_request; 074 private boolean m_validated; 075 076 public Installer( final HttpServletRequest request, final ServletConfig config ) { 077 // Get wiki session for this user 078 m_engine = Wiki.engine().find( config ); 079 m_session = Wiki.session().find( m_engine, request ); 080 081 // Get the file for properties 082 m_propertyFile = new File(TMP_DIR, PROPFILENAME); 083 m_props = new Properties(); 084 085 // Stash the request 086 m_request = request; 087 m_validated = false; 088 } 089 090 /** 091 * Returns <code>true</code> if the administrative user had been created previously. 092 * 093 * @return the result 094 */ 095 public boolean adminExists() { 096 // See if the admin user exists already 097 final UserManager userMgr = m_engine.getManager( UserManager.class ); 098 final UserDatabase userDb = userMgr.getUserDatabase(); 099 try { 100 userDb.findByLoginName( ADMIN_ID ); 101 return true; 102 } catch ( final NoSuchPrincipalException e ) { 103 return false; 104 } 105 } 106 107 /** 108 * Creates an administrative user and returns the new password. If the admin user exists, the password will be <code>null</code>. 109 * 110 * @return the password 111 */ 112 public String createAdministrator() throws WikiSecurityException { 113 if ( !m_validated ) { 114 throw new WikiSecurityException( "Cannot create administrator because one or more of the installation settings are invalid." ); 115 } 116 117 if ( adminExists() ) { 118 return null; 119 } 120 121 // See if the admin user exists already 122 final UserManager userMgr = m_engine.getManager( UserManager.class ); 123 final UserDatabase userDb = userMgr.getUserDatabase(); 124 String password = null; 125 126 try { 127 userDb.findByLoginName( ADMIN_ID ); 128 } catch( final NoSuchPrincipalException e ) { 129 // Create a random 12-character password 130 password = TextUtil.generateRandomPassword(); 131 final UserProfile profile = userDb.newProfile(); 132 profile.setLoginName( ADMIN_ID ); 133 profile.setFullname( ADMIN_NAME ); 134 profile.setPassword( password ); 135 userDb.save( profile ); 136 } 137 138 // Create a new admin group 139 final GroupManager groupMgr = m_engine.getManager( GroupManager.class ); 140 Group group; 141 try { 142 group = groupMgr.getGroup( ADMIN_GROUP ); 143 group.add( new WikiPrincipal( ADMIN_NAME ) ); 144 } catch( final NoSuchPrincipalException e ) { 145 group = groupMgr.parseGroup( ADMIN_GROUP, ADMIN_NAME, true ); 146 } 147 groupMgr.setGroup( m_session, group ); 148 149 return password; 150 } 151 152 /** 153 * Returns the properties as a "key=value" string separated by newlines 154 * @return the string 155 */ 156 public String getPropertiesList() { 157 final StringBuilder result = new StringBuilder(); 158 final Set< String > keys = m_props.stringPropertyNames(); 159 for( final String key : keys ) { 160 result.append(key ).append( " = " ).append( m_props.getProperty( key ) ).append( "\n" ); 161 } 162 return result.toString(); 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}