001/*
002 *  Copyright 2017 Anyware Services
003 *
004 *  Licensed under the Apache License, Version 2.0 (the "License");
005 *  you may not use this file except in compliance with the License.
006 *  You may obtain a copy of the License at
007 *
008 *      http://www.apache.org/licenses/LICENSE-2.0
009 *
010 *  Unless required by applicable law or agreed to in writing, software
011 *  distributed under the License is distributed on an "AS IS" BASIS,
012 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 *  See the License for the specific language governing permissions and
014 *  limitations under the License.
015 */
016package org.ametys.plugins.linkdirectory.link;
017
018import java.io.IOException;
019import java.io.InputStream;
020import java.net.URL;
021import java.util.ArrayList;
022import java.util.HashMap;
023import java.util.List;
024import java.util.Map;
025
026import org.apache.avalon.framework.parameters.Parameters;
027import org.apache.avalon.framework.service.ServiceException;
028import org.apache.avalon.framework.service.ServiceManager;
029import org.apache.cocoon.acting.ServiceableAction;
030import org.apache.cocoon.environment.ObjectModelHelper;
031import org.apache.cocoon.environment.Redirector;
032import org.apache.cocoon.environment.Request;
033import org.apache.cocoon.environment.Response;
034import org.apache.cocoon.environment.SourceResolver;
035import org.apache.commons.lang3.StringUtils;
036import org.apache.http.HttpEntity;
037import org.apache.http.client.config.RequestConfig;
038import org.apache.http.client.methods.CloseableHttpResponse;
039import org.apache.http.client.methods.HttpGet;
040import org.apache.http.entity.ContentType;
041import org.apache.http.impl.client.CloseableHttpClient;
042import org.apache.http.impl.client.HttpClientBuilder;
043
044import org.ametys.core.cocoon.JSonReader;
045import org.ametys.core.upload.Upload;
046import org.ametys.core.upload.UploadManager;
047import org.ametys.core.user.CurrentUserProvider;
048import org.ametys.core.user.UserIdentity;
049import org.ametys.core.util.JSONUtils;
050import org.ametys.plugins.linkdirectory.DirectoryHelper;
051import org.ametys.plugins.linkdirectory.Link;
052import org.ametys.plugins.linkdirectory.Link.LinkType;
053import org.ametys.web.repository.site.SiteManager;
054import org.ametys.web.url.UrlPreviewComponent;
055
056/**
057 * Adds a new user link in Ametys.
058 */
059public class AddUserLinkAction extends ServiceableAction
060{
061    private static int _ICO_REQUEST_TIMEOUT = 3000;
062    
063    private static final List<String> __ICO_SUPPORTED_MIMETYPES = List.of("image/jpeg", "image/jpg", "image/png", "image/svg+xml");
064    private static final List<String> __ICO_SUPPORTED_EXTENSIONS = List.of(".jpeg", ".jpg", ".png", ".svg");
065    
066    /** The DAO for {@link Link}s */
067    protected LinkDAO _linkDAO;
068    /** The current user provider */
069    protected CurrentUserProvider _currentUserProvider;
070    /** The directory helper */
071    protected DirectoryHelper _directoryHelper;
072    /** The site manager */
073    protected SiteManager _siteManager;
074    /** The url preview component */
075    protected UrlPreviewComponent _urlPreviewComponent;
076    /** The upload manager */
077    protected UploadManager _uploadManager;
078    /** The JSON utils */
079    protected JSONUtils _jsonUtils;
080    
081    @Override
082    public void service(ServiceManager smanager) throws ServiceException
083    {
084        super.service(smanager);
085        _currentUserProvider = (CurrentUserProvider) smanager.lookup(CurrentUserProvider.ROLE);
086        _linkDAO = (LinkDAO) smanager.lookup(LinkDAO.ROLE);
087        _directoryHelper = (DirectoryHelper) smanager.lookup(DirectoryHelper.ROLE);
088        _siteManager = (SiteManager) smanager.lookup(SiteManager.ROLE);
089        _urlPreviewComponent = (UrlPreviewComponent) smanager.lookup(UrlPreviewComponent.ROLE);
090        _uploadManager = (UploadManager) smanager.lookup(UploadManager.ROLE);
091        _jsonUtils = (JSONUtils) smanager.lookup(JSONUtils.ROLE);
092    }
093    
094    @Override
095    public Map act(Redirector redirector, SourceResolver resolver, Map objectModel, String source, Parameters parameters) throws Exception
096    {
097        Request request = ObjectModelHelper.getRequest(objectModel);
098        Map<String, Object> result = new HashMap<>();
099        
100        UserIdentity user = _currentUserProvider.getUser();
101        
102        // Check user authenticated
103        if (user != null)
104        {
105            // Create link
106            Map<String, Object> createParameters = new HashMap<>();
107            String siteName = request.getParameter("siteName");
108            String lang = request.getParameter("lang");
109            String url = request.getParameter("url");
110            //The color key, default color will be used if it is not present or empty
111            String color = request.getParameter("color");
112            
113            createParameters.put("siteName", siteName);
114            createParameters.put("lang", lang);
115            createParameters.put("url-type", LinkType.URL.toString());
116            createParameters.put("url", url);
117            createParameters.put("title", request.getParameter("title"));
118            createParameters.put("color", color);
119            
120            String userAgent = request.getHeader("User-Agent");
121            Upload upload = _getLinkFavicon(url, user, userAgent);
122            if (upload != null)
123            {
124                Map<String, Object> linkMap = new HashMap<>();
125                linkMap.put("id", upload.getId());
126                linkMap.put("type", "external");
127                createParameters.put("picture", _jsonUtils.convertObjectToJson(linkMap));
128            }
129            
130            _handleTheme(request, createParameters, siteName, lang);
131            
132            Map<String, Object> createdLinkresult = _linkDAO.createUserLink(createParameters);
133            
134            if (createdLinkresult.containsKey("already-exists"))
135            {
136                result.put("error", "already-exists");
137            }
138            else
139            {
140                result.put("error", "none");
141                result.putAll(createdLinkresult);
142            }
143        }
144        else
145        {
146            result.put("error", "unauthenticated-user");
147        }
148        
149        Response response = ObjectModelHelper.getResponse(objectModel);
150        response.setHeader("Access-Control-Allow-Origin", request.getHeader("Origin"));
151        response.setHeader("Access-Control-Allow-Credentials", "true");
152        
153        request.setAttribute(JSonReader.OBJECT_TO_READ, result);
154        return EMPTY_MAP;
155    }
156    
157    private void _handleTheme(Request request, Map<String, Object> createParameters, String siteName, String lang)
158    {
159        String themeName = request.getParameter("themes");
160        
161        List<String> themes = new ArrayList<>();
162        // We have a theme name ...
163        if (StringUtils.isNotBlank(themeName))
164        {
165            // ... retrieve JCR id from theme name
166            if (_directoryHelper.themeExists(themeName, siteName, lang))
167            {
168                themes.add(themeName);
169            }
170        }
171        // if not, just send an empty list for the user link creation
172        createParameters.put("themes", themes);
173    }
174    
175    /**
176     * Set the favicon of the link url
177     * @param url the url of the favicon
178     * @param user the user
179     * @param userAgent The user agent
180     * @return the favicon upload map
181     */
182    protected Upload _getLinkFavicon(String url, UserIdentity user, String userAgent)
183    {
184        Upload upload = null;
185        try
186        {
187            String favIconLink = _urlPreviewComponent.getFavicon(url);
188            
189            if (StringUtils.isBlank(favIconLink))
190            {
191                return null;
192            }
193            
194            URL favIconURL = new URL(favIconLink);
195            String favIconName = StringUtils.substringAfterLast(favIconURL.getPath(), "/");
196            
197            RequestConfig requestConfig = RequestConfig.custom()
198                    .setConnectTimeout(_ICO_REQUEST_TIMEOUT)
199                    .setConnectionRequestTimeout(_ICO_REQUEST_TIMEOUT)
200                    .setSocketTimeout(_ICO_REQUEST_TIMEOUT)
201                    .build();
202            
203            try (CloseableHttpClient httpclient = HttpClientBuilder.create()
204                                                                   .setDefaultRequestConfig(requestConfig)
205                                                                   .useSystemProperties()
206                                                                   .setUserAgent(userAgent)
207                                                                   .build())
208            {
209                // Prepare a request object
210                HttpGet get = new HttpGet(favIconLink);
211                try (CloseableHttpResponse httpResponse = httpclient.execute(get))
212                {
213                    int statusCode = httpResponse.getStatusLine().getStatusCode();
214                    if (statusCode != 200)
215                    {
216                        getLogger().warn("Error " + statusCode + ". Can't set the favicon with url " + favIconLink + " to the link " + url);
217                        return upload;
218                    }
219                    
220                    try (InputStream is = httpResponse.getEntity().getContent())
221                    {
222                        HttpEntity entity = httpResponse.getEntity();
223                        String mimeType = ContentType.get(entity).getMimeType();
224                        if (mimeType.equals("image/x-icon") || mimeType.equals("image/vnd.microsoft.icon") || favIconName.endsWith(".ico"))
225                        {
226                            // If the image is .ico, convert it in .png
227                            InputStream convertedIcoToPngIS = _urlPreviewComponent.convertIcoToPng(is);
228                            upload = _uploadManager.storeUpload(user, StringUtils.replace(favIconName, ".ico", ".png"), convertedIcoToPngIS);
229                        }
230                        else if (_checkFavico(favIconName, mimeType))
231                        {
232                            upload = _uploadManager.storeUpload(user, favIconName, is);
233                        }
234                        else
235                        {
236                            getLogger().warn("Unsupported mime-type " + mimeType + " for favicon from url " + favIconLink + " to the link " + url);
237                        }
238                    }
239                }
240            }
241            catch (IOException e)
242            {
243                getLogger().warn("Unable to download favicon from url " + favIconLink + " to the link " + url, e);
244            }
245        }
246        catch (Exception e) 
247        {
248            getLogger().warn("Unable to get favicon from link url " + url, e);
249        }
250        
251        return upload;
252    }
253    
254    private boolean _checkFavico(String favicoName, String mimeType)
255    {
256        return __ICO_SUPPORTED_MIMETYPES.stream().anyMatch(m -> StringUtils.equalsIgnoreCase(m, mimeType))
257                || __ICO_SUPPORTED_EXTENSIONS.stream().anyMatch(ext -> favicoName.toLowerCase().endsWith(ext));
258        
259    }
260}