PluginJspBean.java

/*
 * Copyright (c) 2002-2022, City of Paris
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 *  1. Redistributions of source code must retain the above copyright notice
 *     and the following disclaimer.
 *
 *  2. Redistributions in binary form must reproduce the above copyright notice
 *     and the following disclaimer in the documentation and/or other materials
 *     provided with the distribution.
 *
 *  3. Neither the name of 'Mairie de Paris' nor 'Lutece' nor the names of its
 *     contributors may be used to endorse or promote products derived from
 *     this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 *
 * License 1.0
 */
package fr.paris.lutece.portal.web.system;

import fr.paris.lutece.portal.business.portlet.PortletType;
import fr.paris.lutece.portal.business.portlet.PortletTypeHome;
import fr.paris.lutece.portal.service.admin.AccessDeniedException;
import fr.paris.lutece.portal.service.admin.AdminUserService;
import fr.paris.lutece.portal.service.database.AppConnectionService;
import fr.paris.lutece.portal.service.i18n.I18nService;
import fr.paris.lutece.portal.service.init.AppInfo;
import fr.paris.lutece.portal.service.message.AdminMessage;
import fr.paris.lutece.portal.service.message.AdminMessageService;
import fr.paris.lutece.portal.service.plugin.Plugin;
import fr.paris.lutece.portal.service.plugin.PluginService;
import fr.paris.lutece.portal.service.security.SecurityTokenService;
import fr.paris.lutece.portal.service.template.AppTemplateService;
import fr.paris.lutece.portal.service.util.AppLogService;
import fr.paris.lutece.portal.web.admin.AdminFeaturesPageJspBean;
import fr.paris.lutece.util.ReferenceList;
import fr.paris.lutece.util.html.HtmlTemplate;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;

import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;

/**
 * This class provides the user interface to manage the lutece plugins (install, enable, disable)
 */
public class PluginJspBean extends AdminFeaturesPageJspBean
{
    // //////////////////////////////////////////////////////////////////////////////
    // Constants
    public static final String RIGHT_MANAGE_PLUGINS = "CORE_PLUGINS_MANAGEMENT";
    private static final long serialVersionUID = -9058113426951331118L;
    private static final String TEMPLATE_MANAGE_PLUGINS = "admin/system/manage_plugins.html";
    private static final String MARK_PLUGINS_LIST = "plugins_list";
    private static final String MARK_CORE = "core";
    private static final String MARK_POOLS_LIST = "pools_list";
    private static final String MARK_FILTER_LIST = "filter_list";
    private static final String MARK_CURRENT_FILTER = "current_filter";
    private static final String PROPERTY_PLUGIN_MESSAGE = "portal.system.message.confirmDisable";
    private static final String PROPERTY_PLUGIN_PORTLET_EXIST_MESSAGE = "portal.system.message.portletExist";
    private static final String PROPERTY_PLUGIN_NO_CORE_COMPATIBILITY_MESSAGE = "portal.system.message.noCoreCompatibility";
    private static final String PROPERTY_PLUGIN_INSTALL_ERROR = "portal.system.message.installError";
    private static final String PARAM_PLUGIN_NAME = "plugin_name";
    private static final String PARAM_PLUGIN_TYPE = "plugin_type";
    private static final String PARAM_DB_POOL_NAME = "db_pool_name";
    private static final String PARAM_PLUGIN_TYPE_ALL = "all";
    private static final String PARAM_PLUGIN_TYPE_PORTLET = "portlet";
    private static final String PARAM_PLUGIN_TYPE_APPLICATION = "application";
    private static final String PARAM_PLUGIN_TYPE_FEATURE = "feature";
    private static final String PARAM_PLUGIN_TYPE_INSERTSERVICE = "insertservice";
    private static final String PARAM_PLUGIN_TYPE_CONTENTSERVICE = "contentservice";
    private static final String PROPERTY_PLUGIN_TYPE_NAME_ALL = "portal.system.pluginType.name.all";
    private static final String PROPERTY_PLUGIN_TYPE_NAME_APPLICATION = "portal.system.pluginType.name.application";
    private static final String PROPERTY_PLUGIN_TYPE_NAME_PORTLET = "portal.system.pluginType.name.portlet";
    private static final String PROPERTY_PLUGIN_TYPE_NAME_FEATURE = "portal.system.pluginType.name.feature";
    private static final String PROPERTY_PLUGIN_TYPE_NAME_INSERTSERVICE = "portal.system.pluginType.name.insertService";
    private static final String PROPERTY_PLUGIN_TYPE_NAME_CONTENTSERVICE = "portal.system.pluginType.name.contentService";
    private static final String TEMPLATE_PLUGIN_DETAILS = "/admin/system/view_plugin.html";
    private static final String JSP_UNINSTALL_PLUGIN = "jsp/admin/system/DoUninstallPlugin.jsp";

