WikiApp.java

/*
 * Copyright (c) 2002-2023, 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.plugins.wiki.web;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import fr.paris.lutece.plugins.avatar.service.AvatarService;
import fr.paris.lutece.plugins.wiki.business.Image;
import fr.paris.lutece.plugins.wiki.business.ImageHome;
import fr.paris.lutece.plugins.wiki.business.Topic;
import fr.paris.lutece.plugins.wiki.business.TopicHome;
import fr.paris.lutece.plugins.wiki.business.TopicVersion;
import fr.paris.lutece.plugins.wiki.business.TopicVersionHome;
import fr.paris.lutece.plugins.wiki.business.WikiContent;
import fr.paris.lutece.plugins.wiki.service.*;
import fr.paris.lutece.plugins.wiki.service.parser.LuteceHtmlParser;
import fr.paris.lutece.plugins.wiki.service.parser.SpecialChar;
import fr.paris.lutece.plugins.wiki.service.parser.WikiCreoleToMarkdown;
import fr.paris.lutece.plugins.wiki.utils.auth.WikiAnonymousUser;
import fr.paris.lutece.portal.business.page.Page;
import fr.paris.lutece.portal.service.content.XPageAppService;
import fr.paris.lutece.portal.service.datastore.DatastoreService;
import fr.paris.lutece.portal.service.i18n.I18nService;
import fr.paris.lutece.portal.service.message.SiteMessage;
import fr.paris.lutece.portal.service.message.SiteMessageException;
import fr.paris.lutece.portal.service.message.SiteMessageService;
import fr.paris.lutece.portal.service.plugin.Plugin;
import fr.paris.lutece.portal.service.plugin.PluginService;
import fr.paris.lutece.portal.service.prefs.UserPreferencesService;
import fr.paris.lutece.portal.service.search.SearchEngine;
import fr.paris.lutece.portal.service.search.SearchResult;
import fr.paris.lutece.portal.service.security.LuteceUser;
import fr.paris.lutece.portal.service.security.SecurityService;
import fr.paris.lutece.portal.service.security.UserNotSignedException;
import fr.paris.lutece.portal.service.spring.SpringContextService;
import fr.paris.lutece.portal.service.util.AppLogService;
import fr.paris.lutece.portal.service.util.AppPathService;
import fr.paris.lutece.portal.service.util.AppPropertiesService;
import fr.paris.lutece.portal.util.mvc.commons.annotations.Action;
import fr.paris.lutece.portal.util.mvc.commons.annotations.View;
import fr.paris.lutece.portal.util.mvc.xpage.MVCApplication;
import fr.paris.lutece.portal.util.mvc.xpage.annotations.Controller;
import fr.paris.lutece.portal.web.l10n.LocaleService;
import fr.paris.lutece.portal.web.resource.ExtendableResourcePluginActionManager;
import fr.paris.lutece.portal.web.upload.MultipartHttpServletRequest;
import fr.paris.lutece.portal.web.xpages.XPage;
import fr.paris.lutece.util.ReferenceItem;
import fr.paris.lutece.util.ReferenceList;
import fr.paris.lutece.util.html.Paginator;
import fr.paris.lutece.util.url.UrlItem;

import java.io.*;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import javax.servlet.http.HttpServletRequest;

import org.apache.commons.fileupload.FileItem;
import org.apache.commons.lang3.StringUtils;

/**
 * This class provides a simple implementation of an XPage
 */
@Controller( xpageName = "wiki", pageTitleProperty = "wiki.pageTitle", pagePathProperty = "wiki.pagePathLabel" )
public class WikiApp extends MVCApplication
{
    private static final String TEMPLATE_MODIFY_WIKI = "skin/plugins/wiki/modify_page.html";
    private static final String TEMPLATE_VIEW_WIKI = "skin/plugins/wiki/view_page.html";
    private static final String TEMPLATE_VIEW_HISTORY_WIKI = "skin/plugins/wiki/history_page.html";
    private static final String TEMPLATE_VIEW_DIFF_TOPIC_WIKI = "skin/plugins/wiki/diff_topic.html";
    private static final String TEMPLATE_LIST_WIKI = "skin/plugins/wiki/list_wiki.html";
    private static final String TEMPLATE_MAP_WIKI = "skin/plugins/wiki/map_wiki.html";
    private static final String TEMPLATE_SEARCH_WIKI = "skin/plugins/wiki/search_wiki.html";
    private static final String BEAN_SEARCH_ENGINE = "wiki.wikiSearchEngine";

    private static final String PROPERTY_PAGE_PATH = "wiki.pagePathLabel";
    private static final String PROPERTY_PAGE_TITLE = "wiki.pageTitle";
    private static final String PROPERTY_DEFAULT_RESULT_PER_PAGE = "wiki.search_wiki.itemsPerPage";
    private static final String PROPERTY_PATH_MAP = "wiki.path.map";
    private static final String PROPERTY_TITLE_MAP = "wiki.title.map";
    private static final String PROPERTY_PATH_LIST = "wiki.path.list";
    private static final String PROPERTY_TITLE_LIST = "wiki.title.list";
    private static final String PROPERTY_PATH_SEARCH = "wiki.path.search";
    private static final String PROPERTY_TITLE_SEARCH = "wiki.title.search";

    private static final String MARK_TOPIC = "topic";
    private static final String MARK_TOPIC_TITLE = "topic_title";
    private static final String MARK_REFLIST_TOPIC = "reflist_topic";
    private static final String MARK_MAP_TOPIC_TITLE = "map_topic_title";
    private static final String MARK_MAP_TOPIC_CHILDREN = "map_topic_children";
    private static final String MARK_WIKI_ROOT_PAGE_NAME = "wiki_root_page_name";
    private static final String MARK_VERSION = "version";
    private static final String MARK_LATEST_VERSION = "lastVersion";
    private static final String MARK_DIFF = "diff";
    private static final String MARK_RESULT = "result";
    private static final String MARK_LIST_TOPIC_VERSION = "listTopicVersion";
    private static final String MARK_PAGE_ROLES_LIST = "page_roles_list";
    private static final String MARK_QUERY = "query";
    private static final String MARK_PAGINATOR = "paginator";
    private static final String MARK_NB_ITEMS_PER_PAGE = "nb_items_per_page";
    private static final String MARK_EDIT_ROLE = "has_edit_role";
    private static final String MARK_ADMIN_ROLE = "has_admin_role";
    private static final String MARK_EXTEND = "isExtendInstalled";
    private static final String MARK_LANGUAGES_LIST = "languages_list";
    private static final String MARK_CURRENT_LANGUAGE = "current_language";
    private static final String PARAMETER_LANGUAGE = "language";

