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