    /**
     * Returns the plugins management page
     *
     * @param request
     *            The Http request
     * @return Html page
     */
    public String getManagePlugins( HttpServletRequest request )
    {
        Locale locale = AdminUserService.getLocale( request );
        String strPluginTypeFilter = request.getParameter( PARAM_PLUGIN_TYPE );
        Collection<Plugin> listPlugins = PluginService.getPluginList( );
        HashMap<String, Object> model = new HashMap<>( );
        model.put( MARK_PLUGINS_LIST, filterPluginsList( listPlugins, strPluginTypeFilter ) );
        model.put( MARK_CORE, PluginService.getCore( ) );
        model.put( MARK_POOLS_LIST, getPoolsList( ) );
        model.put( MARK_FILTER_LIST, getPluginTypeFilterList( locale ) );
        model.put( MARK_CURRENT_FILTER, ( strPluginTypeFilter != null ) ? strPluginTypeFilter : "" );
        model.put( SecurityTokenService.MARK_TOKEN, SecurityTokenService.getInstance( ).getToken( request, TEMPLATE_MANAGE_PLUGINS ) );

        HtmlTemplate template = AppTemplateService.getTemplate( TEMPLATE_MANAGE_PLUGINS, locale, model );

        return getAdminPage( template.getHtml( ) );
    }

    /**
     * Install a plugin
     *
     * @param request
     *            The Http request
     * @param context
     *            The servlet context
     * @return the url of the page containing a log essage
     * @throws AccessDeniedException
     *             if the security token is invalid
     */
    public String doInstallPlugin( HttpServletRequest request, ServletContext context ) throws AccessDeniedException
    {
        String strPluginName = request.getParameter( PARAM_PLUGIN_NAME );
        Plugin plugin = PluginService.getPlugin( strPluginName );

        if ( !verifyCoreCompatibility( plugin ) )
        {
            Object [ ] args = {
                    plugin.getMinCoreVersion( ), plugin.getMaxCoreVersion( )
            };

            return AdminMessageService.getMessageUrl( request, PROPERTY_PLUGIN_NO_CORE_COMPATIBILITY_MESSAGE, args, AdminMessage.TYPE_STOP );

        }
        if ( !SecurityTokenService.getInstance( ).validate( request, TEMPLATE_MANAGE_PLUGINS ) )
        {
            throw new AccessDeniedException( ERROR_INVALID_TOKEN );
        }
        try
        {
            plugin.install( );
        }
        catch( Exception e )
        {
            AppLogService.error( e.getMessage( ), e );

            return AdminMessageService.getMessageUrl( request, PROPERTY_PLUGIN_INSTALL_ERROR, AdminMessage.TYPE_STOP );
        }

        return getHomeUrl( request );
    }

    /**
     * Uninstall a plugin
     *
     * @param request
     *            The Http request
     * @param context
     *            The servlet context
     * @return the url of the page containing a log essage
     * @throws AccessDeniedException
     *             if the security token is invalid
     */
    public String doUninstallPlugin( HttpServletRequest request, ServletContext context ) throws AccessDeniedException
    {
        if ( !SecurityTokenService.getInstance( ).validate( request, JSP_UNINSTALL_PLUGIN ) )
        {
            throw new AccessDeniedException( ERROR_INVALID_TOKEN );
        }
        try
        {
            String strPluginName = request.getParameter( PARAM_PLUGIN_NAME );
            Plugin plugin = PluginService.getPlugin( strPluginName );
            plugin.uninstall( );
        }
        catch( Exception e )
        {
            AppLogService.error( e.getMessage( ), e );
        }

        return getHomeUrl( request );
    }