    private static final String VIEW_HOME = "home";
    private static final String VIEW_LIST = "list";
    private static final String VIEW_MAP = "map";
    private static final String VIEW_PAGE = "page";
    private static final String VIEW_MODIFY_PAGE = "modifyPage";
    private static final String VIEW_PREVIEW = "preview";
    private static final String VIEW_HISTORY = "history";
    private static final String VIEW_SEARCH = "search";
    private static final String VIEW_DIFF = "diff";
    private static final String VIEW_LIST_IMAGES = "listImages";
    private static final String ACTION_NEW_PAGE = "newPage";
    private static final String ACTION_DELETE_PAGE = "deletePage";
    private static final String ACTION_REMOVE_IMAGE = "removeImage";
    private static final String ACTION_REMOVE_VERSION = "removeVersion";
    private static final String ACTION_CONFIRM_REMOVE_VERSION = "confirmRemoveVersion";
    private static final String ACTION_UPLOAD_IMAGE = "uploadImage";
    private static final String MESSAGE_IMAGE_REMOVED = "wiki.message.image.removed";
    private static final String MESSAGE_NAME_MANDATORY = "wiki.message.error.name.notNull";
    private static final String MESSAGE_FILE_MANDATORY = "wiki.message.error.file.notNull";
    private static final String MESSAGE_CONFIRM_REMOVE_VERSION = "wiki.message.confirmRemoveVersion";
    private static final String MESSAGE_VERSION_REMOVED = "wiki.message.version.removed";
    private static final String MESSAGE_AUTHENTICATION_REQUIRED = "wiki.message.authenticationRequired";
    private static final String MESSAGE_PATH_HIDDEN = "wiki.message.path.hidden";
    private static final String MESSAGE_NO_PUBLISHED_VERSION = "wiki.view_page.noPublishedVersion";
    private static final String ANCHOR_IMAGES = "#images";

    private static final String DSKEY_WIKI_ROOT_LABEL = "wiki.site_property.path.rootLabel";
    private static final String DSKEY_WIKI_ROOT_PAGENAME = "wiki.site_property.path.rootPageName";

    private static final String PAGE_DEFAULT = "home";
    private static final String URL_VIEW_PAGE = "page=wiki&view=page&page_name=";

    private static final String PLUGIN_EXTEND = "extend";

    public static final int ACTION_VIEW = 1;

    private static final int MODE_VIEW = 0;
    private static final int MODE_EDIT = 1;

    // private fields
    private String _strCurrentPageIndex;
    private int _nDefaultItemsPerPage;
    private int _nItemsPerPage;

    /**
     * Gets the Home page
     *
     * @param request
     *            The HTTP request
     * @return The XPage
     */
    @View( value = VIEW_HOME, defaultView = true )
    public XPage getHome( HttpServletRequest request )
    {
        String strWikiRootPageName = DatastoreService.getDataValue( DSKEY_WIKI_ROOT_PAGENAME, "" );
        if ( !"".equals( strWikiRootPageName ) )
        {
            Map<String, String> mapParameters = new ConcurrentHashMap<>( );
            mapParameters.put( Constants.PARAMETER_PAGE_NAME, strWikiRootPageName );

            return redirect( request, VIEW_PAGE, mapParameters );
        }
        return redirectView( request, VIEW_LIST );
    }

    /**
     * Gets list page of all wiki pages
     *
     * @param request
     *            The HTTP request
     * @return The page
     */
    @View( VIEW_LIST )
    public XPage getTopicsList( HttpServletRequest request )
    {
        List<Topic> listTopic = getTopicsForUser( request );

        Map<String, String> mapTopicTitle = new HashMap( );

        for ( Topic topic : listTopic )
        {
            mapTopicTitle.put( topic.getPageName( ), getTopicTitle( request, topic ) );
        }

        Map<String, Object> model = getModel( );
        model.put( MARK_MAP_TOPIC_TITLE, mapTopicTitle );
        model.put( MARK_LANGUAGES_LIST, WikiLocaleService.getLanguages( ) );
        model.put( MARK_CURRENT_LANGUAGE, getLanguage( request ) );

        XPage page = getXPage( TEMPLATE_LIST_WIKI, getLocale( request ), model );
        page.setTitle( getPageTitle( I18nService.getLocalizedString( PROPERTY_TITLE_LIST, LocaleService.getContextUserLocale( request ) ) ) );
        page.setExtendedPathLabel( getViewExtendedPath( I18nService.getLocalizedString( PROPERTY_PATH_LIST, LocaleService.getContextUserLocale( request ) ) ) );

        return page;
    }

