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 java.io.File;
022import java.io.FileOutputStream;
023import java.io.IOException;
024import java.io.OutputStream;
025import java.text.MessageFormat;
026import java.util.Properties;
027import java.util.ResourceBundle;
028import java.util.Set;
029
030import javax.servlet.ServletConfig;
031import javax.servlet.http.HttpServletRequest;
032
033import org.apache.wiki.PageManager;
034import org.apache.wiki.WikiEngine;
035import org.apache.wiki.WikiSession;
036import org.apache.wiki.auth.NoSuchPrincipalException;
037import org.apache.wiki.auth.UserManager;
038import org.apache.wiki.auth.WikiPrincipal;
039import org.apache.wiki.auth.WikiSecurityException;
040import org.apache.wiki.auth.authorize.Group;
041import org.apache.wiki.auth.authorize.GroupManager;
042import org.apache.wiki.auth.user.UserDatabase;
043import org.apache.wiki.auth.user.UserProfile;
044import org.apache.wiki.i18n.InternationalizationManager;
045import org.apache.wiki.providers.BasicAttachmentProvider;
046import org.apache.wiki.providers.FileSystemProvider;
047import org.apache.wiki.util.TextUtil;
048
049/**
050 * Manages JSPWiki installation on behalf of <code>admin/Install.jsp</code>.
051 * The contents of this class were previously part of <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 = WikiEngine.PROP_APPNAME;
063    public static final String BASE_URL = WikiEngine.PROP_BASEURL;
064    public static final String STORAGE_DIR = BasicAttachmentProvider.PROP_STORAGEDIR;
065    public static final String LOG_FILE = "log4j.appender.FileLog.File";
066    public static final String PAGE_DIR = FileSystemProvider.PROP_PAGEDIR;
067    public static final String WORK_DIR = WikiEngine.PROP_WORKDIR;
068    public static final String ADMIN_GROUP = "Admin";
069    public static final String PROPFILENAME = "jspwiki-custom.properties" ;
070    public static final String TMP_DIR = System.getProperty("java.io.tmpdir");
071    private final WikiSession m_session;
072    private final File m_propertyFile;
073    private final Properties m_props;
074    private final WikiEngine m_engine;
075    private HttpServletRequest m_request;
076    private boolean m_validated;
077    
078    public Installer( HttpServletRequest request, ServletConfig config ) throws IOException {
079        // Get wiki session for this user
080        m_engine = WikiEngine.getInstance( config );
081        m_session = WikiSession.getWikiSession( m_engine, request );
082        
083        // Get the file for properties
084        m_propertyFile = new File(TMP_DIR, PROPFILENAME);
085        m_props = new Properties();
086        
087        // Stash the request
088        m_request = request;
089        m_validated = false;
090    }
091    
092    /**
093     * Returns <code>true</code> if the administrative user had
094     * been created previously.
095     * @return the result
096     */
097    public boolean adminExists()
098    {
099        // See if the admin user exists already
100        UserManager userMgr = m_engine.getUserManager();
101        UserDatabase userDb = userMgr.getUserDatabase();
102        
103        try
104        {
105            userDb.findByLoginName( ADMIN_ID );
106            return true;
107        }
108        catch ( NoSuchPrincipalException e )
109        {
110            return false;
111        }
112    }
113    
114    /**
115     * Creates an administrative user and returns the new password.
116     * If the admin user exists, the password will be <code>null</code>.
117     * @return the password
118     * @throws WikiSecurityException
119     */
120    public String createAdministrator() throws WikiSecurityException
121    {
122        if ( !m_validated )
123        {
124            throw new WikiSecurityException( "Cannot create administrator because one or more of the installation settings are invalid." );
125        }
126        
127        if ( adminExists() )
128        {
129            return null;
130        }
131        
132        // See if the admin user exists already
133        UserManager userMgr = m_engine.getUserManager();
134        UserDatabase userDb = userMgr.getUserDatabase();
135        String password = null;
136        
137        try
138        {
139            userDb.findByLoginName( ADMIN_ID );
140        }
141        catch ( NoSuchPrincipalException e )
142        {
143            // Create a random 12-character password
144            password = TextUtil.generateRandomPassword();
145            UserProfile profile = userDb.newProfile();
146            profile.setLoginName( ADMIN_ID );
147            profile.setFullname( ADMIN_NAME );
148            profile.setPassword( password );
149            userDb.save( profile );
150        }
151        
152        // Create a new admin group
153        GroupManager groupMgr = m_engine.getGroupManager();
154        Group group = null;
155        try
156        {
157            group = groupMgr.getGroup( ADMIN_GROUP );
158            group.add( new WikiPrincipal( ADMIN_NAME ) );
159        }
160        catch ( NoSuchPrincipalException e )
161        {
162            group = groupMgr.parseGroup( ADMIN_GROUP, ADMIN_NAME, true );
163        }
164        groupMgr.setGroup( m_session, group );
165        
166        return password;
167    }
168    
169    /**
170     * Returns the properties as a "key=value" string separated by newlines
171     * @return the string
172     */
173    public String getPropertiesList()
174    {
175        StringBuilder result = new StringBuilder();
176        Set<String> keys = m_props.stringPropertyNames();
177        for (String key:keys) {
178            result.append(key + " = " + m_props.getProperty(key) + "\n");
179        }
180        return result.toString();
181    }
182
183    public String getPropertiesPath()
184    {
185        return m_propertyFile.getAbsolutePath();
186    }
187
188    /**
189     * Returns a property from the WikiEngine's properties.
190     * @param key the property key
191     * @return the property value
192     */
193    public String getProperty( String key )
194    {
195        return m_props.getProperty( key );
196    }
197    
198    public void parseProperties () throws Exception
199    {
200        ResourceBundle rb = ResourceBundle.getBundle( InternationalizationManager.CORE_BUNDLE,
201                                                      m_session.getLocale() );
202        m_validated = false;
203        
204
205        // Get application name
206        String nullValue = m_props.getProperty( APP_NAME, rb.getString( "install.installer.default.appname" ) );
207        parseProperty( APP_NAME, nullValue );
208        
209        // Get/sanitize base URL
210        nullValue = m_request.getRequestURL().toString();
211        nullValue = nullValue.substring( 0, nullValue.lastIndexOf('/') )+"/";
212        parseProperty( BASE_URL, nullValue );
213        sanitizeURL( BASE_URL );
214        
215        // Get/sanitize page directory
216        nullValue = m_props.getProperty( PAGE_DIR, rb.getString( "install.installer.default.pagedir" ) );
217        parseProperty( PAGE_DIR, nullValue );
218        sanitizePath( PAGE_DIR );
219        
220        // Get/sanitize log directory
221        nullValue = m_props.getProperty( LOG_FILE, TMP_DIR + File.separator + "jspwiki.log" );
222        parseProperty( LOG_FILE, nullValue );
223        sanitizePath( LOG_FILE );
224        
225        // Get/sanitize work directory
226        nullValue = m_props.getProperty( WORK_DIR, TMP_DIR );
227        parseProperty( WORK_DIR, nullValue );
228        sanitizePath( WORK_DIR );
229        
230        // Set a few more default properties, for easy setup
231        m_props.setProperty( STORAGE_DIR, m_props.getProperty( PAGE_DIR ) );
232        m_props.setProperty( PageManager.PROP_PAGEPROVIDER, "VersioningFileProvider" );
233    }
234    
235    public void saveProperties()
236    {
237        ResourceBundle rb = ResourceBundle.getBundle( InternationalizationManager.CORE_BUNDLE, m_session.getLocale() );
238        // Write the file back to disk
239        try
240        {
241            OutputStream out = null;
242            try
243            {
244                out = new FileOutputStream( m_propertyFile );
245                m_props.store( out, null );
246            }
247            finally
248            {
249                if ( out != null )
250                {
251                    out.close();
252                }
253            }
254            m_session.addMessage( INSTALL_INFO, MessageFormat.format(rb.getString("install.installer.props.saved"), m_propertyFile) );
255        }
256        catch( IOException e )
257        {
258            Object[] args = { e.getMessage(), m_props.toString() };
259            m_session.addMessage( INSTALL_ERROR, MessageFormat.format( rb.getString( "install.installer.props.notsaved" ), args ) );
260        }
261    }
262    
263    public boolean validateProperties() throws Exception
264    {
265        ResourceBundle rb = ResourceBundle.getBundle( InternationalizationManager.CORE_BUNDLE, m_session.getLocale() );
266        m_session.clearMessages( INSTALL_ERROR );
267        parseProperties();
268        validateNotNull( BASE_URL, rb.getString( "install.installer.validate.baseurl" ) );
269        validateNotNull( PAGE_DIR, rb.getString( "install.installer.validate.pagedir" ) );
270        validateNotNull( APP_NAME, rb.getString( "install.installer.validate.appname" ) );
271        validateNotNull( WORK_DIR, rb.getString( "install.installer.validate.workdir" ) );
272        validateNotNull( LOG_FILE, rb.getString( "install.installer.validate.logfile" ) );
273        
274        if ( m_session.getMessages( INSTALL_ERROR ).length == 0 )
275        {
276            m_validated = true;
277        }
278        return m_validated;
279    }
280        
281    /**
282     * Sets a property based on the value of an HTTP request parameter.
283     * If the parameter is not found, a default value is used instead.
284     * @param param the parameter containing the value we will extract
285     * @param defaultValue the default to use if the parameter was not passed
286     * in the request
287     */
288    private void parseProperty( String param, String defaultValue )
289    {
290        String value = m_request.getParameter( param );
291        if ( value == null )
292        {
293            value = defaultValue;
294        }
295        m_props.put(param, value);
296    }
297    
298    /**
299     * Simply sanitizes any path which contains backslashes (sometimes Windows
300     * users may have them) by expanding them to double-backslashes
301     * @param key the key of the property to sanitize
302     */
303    private void sanitizePath( String key )
304    {
305        String s = m_props.getProperty( key );
306        s = TextUtil.replaceString(s, "\\", "\\\\" );
307        s = s.trim();
308        m_props.put( key, s );
309    }
310    
311    /**
312     * Simply sanitizes any URL which contains backslashes (sometimes Windows
313     * users may have them)
314     * @param key the key of the property to sanitize
315     */
316    private void sanitizeURL( String key )
317    {
318        String s = m_props.getProperty( key );
319        s = TextUtil.replaceString( s, "\\", "/" );
320        s = s.trim();
321        if (!s.endsWith("/"))
322        {
323            s = s + "/";
324        }
325        m_props.put( key, s );
326    }
327
328    private void validateNotNull( String key, String message )
329    {
330        String value = m_props.getProperty( key );
331        if ( value == null || value.length() == 0 )
332        {
333            m_session.addMessage( INSTALL_ERROR, message );
334        }
335    }
336    
337}