    /**
     * Returns the page of confirmation for uninstalling a plugin
     *
     * @param request
     *            The Http Request
     * @return the HTML page
     */
    public String getConfirmUninstallPlugin( HttpServletRequest request )
    {
        String strPluginName = request.getParameter( PARAM_PLUGIN_NAME );
        Plugin plugin = PluginService.getPlugin( strPluginName );
        Collection<PortletType> listPortletTypes = plugin.getPortletTypes( );
        String strMessageKey = PROPERTY_PLUGIN_MESSAGE;
        Map<String, String> parameters = new HashMap<>( );
        parameters.put( PARAM_PLUGIN_NAME, strPluginName );
        parameters.put( SecurityTokenService.PARAMETER_TOKEN, SecurityTokenService.getInstance( ).getToken( request, JSP_UNINSTALL_PLUGIN ) );
        String strAdminMessageUrl = AdminMessageService.getMessageUrl( request, strMessageKey, JSP_UNINSTALL_PLUGIN, AdminMessage.TYPE_CONFIRMATION,
                parameters );

        for ( PortletType portletType : listPortletTypes )
        {
            String strPluginHomeClass = portletType.getHomeClass( );

            if ( ( plugin.getType( ) & Plugin.PLUGIN_TYPE_PORTLET ) != 0 && isPortletExists( strPluginHomeClass ) )
            {
                strMessageKey = PROPERTY_PLUGIN_PORTLET_EXIST_MESSAGE;
                strAdminMessageUrl = AdminMessageService.getMessageUrl( request, strMessageKey, AdminMessage.TYPE_CONFIRMATION );
            }
        }

        return strAdminMessageUrl;
    }

    /**
     * Defines the database connection pool to be used by the plugin
     * 
     * @param request
     *            The http request
     * @return the URL to redirect after this action
     * @throws AccessDeniedException
     *             if the security token is invalid
     */
    public String doModifyPluginPool( HttpServletRequest request ) throws AccessDeniedException
    {
        if ( !SecurityTokenService.getInstance( ).validate( request, TEMPLATE_MANAGE_PLUGINS ) )
        {
            throw new AccessDeniedException( ERROR_INVALID_TOKEN );
        }
        String strPluginName = request.getParameter( PARAM_PLUGIN_NAME );
        String strDbPoolName = request.getParameter( PARAM_DB_POOL_NAME );

        try
        {
            Plugin plugin = PluginService.getPlugin( strPluginName );
            plugin.updatePoolName( strDbPoolName );
        }
        catch( Exception e )
        {
            AppLogService.error( e.getMessage( ), e );
        }

        return getHomeUrl( request );
    }

    /**
     * Displays a plugin's description
     * 
     * @param request
     *            The HTTP request
     * @return The popup HTML code
     */
    public String getPluginDescription( HttpServletRequest request )
    {
        String strPluginName = request.getParameter( PARAM_PLUGIN_NAME );
        Plugin plugin;
        if ( PluginService.getCore( ).getName( ).equals( strPluginName ) )
        {
            plugin = PluginService.getCore( );
        }
        else
        {
            plugin = PluginService.getPlugin( strPluginName );
        }

        // set the locale for the feature labels
        I18nService.localizeCollection( plugin.getRights( ), getLocale( ) );
        // set the locale for the portlet types labels
        I18nService.localizeCollection( plugin.getPortletTypes( ), getLocale( ) );
        // set the locale for the link services labels
        I18nService.localizeCollection( plugin.getInsertServices( ), getLocale( ) );

        HtmlTemplate template = AppTemplateService.getTemplate( TEMPLATE_PLUGIN_DETAILS, getLocale( ), plugin );

        return getAdminPage( template.getHtml( ) );
    }