    /**
     * Gets maps page of the wiki
     *
     * @param request
     *            The HTTP request
     * @return The page
     */
    @View( VIEW_MAP )
    public XPage getTopicsMap( HttpServletRequest request )
    {
        Collection<Topic> listTopic = getTopicsForUser( request );

        String strWikiRootPageName = DatastoreService.getDataValue( DSKEY_WIKI_ROOT_PAGENAME, AppPropertiesService.getProperty( PAGE_DEFAULT ) );

        Map<String, String> mapTopicTitle = new HashMap( );
        Map<String, List<Topic>> mapTopicChildren = new HashMap( );

        for ( Topic topic : listTopic )
        {
            mapTopicTitle.put( topic.getPageName( ), getTopicTitle( request, topic ) );

            String strParentPageName = topic.getParentPageName( );
            if ( strParentPageName != null && !topic.getPageName( ).isEmpty( ) )
            {
                if ( mapTopicChildren.containsKey( strParentPageName ) )
                {
                    mapTopicChildren.get( strParentPageName ).add( topic );
                }
                else
                {
                    List<Topic> listChildren = new ArrayList<>( );
                    listChildren.add( topic );
                    mapTopicChildren.put( strParentPageName, listChildren );
                }
            }
        }

        Map<String, Object> model = getModel( );
        model.put( MARK_MAP_TOPIC_TITLE, mapTopicTitle );
        model.put( MARK_MAP_TOPIC_CHILDREN, mapTopicChildren );
        model.put( MARK_WIKI_ROOT_PAGE_NAME, strWikiRootPageName );
        model.put( MARK_LANGUAGES_LIST, WikiLocaleService.getLanguages( ) );
        model.put( MARK_CURRENT_LANGUAGE, getLanguage( request ) );

        XPage page = getXPage( TEMPLATE_MAP_WIKI, getLocale( request ), model );
        page.setTitle( getPageTitle( I18nService.getLocalizedString( PROPERTY_TITLE_MAP, LocaleService.getContextUserLocale( request ) ) ) );
        page.setExtendedPathLabel( getViewExtendedPath( I18nService.getLocalizedString( PROPERTY_PATH_MAP, LocaleService.getContextUserLocale( request ) ) ) );

        return page;
    }

    /**
     * Gets the search page
     *
     * @param request
     *            The request
     * @return Thye XPage
     */
    @View( VIEW_SEARCH )
    public XPage getSearch( HttpServletRequest request )
    {
        String strQuery = request.getParameter( Constants.PARAMETER_QUERY );
        String strPortalUrl = AppPathService.getPortalUrl( );

        UrlItem urlWikiXpage = new UrlItem( strPortalUrl );
        urlWikiXpage.addParameter( XPageAppService.PARAM_XPAGE_APP, Constants.PLUGIN_NAME );
        urlWikiXpage.addParameter( Constants.PARAMETER_VIEW, VIEW_SEARCH );

        _strCurrentPageIndex = Paginator.getPageIndex( request, Paginator.PARAMETER_PAGE_INDEX, _strCurrentPageIndex );
        _nDefaultItemsPerPage = AppPropertiesService.getPropertyInt( PROPERTY_DEFAULT_RESULT_PER_PAGE, 10 );
        _nItemsPerPage = Paginator.getItemsPerPage( request, Paginator.PARAMETER_ITEMS_PER_PAGE, _nItemsPerPage, _nDefaultItemsPerPage );

        SearchEngine engine = (SearchEngine) SpringContextService.getBean( BEAN_SEARCH_ENGINE );
        List<SearchResult> listResults = engine.getSearchResults( strQuery, request );

        Paginator paginator = new Paginator( listResults, _nItemsPerPage, urlWikiXpage.getUrl( ), Constants.PARAMETER_PAGE_INDEX, _strCurrentPageIndex );

        Map<String, Object> model = getModel( );
        model.put( MARK_RESULT, paginator.getPageItems( ) );
        model.put( MARK_QUERY, strQuery );
        model.put( MARK_PAGINATOR, paginator );
        model.put( MARK_NB_ITEMS_PER_PAGE, String.valueOf( _nItemsPerPage ) );

        XPage page = getXPage( TEMPLATE_SEARCH_WIKI, getLocale( request ), model );
        page.setTitle( getPageTitle( I18nService.getLocalizedString( PROPERTY_TITLE_SEARCH, getLocale( request ) ) ) );
        page.setExtendedPathLabel( getViewExtendedPath( I18nService.getLocalizedString( PROPERTY_PATH_SEARCH, getLocale( request ) ) ) );

        return page;
    }

    /**
     * Gets a wiki page
     *
     * @param request
     *            The HTTP request
     * @return The XPage
     * @throws SiteMessageException
     *             if an error occurs
     */
    @View( VIEW_PAGE )
    public XPage getTopic( HttpServletRequest request ) throws SiteMessageException, UserNotSignedException
    {
        if ( !SecurityService.isAuthenticationEnable( ) )
        {
            SiteMessageService.setMessage( request, MESSAGE_AUTHENTICATION_REQUIRED, SiteMessage.TYPE_ERROR );
        }
        String strPageName = request.getParameter( Constants.PARAMETER_PAGE_NAME );
        Topic topic = getTopic( request, strPageName, MODE_VIEW );
        TopicVersion version = TopicVersionHome.findLastVersion( topic.getIdTopic( ) );
        String strWikiPage = null;
        String topicTitle = getTopicTitle( request, topic );
        if ( version == null )
        {
            strWikiPage = I18nService.getLocalizedString( MESSAGE_NO_PUBLISHED_VERSION, getLocale( request ) );
        }
        else
        {
            fillUserData( version );
            strWikiPage = WikiService.instance( ).getWikiPage( strPageName, version, getPageUrl( request ), getLanguage( request ) );
            if ( version.getWikiContent( getLanguage( request ) ).getPageTitle( ) != null
                    && !version.getWikiContent( getLanguage( request ) ).getPageTitle( ).isEmpty( ) )
            {
                // if the page title is not empty, we use it instead of the topic title
                topicTitle = version.getWikiContent( getLanguage( request ) ).getPageTitle( );
            }
        }
        Map<String, Object> model = getModel( );
        model.put( MARK_RESULT, strWikiPage );
        model.put( MARK_TOPIC, topic );
        model.put( MARK_TOPIC_TITLE, topicTitle );
        model.put( MARK_LATEST_VERSION, version );
        model.put( MARK_EDIT_ROLE, RoleService.hasEditRole( request, topic ) );
        model.put( MARK_ADMIN_ROLE, RoleService.hasAdminRole( request ) );
        model.put( MARK_EXTEND, isExtend( ) );
        model.put( MARK_LANGUAGES_LIST, WikiLocaleService.getLanguages( ) );
        model.put( MARK_CURRENT_LANGUAGE, getLanguage( request ) );
        XPage page = getXPage( TEMPLATE_VIEW_WIKI, getLocale( request ), model );
        page.setTitle( getPageTitle( getTopicTitle( request, topic ) ) );
        page.setExtendedPathLabel( getPageExtendedPath( topic, request ) );
        return page;
    }

