001/*
002 *  Copyright 2015 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.syndication;
017
018import java.io.IOException;
019import java.io.UnsupportedEncodingException;
020import java.net.URLDecoder;
021import java.util.ArrayList;
022import java.util.Arrays;
023import java.util.HashMap;
024import java.util.List;
025import java.util.Map;
026import java.util.Set;
027import java.util.stream.Collectors;
028
029import org.apache.avalon.framework.service.ServiceException;
030import org.apache.avalon.framework.service.ServiceManager;
031import org.apache.cocoon.ProcessingException;
032import org.apache.cocoon.components.source.impl.SitemapSource;
033import org.apache.cocoon.environment.ObjectModelHelper;
034import org.apache.cocoon.environment.Request;
035import org.apache.cocoon.generation.ServiceableGenerator;
036import org.apache.cocoon.xml.AttributesImpl;
037import org.apache.cocoon.xml.XMLUtils;
038import org.apache.commons.collections.CollectionUtils;
039import org.apache.commons.lang.StringUtils;
040import org.apache.commons.lang3.ArrayUtils;
041import org.apache.excalibur.source.SourceResolver;
042import org.xml.sax.SAXException;
043
044import org.ametys.core.group.Group;
045import org.ametys.core.group.GroupIdentity;
046import org.ametys.core.group.GroupManager;
047import org.ametys.core.right.RightManager;
048import org.ametys.core.user.CurrentUserProvider;
049import org.ametys.core.user.UserIdentity;
050import org.ametys.core.userpref.UserPreferencesException;
051import org.ametys.core.userpref.UserPreferencesManager;
052import org.ametys.core.util.IgnoreRootHandler;
053import org.ametys.core.util.JSONUtils;
054import org.ametys.plugins.repository.data.holder.ModelAwareDataHolder;
055import org.ametys.plugins.repository.data.holder.group.impl.ModelAwareRepeater;
056import org.ametys.plugins.repository.data.holder.group.impl.ModelAwareRepeaterEntry;
057import org.ametys.web.renderingcontext.RenderingContextHandler;
058import org.ametys.web.repository.page.Page;
059import org.ametys.web.repository.page.ZoneItem;
060import org.ametys.web.userpref.FOUserPreferencesConstants;
061
062/**
063 * Generator for feeds service
064 */
065public class FeedsGenerator extends ServiceableGenerator
066{
067    /** User pref key for urls */
068    public static final String USER_PREF_RSS_URL_KEY = "user-pref-rss-urls";
069    
070    /** User pref key for positions */
071    public static final String USER_PREF_RSS_ID_KEY = "user-pref-rss-positions";
072    
073    private static HashMap<String, Integer> _lifeTimes = new HashMap<>();
074    static 
075    {
076        _lifeTimes.put("1", 1440); // 24 hours
077        _lifeTimes.put("2", 180); // 3 hours
078        _lifeTimes.put("3", 30); // 30 minutes
079    }
080    
081    /** Conf access component */
082    protected RssFeedUserPrefsComponent _confAccess;
083    
084    /** The right manager */
085    protected RightManager _rightManager;
086    
087    /** The group manager */
088    protected GroupManager _groupManager;
089    
090    /** The feed cache */
091    protected FeedCache _feedCache;
092    
093    /** The source resolver */
094    protected SourceResolver _resolver;
095    
096    /** The user preferences manager. */
097    protected UserPreferencesManager _userPrefManager;
098    
099    /** The rendering context handler. */
100    protected RenderingContextHandler _renderingContext;
101
102    /** The current user provider */
103    protected CurrentUserProvider _currentUserProvider;
104
105    private JSONUtils _jsonUtils;
106    
107    @Override
108    public void service(ServiceManager serviceManager) throws ServiceException
109    {
110        super.service(serviceManager);
111        _resolver = (SourceResolver) serviceManager.lookup(SourceResolver.ROLE);
112        _feedCache = (FeedCache) serviceManager.lookup(FeedCache.ROLE);
113        _rightManager = (RightManager) manager.lookup(RightManager.ROLE);
114        _groupManager = (GroupManager) serviceManager.lookup(GroupManager.ROLE);
115        _userPrefManager = (UserPreferencesManager) serviceManager.lookup(UserPreferencesManager.ROLE + ".FO");
116        _renderingContext = (RenderingContextHandler) serviceManager.lookup(RenderingContextHandler.ROLE);
117        _confAccess = (RssFeedUserPrefsComponent) serviceManager.lookup(RssFeedUserPrefsComponent.ROLE);
118        _currentUserProvider = (CurrentUserProvider) serviceManager.lookup(CurrentUserProvider.ROLE);
119        _jsonUtils = (JSONUtils) serviceManager.lookup(JSONUtils.ROLE);
120    }
121    
122    public void generate() throws IOException, SAXException, ProcessingException
123    {
124        Request request = ObjectModelHelper.getRequest(objectModel);
125        
126        ZoneItem zoneItem = (ZoneItem) request.getAttribute(ZoneItem.class.getName());
127        UserIdentity user = _currentUserProvider.getUser();
128        
129        Page page = zoneItem.getZone().getPage();
130        String siteName = page.getSiteName();
131        String lang = page.getSitemapName();
132        
133        String storageContext = siteName + "/" + lang + "/" + zoneItem.getId();
134        Map<String, String> contextVars = _getContextVars(siteName, lang);
135        
136        ModelAwareDataHolder serviceParameters = zoneItem.getServiceParameters();
137
138        List<String> urlList = new ArrayList<>();
139        List<String> idList = new ArrayList<>();
140        try
141        {
142            // Check user prefence only if the page has limited access
143            if (!_rightManager.hasAnonymousReadAccess(page))
144            {
145                urlList = _getListUrl(user, storageContext, contextVars);
146                idList = _getListId(user, storageContext, contextVars);
147            }
148        }
149        catch (UserPreferencesException e1)
150        {
151            getLogger().error("Can't have the user preference from user : '" + user + "'");
152        }
153        
154        contentHandler.startDocument();
155        _saxAllFeed(request, zoneItem, serviceParameters, urlList, idList);
156        contentHandler.endDocument();
157    }
158    
159    // Sax all the feed depend on the service parameters
160    private void _saxAllFeed(Request request, ZoneItem zoneItem, ModelAwareDataHolder serviceParameters, List<String> urlList, List<String> userSelectedFeedsId) throws SAXException
161    {
162        long defaultLength = serviceParameters.getValue("length", false, -1L);
163        String defaultLifeTime = serviceParameters.getValue("cache");
164
165        AttributesImpl atts = new AttributesImpl();
166
167        ModelAwareRepeater rssFeeds = serviceParameters.getRepeater("feeds");
168        List<ModelAwareRepeaterEntry> listFeeds = rssFeeds.getEntries().stream()
169                                                                       .filter(entry -> _checkUserAccessRSS(entry))
170                                                                       .collect(Collectors.toList());
171
172        int nbMaxFeedFullSaxed = Math.toIntExact(serviceParameters.getValue("nb-feed-max", false, 0L));
173        int nbMaxUser = Math.toIntExact(serviceParameters.getValue("nb-feed-user", false, 0L));
174        
175        atts.addCDATAAttribute("nbFeedService", String.valueOf(listFeeds.size()));
176        atts.addCDATAAttribute("zoneItemId", String.valueOf(zoneItem.getId()));
177        atts.addCDATAAttribute("nbMaxUser", String.valueOf(nbMaxUser));
178        atts.addCDATAAttribute("nbMax", String.valueOf(nbMaxFeedFullSaxed));
179        
180        Page page = (Page) request.getAttribute(Page.class.getName());
181        atts.addCDATAAttribute("showForm", String.valueOf(_confAccess.showPreferenceForm(page, nbMaxUser, listFeeds.size(), nbMaxFeedFullSaxed)));
182        
183        XMLUtils.startElement(contentHandler, "rssFeeds", atts);
184        
185        _saxFeeds(nbMaxUser, listFeeds, urlList, nbMaxFeedFullSaxed, defaultLength, defaultLifeTime, serviceParameters, userSelectedFeedsId);
186        _saxFeedsConfig(nbMaxUser, listFeeds, urlList, userSelectedFeedsId);
187        
188        XMLUtils.endElement(contentHandler, "rssFeeds");
189    }
190    
191    private void _saxFeedsConfig(long nbMaxUser, List<ModelAwareRepeaterEntry> listFeeds, List<String> urlList, List<String> userSelectedFeedsId) throws SAXException
192    {
193        for (ModelAwareRepeaterEntry rssFeed : listFeeds)
194        {
195            _saxInfoFeed(rssFeed.getValue("url"), rssFeed.getValue("title"), rssFeed.getValue("id"), false, userSelectedFeedsId.contains(rssFeed.getValue("id")));
196        }
197        
198        for (int i = 1; i <= nbMaxUser; i++)
199        {
200            boolean isSelected = userSelectedFeedsId.contains("feed-url" + i) || userSelectedFeedsId.contains(String.valueOf(i + listFeeds.size()));
201            if (i <= urlList.size())
202            {
203                String url  = urlList.get(i - 1);
204                _saxInfoFeed(url, "", "feed-url" + i, true, isSelected);
205            }
206            else
207            {
208                _saxInfoFeed("", "", "feed-url" + i, true, isSelected);
209            }
210        }
211    }
212    
213    private void _saxFeeds(int nbMaxUser, List<ModelAwareRepeaterEntry> listFeeds, List<String> urlList, long nbMaxFeedFullSaxed, long defaultLength, String defaultLifeTime, ModelAwareDataHolder serviceParameters,  List<String> userSelectedFeedsId)
214    {
215        int nbFeedFullSaxed = 0;
216        for (ModelAwareRepeaterEntry rssFeed : listFeeds)
217        {
218            boolean isSelected = userSelectedFeedsId.contains(rssFeed.getValue("id"));
219            if ((nbFeedFullSaxed < nbMaxFeedFullSaxed || nbMaxFeedFullSaxed == 0)
220                    && (userSelectedFeedsId.isEmpty() || isSelected))
221            {
222                nbFeedFullSaxed++;
223                _saxFullInfoFeed(rssFeed, isSelected, defaultLength, defaultLifeTime); // Sax all informations
224            }
225        }
226        
227        for (int i = 1; i <= nbMaxUser; i++)
228        {
229            if (i <= urlList.size())
230            {
231                String url = urlList.get(i - 1);
232                boolean isSelected = userSelectedFeedsId.contains("feed-url" + i) || userSelectedFeedsId.contains(String.valueOf(i + listFeeds.size()));
233                if ((nbFeedFullSaxed < nbMaxFeedFullSaxed || nbMaxFeedFullSaxed == 0)
234                        && (userSelectedFeedsId.isEmpty() || isSelected))
235                {
236                    nbFeedFullSaxed++;
237                    _saxFullInfoFeedCustom(serviceParameters, url, defaultLength, isSelected); // Sax all informations
238                }
239            }
240        }
241    }
242    
243    // Sax few info from the feed for the user preference form
244    private void _saxInfoFeed(String url, String name, String feedId, boolean isCustom, boolean isSelected) throws SAXException
245    {
246        AttributesImpl atts = new AttributesImpl();
247
248        atts.addCDATAAttribute("feedUrl", url);
249        atts.addCDATAAttribute("feedName", name);
250        atts.addCDATAAttribute("feedId", feedId);
251        atts.addCDATAAttribute("isCustom", String.valueOf(isCustom));
252        atts.addCDATAAttribute("isSelected", String.valueOf(isSelected));
253
254        XMLUtils.createElement(contentHandler, "feed-conf", atts);
255    }
256    
257    // Sax all info from the feed for the service (Feed from service parameters)
258    private void _saxFullInfoFeed(ModelAwareRepeaterEntry rssFeed, boolean isSelected, long defaultLength, String defaultLifeTime)
259    {
260        long nbItems = rssFeed.getValue("length", false, Long.valueOf(defaultLength));
261        String lifeTime = rssFeed.getValue("cache");
262        if (StringUtils.isBlank(lifeTime))
263        {
264            lifeTime = defaultLifeTime;
265        }
266        
267        String url = rssFeed.getValue("url");
268        String name = rssFeed.getValue("title");
269        
270        _saxFeed(nbItems, url, name, lifeTime, false, isSelected);
271    }
272    
273    // Sax all info from the feed for the service (Feed from user preference)
274    private void _saxFullInfoFeedCustom(ModelAwareDataHolder serviceParameters, String url, long defaultLength, boolean isSelected)
275    {
276        String lifeTime = serviceParameters.getValue("cache");
277        _saxFeed(defaultLength, url, "", lifeTime, true, isSelected);
278    }
279    
280    // Call the pipeline to sax all information from a feed
281    private void _saxFeed(long length, String url, String name, String lifeTime, Boolean isCustom, Boolean isSelected)
282    {
283        SitemapSource sitemapSource = null;
284        try
285        {
286            HashMap<String, Object> parameter = new HashMap<>();
287            parameter.put("length", length);
288            parameter.put("url", url);
289            parameter.put("name", name);
290            parameter.put("cache", _lifeTimes.get(lifeTime));
291            parameter.put("isCustom", isCustom);
292            parameter.put("isSelected", isSelected);
293            
294            sitemapSource = (SitemapSource) _resolver.resolveURI("cocoon:/feed", null, parameter);
295            sitemapSource.toSAX(new IgnoreRootHandler(contentHandler));
296        }
297        catch (Exception e)
298        {
299            getLogger().error("There is an error in the flux : '" + url + "'");
300        }
301        finally
302        {
303            _resolver.release(sitemapSource);
304        }
305    }
306    
307    // Get the list of url from the user preferences
308    private List<String> _getListUrl(UserIdentity user, String storageContext, Map<String, String> contextVars) throws UserPreferencesException, UnsupportedEncodingException
309    {
310        if (user != null)
311        {
312            String urls = _userPrefManager.getUserPreferenceAsString(user, storageContext, contextVars, USER_PREF_RSS_URL_KEY);
313            
314            List<String> urlAsList = new ArrayList<>();
315            if (StringUtils.isNotEmpty(urls))
316            {
317                String[] urlsAsArray = StringUtils.split(urls, ",");
318                for (String url : urlsAsArray)
319                {
320                    urlAsList.add(URLDecoder.decode(url, "utf-8"));
321                }
322            }
323              
324            return urlAsList;
325        }
326        
327        return new ArrayList<>();
328        
329    }
330    
331    // Get the list of chosen feed (unique id) from the user preferences
332    private List<String> _getListId(UserIdentity user, String storageContext, Map<String, String> contextVars) throws UserPreferencesException
333    {
334        if (user != null)
335        {
336            String ids = _userPrefManager.getUserPreferenceAsString(user, storageContext, contextVars, USER_PREF_RSS_ID_KEY);
337            if (StringUtils.isNotEmpty(ids))
338            {
339                String[] positionsAsArray = ids.split(",");
340                return Arrays.asList(positionsAsArray);
341            }
342        }
343        
344        return new ArrayList<>();
345    }
346    
347    // Check if the user can access to the feed
348    private boolean _checkUserAccessRSS(ModelAwareRepeaterEntry rssFeed)
349    {
350        UserIdentity user = _currentUserProvider.getUser();
351        
352        Boolean limited = rssFeed.getValue("limited");
353        if (limited)
354        {
355            // Return false if no user is logged
356            return user != null;
357        }
358        else 
359        {
360            String[] foGroupsStr = rssFeed.getValue("fo-group", false, ArrayUtils.EMPTY_STRING_ARRAY);
361            Set<GroupIdentity> foGroups = Arrays.asList(foGroupsStr).stream()
362                    .filter(identityAsStr -> identityAsStr.length() > 0)
363                    .map(identityAsStr -> _jsonUtils.convertJsonToMap(identityAsStr))
364                    .map(identityAsMap -> new GroupIdentity((String) identityAsMap.get("groupId"), (String) identityAsMap.get("groupDirectory")))
365                    .collect(Collectors.toSet());
366            
367            UserIdentity[] foUsersArray = rssFeed.getValue("fo-user", false, new UserIdentity[0]);
368            Set<UserIdentity> foUsers = Arrays.asList(foUsersArray)
369                                              .stream()
370                                              .collect(Collectors.toSet());
371            
372            if (CollectionUtils.isEmpty(foUsers) && CollectionUtils.isEmpty(foGroups))
373            {
374                return true;
375            }
376            
377            if (user == null)
378            {
379                return false;
380            }
381            
382            if (foUsers.contains(user))
383            {
384                return true;
385            }
386            
387            for (GroupIdentity foGroup : foGroups)
388            {
389                if (foGroup != null)
390                {
391                    Group group = _groupManager.getGroup(foGroup);
392                    if (group != null && group.getUsers().contains(user))
393                    {
394                        return true;
395                    }
396                }
397            }
398            
399            return false;
400        }
401    }
402    
403    private Map<String, String> _getContextVars(String siteName, String language)
404    {
405        Map<String, String> contextVars = new HashMap<>();
406        
407        contextVars.put(FOUserPreferencesConstants.CONTEXT_VAR_SITENAME, siteName);
408        contextVars.put(FOUserPreferencesConstants.CONTEXT_VAR_LANGUAGE, language);
409        
410        return contextVars;
411    }
412}
413
414