    // ////////////////////////////////////////////////////////////////////////////////////////////////////////
    // Private implementation

    /**
     * Return a filter list of plugins
     * 
     * @param listPlugins
     *            the COllection of plugins
     * @param strPluginTypeFilter
     *            The filter
     * @return list The list of plugins
     */
    private Collection<Plugin> filterPluginsList( Collection<Plugin> listPlugins, String strPluginTypeFilter )
    {
        Collection<Plugin> list = new ArrayList<>( );
        if ( strPluginTypeFilter == null )
        {
            return listPlugins;
        }

        for ( Plugin plugin : listPlugins )
        {
            boolean filter = false;
            if ( strPluginTypeFilter.equals( PARAM_PLUGIN_TYPE_APPLICATION ) )
            {
                // skip this plugin
                filter = ( plugin.getType( ) & Plugin.PLUGIN_TYPE_APPLICATION ) == 0;
            }
            else
                if ( strPluginTypeFilter.equals( PARAM_PLUGIN_TYPE_PORTLET ) )
                {
                    // skip this plugin
                    filter = ( plugin.getType( ) & Plugin.PLUGIN_TYPE_PORTLET ) == 0;
                }
                else
                    if ( strPluginTypeFilter.equals( PARAM_PLUGIN_TYPE_FEATURE ) )
                    {
                        // skip this plugin
                        filter = ( plugin.getType( ) & Plugin.PLUGIN_TYPE_FEATURE ) == 0;
                    }
                    else
                        if ( strPluginTypeFilter.equals( PARAM_PLUGIN_TYPE_INSERTSERVICE ) )
                        {
                            // skip this plugin
                            filter = ( plugin.getType( ) & Plugin.PLUGIN_TYPE_INSERTSERVICE ) == 0;
                        }
                        else
                            if ( strPluginTypeFilter.equals( PARAM_PLUGIN_TYPE_CONTENTSERVICE ) )
                            {
                                // skip this plugin
                                filter = ( plugin.getType( ) & Plugin.PLUGIN_TYPE_CONTENTSERVICE ) == 0;
                            }

            if ( !filter )
            {
                list.add( plugin );
            }
        }
        return list;
    }

    /**
     * Create a ReferenceList containing all Plugin types
     * 
     * @param locale
     *            The Locale
     * @return A ReferenceList containing all Plugin types
     */
    private ReferenceList getPluginTypeFilterList( Locale locale )
    {
        ReferenceList list = new ReferenceList( );
        list.addItem( PARAM_PLUGIN_TYPE_ALL, I18nService.getLocalizedString( PROPERTY_PLUGIN_TYPE_NAME_ALL, locale ) );
        list.addItem( PARAM_PLUGIN_TYPE_APPLICATION, I18nService.getLocalizedString( PROPERTY_PLUGIN_TYPE_NAME_APPLICATION, locale ) );
        list.addItem( PARAM_PLUGIN_TYPE_PORTLET, I18nService.getLocalizedString( PROPERTY_PLUGIN_TYPE_NAME_PORTLET, locale ) );
        list.addItem( PARAM_PLUGIN_TYPE_FEATURE, I18nService.getLocalizedString( PROPERTY_PLUGIN_TYPE_NAME_FEATURE, locale ) );
        list.addItem( PARAM_PLUGIN_TYPE_INSERTSERVICE, I18nService.getLocalizedString( PROPERTY_PLUGIN_TYPE_NAME_INSERTSERVICE, locale ) );
        list.addItem( PARAM_PLUGIN_TYPE_CONTENTSERVICE, I18nService.getLocalizedString( PROPERTY_PLUGIN_TYPE_NAME_CONTENTSERVICE, locale ) );

        return list;
    }

    /**
     * Return a list of pools
     * 
     * @return listPools the list of pools
     */
    private ReferenceList getPoolsList( )
    {
        ReferenceList listPools = new ReferenceList( );
        listPools.addItem( AppConnectionService.NO_POOL_DEFINED, " " );
        AppConnectionService.getPoolList( listPools );

        return listPools;
    }