    /**
     * Creates a new Page
     *
     * @param request
     *            The request
     * @return The XPage
     * @throws UserNotSignedException
     *             if the user is not signed
     * @throws java.io.UnsupportedEncodingException
     *             if an encoding exception occurs
     */
    @Action( ACTION_NEW_PAGE )
    public XPage doCreateTopic( HttpServletRequest request ) throws UserNotSignedException, UnsupportedEncodingException
    {
        WikiAnonymousUser.checkUser( request );

        String strPageName = request.getParameter( Constants.PARAMETER_PAGE_NAME );
        String strParentPageName = request.getParameter( Constants.PARAMETER_PARENT_PAGE_NAME );
        if ( strParentPageName == null )
        {
            strParentPageName = Constants.PARENT_PAGE_NAME_IS_NULL;
        }
        String strPageTitle = strPageName;
        strPageName = WikiUtils.normalize( strPageName );

        Topic topic = TopicHome.findByPageName( strPageName );
        if ( topic == null )
        {
            topic = new Topic( );
            topic.setPageName( strPageName );
            topic.setViewRole( Page.ROLE_NONE );
            topic.setEditRole( Page.ROLE_NONE );
            topic.setParentPageName( strParentPageName );

            TopicHome.create( topic );
        }
        Map<String, String> mapParameters = new ConcurrentHashMap<>( );
        mapParameters.put( Constants.PARAMETER_PAGE_NAME, strPageName );
        mapParameters.put( Constants.PARAMETER_PAGE_TITLE, strPageTitle );
        return redirect( request, VIEW_MODIFY_PAGE, mapParameters );
    }

    /**
     * Displays the form to update a wiki page
     *
     * @param request
     *            The HTTP request
     * @return The XPage
     * @throws SiteMessageException
     *             if an exception occurs
     */
    @View( VIEW_MODIFY_PAGE )
    public XPage getModifyTopic( HttpServletRequest request ) throws SiteMessageException, UserNotSignedException
    {
        WikiAnonymousUser.checkUser( request );
        String strPageName = request.getParameter( Constants.PARAMETER_PAGE_NAME );
        Integer nVersion = getVersionTopicVersionId( request );
        Topic topic;
        Topic topicSession = (Topic) request.getSession( ).getAttribute( MARK_TOPIC );
        if ( topicSession != null && topicSession.getPageName( ).equals( strPageName ) )
        {
            topic = topicSession;
            request.getSession( ).removeAttribute( MARK_TOPIC );
        }
        else
        {
            topic = getTopic( request, strPageName, MODE_EDIT );
        }
        String strLocale = WikiLocaleService.getDefaultLanguage( );
        try {
            if( request.getParameter( Constants.PARAMETER_LOCAL ) != null )
            {
                strLocale = request.getParameter( Constants.PARAMETER_LOCAL );
            }
        } catch (Exception e) {
          AppLogService.error("no local parameter local", e);
        }

        TopicVersion topicVersion = TopicVersionHome.findLastVersion( topic.getIdTopic( ) );

        if ( topicVersion != null )
        {
            WikiContent content = topicVersion.getWikiContent( strLocale );

            if ( content != null )
            {
                String markupContent = content.getWikiContent( );
                if ( !markupContent.startsWith( Constants.MARKDOWN_TAG ) )
                {
                    String url = request.getRequestURL( ).toString( );
                    String newMarkdown = WikiCreoleToMarkdown.wikiCreoleToMd( markupContent, strPageName, url, strLocale );
                    content.setWikiContent( newMarkdown );
                }
                else
                {
                    content.setWikiContent( SpecialChar.renderWiki( markupContent ) );
                }
                content.setPageTitle( SpecialChar.renderWiki( content.getPageTitle( ) ) );
                topicVersion.addLocalizedWikiContent( strLocale, content );
            }
        }
        Map<String, Object> model = getModel( );
        ReferenceList topicRefList = getTopicsReferenceListForUser( request, true );
        topicRefList.removeIf( x -> x.getCode( ).equals( topic.getPageName( ) ) );
        List<String> topicNameList = getTopicNameListForUser( request);
        model.put( MARK_TOPIC, topic );
        model.put( MARK_VERSION, topicVersion );
        model.put( MARK_PAGE_ROLES_LIST, RoleService.getUserRoles( request ) );
        model.put( MARK_EDIT_ROLE, RoleService.hasEditRole( request, topic ) );
        model.put( MARK_ADMIN_ROLE, RoleService.hasAdminRole( request ) );
        model.put( MARK_LANGUAGES_LIST, WikiLocaleService.getLanguages( ) );
        model.put( MARK_REFLIST_TOPIC, topicRefList );
        model.put( "locale", strLocale );
        model.put( "topicNameList", topicNameList );
        ExtendableResourcePluginActionManager.fillModel( request, null, model, Integer.toString( topic.getIdTopic( ) ), Topic.RESOURCE_TYPE );
        XPage page = getXPage( TEMPLATE_MODIFY_WIKI, request.getLocale( ), model );
        page.setTitle( getPageTitle( getTopicTitle( request, topic ) ) );
        page.setExtendedPathLabel( getPageExtendedPath( topic, request ) );

        return page;
    }

