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