001/*
002 *  Copyright 2010 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.web.filter;
017
018import java.io.IOException;
019import java.util.ArrayList;
020import java.util.Iterator;
021import java.util.List;
022import java.util.Map;
023
024import org.apache.avalon.framework.component.Component;
025import org.apache.avalon.framework.service.ServiceException;
026import org.apache.avalon.framework.service.ServiceManager;
027import org.apache.avalon.framework.service.Serviceable;
028import org.apache.cocoon.components.source.impl.SitemapSource;
029import org.apache.cocoon.xml.AttributesImpl;
030import org.apache.cocoon.xml.XMLUtils;
031import org.apache.excalibur.source.SourceResolver;
032import org.slf4j.Logger;
033import org.slf4j.LoggerFactory;
034import org.xml.sax.ContentHandler;
035import org.xml.sax.SAXException;
036
037import org.ametys.cms.content.ContentHelper;
038import org.ametys.cms.filter.ContentFilter;
039import org.ametys.cms.repository.Content;
040import org.ametys.core.user.CurrentUserProvider;
041import org.ametys.core.user.UserIdentity;
042import org.ametys.core.util.DateUtils;
043import org.ametys.core.util.IgnoreRootHandler;
044import org.ametys.plugins.repository.AmetysObjectIterable;
045import org.ametys.web.filter.WebContentFilter.AccessLimitation;
046import org.ametys.web.pageaccess.ContentAccessManager;
047import org.ametys.web.pageaccess.ContentAccessManager.ContentAccess;
048import org.ametys.web.renderingcontext.RenderingContextHandler;
049import org.ametys.web.repository.content.WebContent;
050import org.ametys.web.repository.page.Page;
051
052/**
053 * Component helper for manipulating {@link ContentFilter}
054 */
055public class ContentFilterHelper implements Component, Serviceable
056{
057    /** The Avalon Role */
058    public static final String ROLE = ContentFilterHelper.class.getName();
059    
060    /** Logger */
061    protected static final Logger __LOGGER = LoggerFactory.getLogger(ContentFilterHelper.class);
062    
063    /** The content access manager. */
064    protected ContentAccessManager _contentAccessManager;
065    
066    /** The rendering context handler. */
067    protected RenderingContextHandler _renderingContentHandler;
068    
069    /** The current user provider */
070    protected CurrentUserProvider _currentUserProvider;
071
072    /** The content helper */
073    protected ContentHelper _contentHelper;
074    
075    private SourceResolver _resolver;
076
077    
078    @Override
079    public void service(ServiceManager manager) throws ServiceException
080    {
081        _resolver = (SourceResolver) manager.lookup(SourceResolver.ROLE);
082        _contentAccessManager = (ContentAccessManager) manager.lookup(ContentAccessManager.ROLE);
083        _renderingContentHandler = (RenderingContextHandler) manager.lookup(RenderingContextHandler.ROLE);
084        _currentUserProvider = (CurrentUserProvider) manager.lookup(CurrentUserProvider.ROLE);
085        _contentHelper = (ContentHelper) manager.lookup(ContentHelper.ROLE);
086    }
087    
088    /**
089     * SAX all contents matching the given filter
090     * @param handler The content handler to SAX into
091     * @param filter The filter
092     * @throws SAXException If an error occurs while SAXing
093     * @throws IOException If an error occurs while retrieving content.
094     */
095    public void saxMatchingContents (ContentHandler handler, ContentFilter filter) throws SAXException, IOException
096    {
097        AmetysObjectIterable<Content> contents = filter.getMatchingContents();
098        Iterator<Content> it = contents.iterator();
099        
100        int index = 0;
101        while (it.hasNext() && index < filter.getLength())
102        {
103            Content content = it.next();
104            saxContent(handler, content, filter.getView(), false);
105            index++;
106        }
107    }
108    
109    /**
110     * SAX all contents matching the given filter
111     * @param handler The content handler to SAX into
112     * @param filter The filter
113     * @param siteName The current site name. Can be null.
114     * @param lang The current language. Can be null.
115     * @param page The current page. Can be null.
116     * @throws SAXException If an error occurs while SAXing
117     * @throws IOException If an error occurs while retrieving content.
118     */
119    public void saxMatchingContents (ContentHandler handler, WebContentFilter filter, String siteName, String lang, Page page) throws SAXException, IOException
120    {
121        long t0 = System.currentTimeMillis();
122        
123        boolean checkUserAccess = filter.getAccessLimitation() == AccessLimitation.USER_ACCESS;
124        
125        AmetysObjectIterable<Content> contents = filter.getMatchingContents(siteName, lang, page);
126        Iterator<Content> it = contents.iterator();
127        
128        if (__LOGGER.isDebugEnabled())
129        {
130            long t1 = System.currentTimeMillis();
131            __LOGGER.debug("get matching content processing time " + (t1 - t0) + " ms");
132        }
133        
134        int index = 0;
135        while (it.hasNext() && index < filter.getLength())
136        {
137            long t00 = System.currentTimeMillis();
138
139            Content content = it.next();
140
141            if (isContentValid(content, page, filter))
142            {
143                saxContent(handler, content, filter.getView(), checkUserAccess);
144                index++;
145            }
146            
147            long t01 = System.currentTimeMillis();
148            if (__LOGGER.isDebugEnabled())
149            {
150                __LOGGER.debug("saxing content " + index + " processing time " + (t01 - t00) + " ms");
151            }
152        }
153
154        if (__LOGGER.isDebugEnabled())
155        {
156            long t2 = System.currentTimeMillis();
157            __LOGGER.debug("saxing all contents processing time " + (t2 - t0) + " ms");
158        }
159    }
160    
161    /**
162     * Get the ID of all contents matching the given filter.
163     * This method filters orphan contents and limits the results if necessary.
164     * @param filter The filter
165     * @param siteName The current site name. Can be null.
166     * @param lang The current language. Can be null.
167     * @param page The current page. Can be null.
168     * @return a List of all content IDs.
169     */
170    public List<String> getMatchingContentIds(WebContentFilter filter, String siteName, String lang, Page page)
171    {
172        List<String> contentIds = new ArrayList<>();
173        
174        long t0 = System.currentTimeMillis();
175        
176        if (__LOGGER.isDebugEnabled())
177        {
178            long t1 = System.currentTimeMillis();
179            __LOGGER.debug("get matching content processing time " + (t1 - t0) + " ms");
180        }
181        
182        try (AmetysObjectIterable<Content> contents = filter.getMatchingContents(siteName, lang, page);)
183        {
184            Iterator<Content> it = contents.iterator();
185            
186            int index = 0;
187            while (it.hasNext() && index < filter.getLength())
188            {
189                long t00 = System.currentTimeMillis();
190
191                Content content = it.next();
192                if (isContentValid(content, page, filter))
193                {
194                    contentIds.add(content.getId());
195                    index++;
196                }
197
198                if (__LOGGER.isDebugEnabled())
199                {
200                    long t01 = System.currentTimeMillis();
201                    __LOGGER.debug("get content " + index + " processing time " + (t01 - t00) + " ms");
202                }
203            }
204            
205            return contentIds;
206        }
207        finally
208        {
209            if (__LOGGER.isDebugEnabled())
210            {
211                long t2 = System.currentTimeMillis();
212                __LOGGER.debug("get all contents processing time " + (t2 - t0) + " ms");
213            }
214        }
215    }
216    
217    /**
218     * SAX a content in its specific view
219     * @param handler The content handler to SAX into
220     * @param content The content to SAX
221     * @param viewName The view to use
222     * @param checkUserAccess Set to true to check FO user access when rendering content
223     * @throws SAXException If an error occurs while SAXing
224     * @throws IOException If an error occurs while retrieving content.
225     */
226    public void saxContent (ContentHandler handler, Content content, String viewName, boolean checkUserAccess) throws SAXException, IOException
227    {
228        AttributesImpl attrs = new AttributesImpl();
229        attrs.addCDATAAttribute("id", content.getId());
230        attrs.addCDATAAttribute("name", content.getName());
231        attrs.addCDATAAttribute("title", content.getTitle(null));
232        attrs.addCDATAAttribute("lastModifiedAt", DateUtils.zonedDateTimeToString(content.getLastModified()));
233        
234        XMLUtils.startElement(handler, "content", attrs);
235        
236        String uri = _contentHelper.getContentHtmlViewUrl(content, viewName, Map.of("checkUserAccess", String.valueOf(checkUserAccess)));
237        SitemapSource src = null;
238        
239        try
240        {
241            src = (SitemapSource) _resolver.resolveURI(uri);
242            src.toSAX(new IgnoreRootHandler(handler));
243        }
244        finally
245        {
246            _resolver.release(src);
247        }
248        
249        XMLUtils.endElement(handler, "content");
250    }
251    
252    /**
253     * Test if the content is accessible in accordance with the filter options (mask orphans, access limitation policy, ...)
254     * @param content the content to test.
255     * @param currentPage the current page.
256     * @param filter the content filter.
257     * @return true if the content is accessible, false otherwise.
258     */
259    public boolean isContentValid(Content content, Page currentPage, ContentFilter filter)
260    {
261        if (filter instanceof WebContentFilter)
262        {
263            WebContentFilter webFilter = (WebContentFilter) filter;
264            
265            boolean maskOrphan = webFilter.maskOrphanContents();
266            
267            if (!maskOrphan
268                || maskOrphan && !isOrphan(content))
269            {
270                return isAccessible(content, currentPage, webFilter);
271            }
272            
273            return false;
274        }
275        
276        return true;
277    }
278    
279    /**
280     * Test if the content is accessible in accordance with the filter's access limitation policy.
281     * @param content the content to test.
282     * @param currentPage the current page.
283     * @param filter the web content filter.
284     * @return true if the content is accessible, false otherwise.
285     */
286    protected boolean isAccessible(Content content, Page currentPage, WebContentFilter filter)
287    {
288        if (content instanceof WebContent)
289        {
290            WebContent webContent = (WebContent) content;
291            
292            AccessLimitation accessLimitation = filter.getAccessLimitation();
293            UserIdentity user = _currentUserProvider.getUser();
294            
295            if (accessLimitation == AccessLimitation.PAGE_ACCESS)
296            {
297                if (currentPage == null)
298                {
299                    // The current page is null when coming from a static filter
300                    // Test if the content is displayed in at least one public page (anonymous access)
301                    return _contentAccessManager.getAccess(webContent, null, false) == ContentAccess.UNRESTRICTED;
302                }
303                else
304                {
305                    // Test if the content is displayed in a page having the same access rights as the current page.
306                    return _contentAccessManager.isAccessibleByPage(webContent, currentPage);
307                }
308            }
309            else if (accessLimitation == AccessLimitation.USER_ACCESS)
310            {
311                // Test if the content is displayed in a page accessible by the current user.
312                ContentAccess access = _contentAccessManager.getAccess(webContent, user);
313                return access == ContentAccess.ALLOWED || access == ContentAccess.UNRESTRICTED;
314            }
315        }
316        
317        return true;
318    }
319    
320    /**
321     * Determines if a content is orphan
322     * @param content the content to test
323     * @return true if the content is orphan
324     */
325    protected boolean isOrphan (Content content)
326    {
327        if (content instanceof WebContent)
328        {
329            return ((WebContent) content).getReferencingPages().isEmpty();
330        }
331        return true;
332    }
333    
334}