    /**
     * Displays the preview of a wiki page
     *
     * @param request
     *            The HTTP request
     * @return The XPage
     * @throws SiteMessageException
     *             if an exception occurs
     */
    @View( VIEW_PREVIEW )
    public XPage getPreviewTopic( HttpServletRequest request ) throws IOException, SiteMessageException, UserNotSignedException
    {
        StringBuilder sb = new StringBuilder( );
        BufferedReader reader = request.getReader( );
        String line;
        while ( ( line = reader.readLine( ) ) != null )
        {
            sb.append( line );
        }
        String requestBody = sb.toString( );
        String wikiPageUrl = "";
        String pageTitle = "";
        String htmlContent = "";
        ObjectMapper mapper = new ObjectMapper( );
        HashMap<String, String> previewContent = new HashMap<>( );

        try
        {
            ContentDeserializer newContent = ContentDeserializer.deserializeWikiContent( requestBody );
             WikiAnonymousUser.checkUser( request );
                wikiPageUrl = newContent.getWikiPageUrl( );
                pageTitle = newContent.getTopicTitle( );
                htmlContent = newContent.getWikiHtmlContent( );
                htmlContent = LuteceHtmlParser.parseHtml( htmlContent, wikiPageUrl, pageTitle );
                htmlContent = SpecialChar.renderWiki( htmlContent );
                previewContent.put( "htmlContent", htmlContent );
        }
        catch( Exception e )
        {
            AppLogService.error( e.getMessage( ), e );
        }
        return responseJSON(mapper.writeValueAsString( previewContent));
    }

    /**
     * Displays the history page
     *
     * @param request
     *            The HTTP request
     * @return The XPage
     * @throws SiteMessageException
     *             if an error occurs
     */
    @View( VIEW_HISTORY )
    public XPage getHistory( HttpServletRequest request ) throws SiteMessageException, UserNotSignedException
    {
        String strPageName = request.getParameter( Constants.PARAMETER_PAGE_NAME );
        Topic topic = getTopic( request, strPageName, MODE_VIEW );
        Map<String, Object> model = getModel( );
        Collection<TopicVersion> listTopicVersions = TopicVersionHome.findAllVersions( topic.getIdTopic( ) );
        fillUsersData( listTopicVersions );
        model.put( MARK_LIST_TOPIC_VERSION, listTopicVersions );
        model.put( MARK_TOPIC, topic );
        model.put( MARK_EDIT_ROLE, RoleService.hasEditRole( request, topic ) );
        model.put( MARK_ADMIN_ROLE, RoleService.hasAdminRole( request ) );
        model.put( MARK_LANGUAGES_LIST, WikiLocaleService.getLanguages( ) );
        model.put( MARK_CURRENT_LANGUAGE, getLanguage( request ) );

        XPage page = getXPage( TEMPLATE_VIEW_HISTORY_WIKI, request.getLocale( ), model );
        page.setTitle( getPageTitle( getTopicTitle( request, topic ) ) );
        page.setExtendedPathLabel( getPageExtendedPath( topic, request ) );

        return page;
    }

    /**
     * Displays the diff page
     *
     * @param request
     *            The HTTP request
     * @return The XPage
     * @throws SiteMessageException
     *             if an error occurs
     */
    @View( VIEW_DIFF )
    public XPage getDiff( HttpServletRequest request ) throws SiteMessageException, UserNotSignedException
    {
        String strPageName = request.getParameter( Constants.PARAMETER_PAGE_NAME );
        Topic topic = getTopic( request, strPageName, MODE_VIEW );
        Boolean viewDiffHtml = true;
        if( request.getParameter( Constants.PARAMETER_VIEW_DIFF_HTML ) != null )
        {
            viewDiffHtml = Boolean.parseBoolean( request.getParameter( Constants.PARAMETER_VIEW_DIFF_HTML ) );
        }
        String strNewVersion = request.getParameter( Constants.PARAMETER_NEW_VERSION );
        String strOldVersion = request.getParameter( Constants.PARAMETER_OLD_VERSION );
        int nNewTopicVersion = Integer.parseInt( strNewVersion );
        int nOldTopicVersion = Integer.parseInt( strOldVersion );
        int nPrevTopicVersion = ( nOldTopicVersion == 0 ) ? nNewTopicVersion : nOldTopicVersion;

        TopicVersion newTopicVersion = TopicVersionHome.findByPrimaryKey( nNewTopicVersion );
        TopicVersion oldTopicVersion = TopicVersionHome.findByPrimaryKey( nPrevTopicVersion );

        String strLanguage = getLanguage( request );
        String strDiff = "";
        if(viewDiffHtml){
            String strNewHtml = WikiService.instance( ).getWikiPage( strPageName, newTopicVersion, strLanguage );
            String strOldHtml = WikiService.instance( ).getWikiPage( strPageName, oldTopicVersion, strLanguage );
            strDiff = DiffService.getDiff( strOldHtml, strNewHtml );
        } else {
            String strNewSource = SpecialChar.renderWiki(TopicVersionHome.findByPrimaryKey( nNewTopicVersion ).getWikiContent( strLanguage ).getWikiContent( ));
            String strOldSource = SpecialChar.renderWiki(TopicVersionHome.findByPrimaryKey( nPrevTopicVersion ).getWikiContent( strLanguage ).getWikiContent( ));
            strDiff = DiffService.getDiff( strOldSource, strNewSource );
        }
        Map<String, Object> model = getModel( );
        model.put( Constants.PARAMETER_NEW_VERSION, newTopicVersion.getIdTopicVersion() );
        model.put( Constants.PARAMETER_OLD_VERSION, oldTopicVersion.getIdTopicVersion() );
        model.put( Constants.PARAMETER_VIEW_DIFF_HTML, viewDiffHtml );
        model.put( MARK_DIFF, strDiff );
        model.put( MARK_TOPIC, topic );

        XPage page = getXPage( TEMPLATE_VIEW_DIFF_TOPIC_WIKI, request.getLocale( ), model );
        page.setTitle( getPageTitle( getTopicTitle( request, topic ) ) );
        page.setExtendedPathLabel( getPageExtendedPath( topic, request ) );

        return page;
    }
    /**
     * Deletes a wiki page
     *
     * @param request
     *            The HTTP Request
     * @return The redirect URL
     * @throws UserNotSignedException
     *             if user not connected
     */
    @Action( ACTION_DELETE_PAGE )
    public XPage doDeleteTopic( HttpServletRequest request ) throws UserNotSignedException
    {

        String strPageName = request.getParameter( Constants.PARAMETER_PAGE_NAME );
        Topic topic = TopicHome.findByPageName( strPageName );
        if ( RoleService.hasAdminRole( request ) )
        {
            TopicHome.remove( topic.getIdTopic( ) );
        }
        XPage page = redirectView( request, VIEW_MAP );
        return page;
    }

