    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}