    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}