    /**
     * Uploads an image
     *
     * @param request
     *            The HTTP request
     * @return The XPage
     * @throws UserNotSignedException
     *             if the user is not signed
     */
    @Action( ACTION_UPLOAD_IMAGE )
    public XPage doUploadImage( HttpServletRequest request ) throws UserNotSignedException
    {
        String strPageName = request.getParameter( Constants.PARAMETER_PAGE_NAME );
        String strName = request.getParameter( Constants.PARAMETER_IMAGE_NAME );
        String strTopicId = request.getParameter( Constants.PARAMETER_TOPIC_ID );
        if ( RoleService.hasAdminRole( request ) )
        {
            MultipartHttpServletRequest multipartRequest = (MultipartHttpServletRequest) request;
            FileItem fileItem = multipartRequest.getFile( Constants.PARAMETER_IMAGE_FILE );
            Image image = new Image( );
            boolean bError = false;

            if ( ( fileItem == null ) || ( fileItem.getName( ) == null ) || "".equals( fileItem.getName( ) ) )
            {
                bError = true;
                addError( MESSAGE_FILE_MANDATORY, request.getLocale( ) );
            }

            if ( ( strName == null ) || strName.trim( ).equals( "" ) )
            {
                bError = true;
                addError( MESSAGE_NAME_MANDATORY, request.getLocale( ) );
            }

            if ( !bError )
            {
                image.setName( strName );
                image.setTopicId( Integer.parseInt( strTopicId ) );

                if ( ( fileItem != null ) && ( fileItem.getName( ) != null ) && !"".equals( fileItem.getName( ) ) )
                {
                    image.setValue( fileItem.get( ) );
                    image.setMimeType( fileItem.getContentType( ) );
                }
                else
                {
                    image.setValue( null );
                }

                image.setWidth( 500 );
                image.setHeight( 500 );

                ImageHome.create( image );
            }
        }

        Map<String, String> mapParameters = new ConcurrentHashMap<>( );
        mapParameters.put( Constants.PARAMETER_PAGE_NAME, strPageName + ANCHOR_IMAGES );

        return redirect( request, VIEW_MODIFY_PAGE, mapParameters );
    }

    /**
     * Handles the removal form of an image
     *
     * @param request
     *            The Http request
     * @return the jsp URL to display the form to manage images
     * @throws UserNotSignedException
     *             if the user is not signed
     */
    @Action( ACTION_REMOVE_IMAGE )
    public XPage doRemoveImage( HttpServletRequest request ) throws UserNotSignedException
    {
        int nId = Integer.parseInt( request.getParameter( Constants.PARAMETER_IMAGE_ID ) );
        int nTopicId = Integer.parseInt( request.getParameter( Constants.PARAMETER_TOPIC_ID ) );
        Topic topic = TopicHome.findByPrimaryKey( nTopicId );
        if ( RoleService.hasEditRole( request, topic ) )
        {
            ImageHome.remove( nId );
            addInfo( MESSAGE_IMAGE_REMOVED, getLocale( request ) );

        }
        return null;
    }

    /**
     * Manages the removal form of a topic version whose identifier is in the http request
     *
     * @param request
     *            The Http request
     * @return the html code to confirm
     * @throws SiteMessageException
     *             A site message
     */
    @Action( ACTION_CONFIRM_REMOVE_VERSION )
    public XPage getConfirmRemoveVersion( HttpServletRequest request ) throws SiteMessageException
    {
        int nId = Integer.parseInt( request.getParameter( Constants.PARAMETER_TOPIC_VERSION_ID ) );
        UrlItem url = new UrlItem( AppPathService.getPortalUrl( ) );
        url.addParameter( Constants.PARAMETER_PAGE, Constants.PLUGIN_NAME );
        url.addParameter( Constants.PARAMETER_PAGE_NAME, request.getParameter( Constants.PARAMETER_PAGE_NAME ) );
        url.addParameter( Constants.PARAMETER_ACTION, ACTION_REMOVE_VERSION );
        url.addParameter( Constants.PARAMETER_TOPIC_VERSION_ID, nId );

        SiteMessageService.setMessage( request, MESSAGE_CONFIRM_REMOVE_VERSION, SiteMessage.TYPE_CONFIRMATION, url.getUrl( ) );

        return null;
    }

    /**
     * Handles the removal form of a topic version
     *
     * @param request
     *            The Http request
     * @return the jsp URL to display the form to manage images
     * @throws UserNotSignedException
     *             if the user is not signed
     */
    @Action( ACTION_REMOVE_VERSION )
    public XPage doRemoveVersion( HttpServletRequest request ) throws UserNotSignedException
    {
        WikiAnonymousUser.checkUser( request );

        // requires admin role
        if ( RoleService.hasAdminRole( request ) )
        {
            int nId = Integer.parseInt( request.getParameter( Constants.PARAMETER_TOPIC_VERSION_ID ) );
            TopicVersionHome.remove( nId );
            addInfo( MESSAGE_VERSION_REMOVED, getLocale( request ) );
        }

        Map<String, String> mapParameters = new ConcurrentHashMap<>( );
        mapParameters.put( Constants.PARAMETER_PAGE_NAME, request.getParameter( Constants.PARAMETER_PAGE_NAME ) );

        return redirect( request, VIEW_HISTORY, mapParameters );
    }

    /**
     * Returns the image list as JSON
     *
     * @param request
     *            The HTTP request
     * @return A JSON flow
     */
    @View( VIEW_LIST_IMAGES )
    public XPage getListImages( HttpServletRequest request ) throws JsonProcessingException {
        String strTopicId = request.getParameter( Constants.PARAMETER_TOPIC_ID );
        List<String> imageList = new ArrayList<>( );

        if ( strTopicId != null )
        {
            int nTopicId = Integer.parseInt( strTopicId );
            List<Image> list = ImageHome.findByTopic( nTopicId );
            for ( Image image : list )
            {
                HashMap<String, String> imageMap = new HashMap<>( );
                ObjectMapper mapper = new ObjectMapper( );
                imageMap.put( "id", Integer.toString( image.getId( ) ) );
                imageMap.put( "name", image.getName( ) );
                imageList.add( mapper.writeValueAsString(imageMap) );;

            }
        }
        return responseJSON(imageList.toString());
    }