    /**
     * Returns the status of the existence of a portlet on the site
     *
     * @param strPluginHomeClass
     *            The home class of the plugin
     * @return The existence status as a boolean
     */
    private boolean isPortletExists( String strPluginHomeClass )
    {
        String strPortletTypeId = PortletTypeHome.getPortletTypeId( strPluginHomeClass );

        return ( PortletTypeHome.getNbPortletTypeByPortlet( strPortletTypeId ) != 0 );
    }

    /**
     * Verify the core compatibility for a plugin
     *
     * @param plugin
     *            The plugin
     * @return true if compatible with the current core version
     */
    private boolean verifyCoreCompatibility( Plugin plugin )
    {
        String strCoreVersion = AppInfo.getVersion( );

        // Remove version qualifier (-SNAPSHOT, -RC-XX, ...)
        int nPos = strCoreVersion.indexOf( '-' );

        if ( nPos > 0 )
        {
            strCoreVersion = strCoreVersion.substring( 0, nPos );
        }

        String [ ] coreVersion = strCoreVersion.split( "\\." );

        String strMinCoreVersion = ( plugin.getMinCoreVersion( ) == null ) ? "" : plugin.getMinCoreVersion( );
        String strMaxCoreVersion = ( plugin.getMaxCoreVersion( ) == null ) ? "" : plugin.getMaxCoreVersion( );

        // test the min core version
        boolean bMin = ( strMinCoreVersion == null ) || strMinCoreVersion.trim( ).equals( "" );

        if ( ( strMinCoreVersion != null ) && !strMinCoreVersion.trim( ).equals( "" ) )
        {
            String [ ] minCoreVersion = strMinCoreVersion.split( "\\." );

            if ( checkCoreMinCompatibility( minCoreVersion, coreVersion ) )
            {
                AppLogService.debug( "Min core version ok : {}", plugin.getMinCoreVersion( ) );
                bMin = true;
            }
        }

        // test the max core version
        boolean bMax = ( strMaxCoreVersion == null ) || strMaxCoreVersion.trim( ).equals( "" );

        if ( ( strMaxCoreVersion != null ) && !strMaxCoreVersion.trim( ).equals( "" ) )
        {
            String [ ] maxCoreVersion = strMaxCoreVersion.split( "\\." );

            if ( checkCoreMaxCompatibility( maxCoreVersion, coreVersion ) )
            {
                AppLogService.debug( "Max core version ok : {}", plugin.getMaxCoreVersion( ) );
                bMax = true;
            }
        }

        return bMin && bMax;
    }

    /**
     * Checks the compatibility
     * 
     * @param minCoreVersion
     *            The min core version
     * @param coreVersion
     *            The current core version
     * @return true if compatible with the current core version
     */
    private boolean checkCoreMinCompatibility( String [ ] minCoreVersion, String [ ] coreVersion )
    {
        for ( int i = 0; i < Math.min( minCoreVersion.length, coreVersion.length ); ++i )
        {
            if ( ( Integer.parseInt( minCoreVersion [i] ) ) < ( Integer.parseInt( coreVersion [i] ) ) )
            {
                return true;
            }

            if ( ( Integer.parseInt( minCoreVersion [i] ) ) > ( Integer.parseInt( coreVersion [i] ) ) )
            {
                return false;
            }
        }

        return true; // inclusive
    }

    /**
     * Checks the compatibility
     * 
     * @param maxCoreVersion
     *            The max core version
     * @param coreVersion
     *            The current core version
     * @return true if compatible with the current core version
     */
    private boolean checkCoreMaxCompatibility( String [ ] maxCoreVersion, String [ ] coreVersion )
    {
        for ( int i = 0; i < Math.min( maxCoreVersion.length, coreVersion.length ); ++i )
        {
            if ( ( Integer.parseInt( maxCoreVersion [i] ) ) > ( Integer.parseInt( coreVersion [i] ) ) )
            {
                return true;
            }

            if ( ( Integer.parseInt( maxCoreVersion [i] ) ) < ( Integer.parseInt( coreVersion [i] ) ) )
            {
                return false;
            }
        }

        return false; // exclusive
    }
}