    /**
     * Gets the topic corresponding to the given page name
     *
     * @param request
     *            The HTTP request
     * @param strPageName
     *            The page name
     * @param nMode
     *            The mode VIEW vs EDIT
     * @return The topic
     * @throws SiteMessageException
     *             If an error occurs
     */
    private Topic getTopic( HttpServletRequest request, String strPageName, int nMode ) throws SiteMessageException, UserNotSignedException
    {
        Topic topic = TopicHome.findByPageName( strPageName );

        if ( topic == null )
        {
            SiteMessageService.setMessage( request, Constants.MESSAGE_PAGE_NOT_EXISTS, SiteMessage.TYPE_STOP );
        }
        else
        {
            String strRole;
            String strMessage;
            switch( nMode )
            {
                case MODE_EDIT:
                    strRole = topic.getEditRole( );
                    strMessage = Constants.MESSAGE_USER_NOT_IN_ROLE;
                    break;
                case MODE_VIEW:
                default:
                    strRole = topic.getViewRole( );
                    strMessage = Constants.MESSAGE_USER_NOT_IN_ROLE;
                    break;
            }

            if ( SecurityService.isAuthenticationEnable( ) && ( !Page.ROLE_NONE.equals( strRole ) ) )
            {
                LuteceUser user = SecurityService.getInstance( ).getRemoteUser( request );

                if ( user == null )
                {
                    throw new UserNotSignedException( );
                }

                if ( !SecurityService.getInstance( ).isUserInRole( request, strRole ) )
                {
                    SiteMessageService.setMessage( request, strMessage, SiteMessage.TYPE_STOP );
                }
            }
        }

        return topic;
    }

    /**
     * Gets topics visible for a given user
     *
     * @param request
     *            The HTTP request
     * @return The list of topics
     */
    private List<Topic> getTopicsForUser( HttpServletRequest request )
    {
        Collection<Topic> listTopicAll = TopicHome.getTopicsList( );
        List<Topic> listTopic;

        if ( SecurityService.isAuthenticationEnable( ) )
        {
            listTopic = new ArrayList<>( );

            for ( Topic topic : listTopicAll )
            {
                String strRole = topic.getViewRole( );

                if ( !Page.ROLE_NONE.equals( strRole ) )
                {
                    if ( SecurityService.getInstance( ).isUserInRole( request, strRole ) )
                    {
                        listTopic.add( topic );
                    }
                }
                else
                {
                    listTopic.add( topic );
                }
            }
        }
        else
        {
            listTopic = new ArrayList<>( listTopicAll );
        }

        return listTopic;
    }

    /**
     * Returns a reference list of pages for user
     *
     * @param request
     *            The HTTP request
     * @param bFirstItem
     *            True if the reference list must have a first empty default item
     * @return The reference list
     */
    private ReferenceList getTopicsReferenceListForUser( HttpServletRequest request, boolean bFirstItem )
    {
        ReferenceList list = new ReferenceList( );

        if ( bFirstItem )
        {
            list.addItem( "", "" );
        }
        for ( Topic topic : getTopicsForUser( request ) )
        {
            list.addItem( topic.getPageName( ), getTopicTitle( request, topic ) );
        }

        return list;
    }
    /*
     * @return the list of pages for user with topic name as value
     * @param request
     */
    private List<String> getTopicNameListForUser( HttpServletRequest request )
    {
        List<String> list = new ArrayList<>( );

        for ( Topic topic : getTopicsForUser( request ) )
        {
            list.add( topic.getPageName( ) );
        }

        return list;
    }

    /**
     * Builds the page title
     *
     * @param strPageName
     *            The page name
     * @return The page title
     */
    private String getPageTitle( String strPageName )
    {
        StringBuilder sbPath = new StringBuilder( );
        sbPath.append( AppPropertiesService.getProperty( PROPERTY_PAGE_TITLE ) ).append( ' ' ).append( strPageName );

        return sbPath.toString( );
    }

    /**
     * Builds the extended path for views other than page
     *
     * @param strViewTitle
     *            The view title
     * @return The list that content path info
     */
    private ReferenceList getViewExtendedPath( String strViewTitle )
    {
        String strWikiRootLabel = DatastoreService.getDataValue( DSKEY_WIKI_ROOT_LABEL, AppPropertiesService.getProperty( PROPERTY_PAGE_PATH ) );
        String strWikiRootPageName = DatastoreService.getDataValue( DSKEY_WIKI_ROOT_PAGENAME, PAGE_DEFAULT );

        ReferenceList list = new ReferenceList( );

        list.addItem( strWikiRootLabel, URL_VIEW_PAGE + strWikiRootPageName );
        list.addItem( strViewTitle, "" );

        return list;
    }

    /**
     * Builds the extended path for wiki page
     *
     * @param topic
     *            The topic page
     * @param request
     *            The request
     * @return The list that content path info
     */
    private ReferenceList getPageExtendedPath( Topic topic, HttpServletRequest request )
    {
        String strWikiRootLabel = DatastoreService.getDataValue( DSKEY_WIKI_ROOT_LABEL, AppPropertiesService.getProperty( PROPERTY_PAGE_PATH ) );
        String strWikiRootPageName = DatastoreService.getDataValue( DSKEY_WIKI_ROOT_PAGENAME, AppPropertiesService.getProperty( PAGE_DEFAULT ) );

        ReferenceList list = new ReferenceList( );
        ReferenceItem item;

        String strTopicTitle;
        String strTopicUrl;

        if ( !topic.getPageName( ).equals( strWikiRootPageName ) )
        {
            list.addItem( getTopicTitle( request, topic ), "" );
        }

        while ( topic != null && !topic.getParentPageName( ).isEmpty( ) && topic.getParentPageName( ) != null
                && !topic.getParentPageName( ).equals( strWikiRootPageName ) && !isNameInReferenceList( list, URL_VIEW_PAGE + topic.getParentPageName( ) ) )
        {
            topic = TopicHome.findByPageName( topic.getParentPageName( ) );

            if ( topic != null )
            {
                strTopicTitle = getTopicTitle( request, topic );
                strTopicUrl = URL_VIEW_PAGE + topic.getPageName( );

                if ( SecurityService.isAuthenticationEnable( ) && ( !Page.ROLE_NONE.equals( topic.getViewRole( ) ) ) )
                {
                    if ( !SecurityService.getInstance( ).isUserInRole( request, topic.getViewRole( ) ) )
                    {
                        strTopicTitle = I18nService.getLocalizedString( MESSAGE_PATH_HIDDEN, getLocale( request ) );
                        strTopicUrl = "";
                    }
                }

                item = new ReferenceItem( );
                item.setCode( strTopicTitle );
                item.setName( strTopicUrl );

                list.add( 0, item );
            }
        }

        item = new ReferenceItem( );
        item.setCode( strWikiRootLabel );
        item.setName( URL_VIEW_PAGE + strWikiRootPageName );

        list.add( 0, item );

        return list;
    }

    /**
     * Checks if name is already in an item of the reference list
     *
     * @param list
     *            The reference list
     * @param name
     *            The name
     * @return true if the name is in list and false otherwise
     */
    private boolean isNameInReferenceList( ReferenceList list, String name )
    {
        for ( ReferenceItem item : list )
        {
            if ( item.getName( ).equals( name ) )
            {
                return true;
            }
        }

        return false;
    }

    /**
     * Fills all versions with users infos
     *
     * @param listVersions
     *            The version
     */
    private void fillUsersData( Collection<TopicVersion> listVersions )
    {
        for ( TopicVersion version : listVersions )
        {
            fillUserData( version );
        }
    }

    /**
     * Fills the version with users infos
     *
     * @param version
     *            The version
     */
    private void fillUserData( TopicVersion version )
    {
        String strUserId = version.getLuteceUserId( );

        LuteceUser user = SecurityService.getInstance( ).getUser( strUserId );

        if ( user != null )
        {
            version.setUserName( user.getUserInfo( LuteceUser.NAME_GIVEN ) + " " + user.getUserInfo( LuteceUser.NAME_FAMILY ) );
            version.setUserAvatarUrl( AvatarService.getAvatarUrl( user.getEmail( ) ) );
        }
        else
        {
            version.setUserAvatarUrl( AvatarService.getAvatarUrl( strUserId ) );
        }

        version.setUserPseudo( UserPreferencesService.instance( ).getNickname( strUserId ) );
    }

    /**
     * Gets the current page URL from the request
     *
     * @param request
     *            The request
     * @return The URL
     */
    private String getPageUrl( HttpServletRequest request )
    {
        return request.getRequestURI( ).substring( request.getContextPath( ).length( ) + 1 ) + "?" + request.getQueryString( );
    }

    /**
     * Returns if the plugin Extend is available
     *
     * @return true if extend is installed and activated otherwise false
     */
    private boolean isExtend( )
    {
        Plugin plugin = PluginService.getPlugin( PLUGIN_EXTEND );
        return ( ( plugin != null ) && plugin.isInstalled( ) );
    }

    /**
     * Stores the current selected language in the user's session
     *
     * @param request
     *            The request
     * @param strLanguage
     *            The language
     */
    private void setLanguage( HttpServletRequest request, String strLanguage )
    {
        Locale userSelectedLocale = new Locale( strLanguage );
        if ( LocaleService.isSupported( userSelectedLocale ) )
        {
            LocaleService.setUserSelectedLocale( request, userSelectedLocale );
        }
    }

    /**
     * Retrieves the current selected language from the user's session
     *
     * @param request
     *            The request
     * @return The Language
     */
    private String getLanguage( HttpServletRequest request )
    {
        String strLanguage = null;

        if ( request.getParameter( PARAMETER_LANGUAGE ) != null )
        {
            // consider the language parameter in the URL if exists
            strLanguage = request.getParameter( PARAMETER_LANGUAGE );
            setLanguage( request, strLanguage );
        }
        else
        {
            // otherwise, consider the language stored in the user's session
            strLanguage = LocaleService.getUserSelectedLocale( request ).getLanguage( );
            setLanguage( request, strLanguage );
        }

        return LocaleService.getContextUserLocale( request ).getLanguage( );
    }

    /**
     * Returns a topic title
     *
     * @param topic
     *            The topic
     * @param strLanguage
     *            The language
     * @return The title
     */
    private String getTopicTitle( Topic topic, String strLanguage )
    {
        TopicVersion version = TopicVersionHome.findLastVersion( topic.getIdTopic( ) );
        if ( version != null && version.getWikiContent( strLanguage ) != null && StringUtils.isNotEmpty( version.getWikiContent(strLanguage).getPageTitle( ) ) )
        {
            return version.getWikiContent( strLanguage ).getPageTitle( );
        }
        else
        {
            return topic.getPageName( );
        }
    }
    /**
     * Returns a topic title
     *
     * @param request
     *            The HTTP request
     * @param topic
     *            The topic
     * @return The title
     */
    private String getTopicTitle( HttpServletRequest request, Topic topic )
    {
        return getTopicTitle( topic, getLanguage( request ) );
    }

    /**
     * Retrieves the current selected version from the parameter
     *
     * @param request
     *            The request
     * @return The Language
     */
    private Integer getVersionTopicVersionId( HttpServletRequest request )
    {
        Integer versionId = null;

        if ( request.getParameter( Constants.PARAMETER_TOPIC_VERSION_ID ) != null )
        {
            versionId = Integer.parseInt( request.getParameter( Constants.PARAMETER_TOPIC_VERSION_ID ) );
            return versionId;
        }

        return versionId;
    }

}