001/*
002 *  Copyright 2018 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.ugc.clientsideelement;
017
018import java.util.ArrayList;
019import java.util.Arrays;
020import java.util.HashMap;
021import java.util.List;
022import java.util.Map;
023import java.util.Set;
024import java.util.stream.Collectors;
025
026import javax.jcr.Node;
027import javax.jcr.RepositoryException;
028import javax.jcr.Value;
029
030import org.apache.avalon.framework.service.ServiceException;
031import org.apache.avalon.framework.service.ServiceManager;
032import org.apache.commons.lang3.StringUtils;
033import org.apache.jackrabbit.value.StringValue;
034
035import org.ametys.cms.contenttype.ContentType;
036import org.ametys.cms.contenttype.ContentTypeExtensionPoint;
037import org.ametys.core.observation.Event;
038import org.ametys.core.observation.ObservationManager;
039import org.ametys.core.ui.Callable;
040import org.ametys.core.util.LambdaUtils;
041import org.ametys.plugins.repository.AmetysObjectResolver;
042import org.ametys.plugins.repository.jcr.JCRAmetysObject;
043import org.ametys.plugins.ugc.observation.ObservationConstants;
044import org.ametys.plugins.ugc.page.UGCPageHandler;
045import org.ametys.plugins.ugc.page.VirtualUGCPageFactory;
046import org.ametys.runtime.i18n.I18nizableText;
047import org.ametys.runtime.i18n.I18nizableTextParameter;
048import org.ametys.web.clientsideelement.AbstractPageClientSideElement;
049import org.ametys.web.repository.page.LockablePage;
050import org.ametys.web.repository.page.ModifiablePage;
051import org.ametys.web.repository.page.Page;
052
053/**
054 * Client side element for a controller which set/remove the root page of a ugc.
055 */
056public class SetUGCRootClientSideElement extends AbstractPageClientSideElement
057{
058    /** Observer manager. */
059    protected ObservationManager _observationManager;
060    /** The extension point for content types */
061    protected ContentTypeExtensionPoint _contentTypeEP;
062    /** The UGC page handler */
063    protected UGCPageHandler _ugcPageHandler;
064
065    @Override
066    public void service(ServiceManager smanager) throws ServiceException
067    {
068        super.service(smanager);
069        _observationManager = (ObservationManager) smanager.lookup(ObservationManager.ROLE);
070        _contentTypeEP = (ContentTypeExtensionPoint) smanager.lookup(ContentTypeExtensionPoint.ROLE);
071        _ugcPageHandler = (UGCPageHandler) smanager.lookup(UGCPageHandler.ROLE);
072        
073    }
074    
075    /**
076     * Gets the status of the given page
077     * @param pageId The page id
078     * @return the status of the given page
079     */
080    @Callable (rights = Callable.SKIP_BUILTIN_CHECK)
081    public Map<String, Object> getStatus(String pageId)
082    {
083        Map<String, Object> result = new HashMap<>();
084        
085        Map<String, Object> parameters = this._script.getParameters();
086        
087        Page page = _resolver.resolveById(pageId);
088        
089        if (!hasRight(getRights(Map.of())))
090        {
091            throw new IllegalStateException("User " + _currentUserProvider.getUser() + " try to access privileges feature without sufficient rights");
092        }
093        
094        if (page instanceof JCRAmetysObject)
095        {
096            if (_isUGCRootPage((JCRAmetysObject) page))
097            {
098                List<String> i18nParameters = new ArrayList<>();
099                i18nParameters.add(page.getTitle());
100    
101                I18nizableText ed = (I18nizableText) parameters.get("ugc-page-description");
102                I18nizableText msg = new I18nizableText(ed.getCatalogue(), ed.getKey(), i18nParameters);
103                result.put("ugc-page-title", msg);
104                
105                ed = (I18nizableText) parameters.get("remove-ugc-page-description");
106                msg = new I18nizableText(ed.getCatalogue(), ed.getKey(), i18nParameters);
107                result.put("remove-ugc-page-title", msg);
108                
109                String contentTypeId = page.getValue(UGCPageHandler.CONTENT_TYPE_DATA_NAME, StringUtils.EMPTY);
110                if (StringUtils.isNotEmpty(contentTypeId))
111                {
112                    I18nizableText contentTypeText = _contentTypeEP.hasExtension(contentTypeId) ? _contentTypeEP.getExtension(contentTypeId).getLabel() : new I18nizableText(contentTypeId);
113                    
114                    Map<String, I18nizableTextParameter> contentTypeI18nParameters = new HashMap<>();
115                    contentTypeI18nParameters.put("0", contentTypeText);
116                    
117                    ed = (I18nizableText) parameters.get("contenttype-ugc-page-description");
118                    msg = new I18nizableText(ed.getCatalogue(), ed.getKey(), contentTypeI18nParameters);
119                    result.put("contenttype-ugc-page-description", msg);
120                }
121                
122                result.put("ugc-page-id", new I18nizableText(page.getId()));
123            }
124            else if (!_ugcPageHandler.getUGCRootPages(page.getSiteName(), page.getSitemapName()).isEmpty())
125            {
126                I18nizableText ed = (I18nizableText) parameters.get("ugc-page-already-exist");
127                I18nizableText msg = new I18nizableText(ed.getCatalogue(), ed.getKey());
128                result.put("ugc-page-already-exist", msg);
129            }
130            else
131            {
132                List<String> i18nParameters = new ArrayList<>();
133                i18nParameters.add(page.getTitle());
134    
135                I18nizableText ed = (I18nizableText) parameters.get("add-ugc-page-description");
136                I18nizableText msg = new I18nizableText(ed.getCatalogue(), ed.getKey(), i18nParameters);
137                
138                result.put("add-ugc-page-id", new I18nizableText(page.getId()));
139                result.put("add-ugc-page-title", msg);
140            }
141        }
142        else
143        {
144            List<String> noJcrI18nParameters = new ArrayList<>();
145            noJcrI18nParameters.add(page.getTitle());
146
147            I18nizableText ed = (I18nizableText) parameters.get("no-jcr-page-description");
148            I18nizableText msg = new I18nizableText(ed.getCatalogue(), ed.getKey(), noJcrI18nParameters);
149            
150            result.put("no-jcr-page-id", new I18nizableText(page.getId()));
151            result.put("no-jcr-page-title", msg);
152        }
153        
154        return result;
155    }
156    
157    /**
158     * Sets the given page as the root of a ugc
159     * @param pageId The id of the page
160     * @param contentTypeId The id of the content type
161     * @param attributePath The classification attribute path
162     * @param classificationPageVisible the visibility of transitional pages
163     * @return A result map
164     * @throws RepositoryException if a repository error occurred
165     */
166    @Callable (rights = Callable.SKIP_BUILTIN_CHECK)
167    public Map<String, Object> setUGCRoot(String pageId, String contentTypeId, String attributePath, boolean classificationPageVisible) throws RepositoryException
168    {
169        Map<String, Object> result = new HashMap<>();
170        
171        Page page = _resolver.resolveById(pageId);
172        
173        if (!hasRight(page))
174        {
175            throw new IllegalStateException("User " + _currentUserProvider.getUser() + " try to access privileges feature without sufficient rights");
176        }
177        
178        if (page instanceof LockablePage lockablePage && lockablePage.isLocked())
179        {
180            throw new IllegalStateException("Cannot set the locked page '/" + page.getSitemapName() + "/" + page.getPathInSitemap() + "' as UGC root");
181        }
182        
183        ContentType contentType = _contentTypeEP.getExtension(contentTypeId);
184        if (contentType == null)
185        {
186            result.put("error", "wrong-content-type");
187            return result;
188        }
189        else if (StringUtils.isNotBlank(attributePath))
190        {
191            if (!contentType.hasModelItem(attributePath))
192            {
193                result.put("error", "wrong-metadata");
194                return result;
195            }
196        }
197        
198        Page currentUGCPage = _ugcPageHandler.getUGCRootPage(page.getSiteName(), page.getSitemapName(), contentTypeId);
199        
200        Map<String, Object> eventParams = new HashMap<>();
201        eventParams.put(org.ametys.web.ObservationConstants.ARGS_PAGE, page);
202        
203        if (currentUGCPage != null && currentUGCPage.getId().equals(pageId))
204        {
205            // Unindex pages for all workspaces before the properties changed 
206            _observationManager.notify(new Event(ObservationConstants.EVENT_UGC_ROOT_UPDATING, _currentUserProvider.getUser(), eventParams));
207            
208            _updateUGCRootProperty(page, contentTypeId, attributePath, classificationPageVisible);
209        }
210        else
211        {
212            _addUGCRootProperty(page, contentTypeId, attributePath, classificationPageVisible);
213        }
214        
215        // Live synchronization
216        _notifyPageUpdated(page);
217        
218        // Indexation
219        _observationManager.notify(new Event(ObservationConstants.EVENT_UGC_ROOT_UPDATED, _currentUserProvider.getUser(), eventParams));
220        
221        return result;
222    }
223    
224    /**
225     * Remove the ugc root status to the given page
226     * @param pageId The id of the page
227     * @return A result map
228     * @throws RepositoryException if a repository error occured
229     */
230    @Callable (rights = Callable.SKIP_BUILTIN_CHECK)
231    public Map<String, Object> removeUGCRoot(String pageId) throws RepositoryException
232    {
233        Map<String, Object> result = new HashMap<>();
234        
235        Page page = _resolver.resolveById(pageId);
236        
237        if (page instanceof JCRAmetysObject)
238        {
239            if (!hasRight(page))
240            {
241                throw new IllegalStateException("User '" + _currentUserProvider.getUser() + "' tried to access a privileged feature without convenient right");
242            }
243            
244            if (page instanceof LockablePage lockablePage && lockablePage.isLocked())
245            {
246                throw new IllegalStateException("Cannot unset the locked page '/" + page.getSitemapName() + "/" + page.getPathInSitemap() + "' as UGC root");
247            }
248
249            if (!_isUGCRootPage((JCRAmetysObject) page))
250            {
251                result.put("error", "no-root");
252                return result;
253            }
254            
255            Map<String, Object> eventParams = new HashMap<>();
256            eventParams.put(org.ametys.web.ObservationConstants.ARGS_PAGE, page);
257            
258            // Unindex pages for all workspaces before the properties were removed 
259            _observationManager.notify(new Event(ObservationConstants.EVENT_UGC_ROOT_DELETING, _currentUserProvider.getUser(), eventParams));
260            
261            _removeUGCRootProperty(page);
262            
263            _notifyPageUpdated(page);
264            
265            // After live synchronization
266            _observationManager.notify(new Event(ObservationConstants.EVENT_UGC_ROOT_DELETED, _currentUserProvider.getUser(), eventParams));
267        }
268        else
269        {
270            result.put("error", "no-root");
271        }
272        return result;
273    }
274    
275    /**
276     * Gets information about ugc root status on the given.
277     * @param pageId The id of the page
278     * @return information about ugc root status on the given.
279     */
280    @Callable (rights = Callable.SKIP_BUILTIN_CHECK)
281    public Map<String, Object> getRootPageInfo(String pageId)
282    {
283        Map<String, Object> result = new HashMap<>();
284        
285        Page page = _resolver.resolveById(pageId);
286        
287        if (!hasRight(page))
288        {
289            throw new IllegalStateException("User " + _currentUserProvider.getUser() + " try to access privileges feature without sufficient rights");
290        }
291        
292        Set<Page> currentUGCPages = _ugcPageHandler.getUGCRootPages(page.getSiteName(), page.getSitemapName());
293        
294        if (currentUGCPages.contains(page))
295        {
296            result.put("isRoot", true);
297            result.put("contentType", page.getValue(UGCPageHandler.CONTENT_TYPE_DATA_NAME));
298            result.put("metadata", page.getValue(UGCPageHandler.CLASSIFICATION_ATTRIBUTE_DATA_NAME));
299            result.put("is-visible", page.getValue(UGCPageHandler.CLASSIFICATION_PAGE_VISIBLE_DATA_NAME, false));
300        }
301        else
302        {
303            result.put("isRoot", false);
304        }
305        
306        return result;
307    }
308    
309    private void _addUGCRootProperty(Page page, String contentType, String metadata, boolean classificationPageVisible) throws RepositoryException
310    {
311        if (page instanceof JCRAmetysObject)
312        {
313            JCRAmetysObject jcrPage = (JCRAmetysObject) page;
314            Node node = jcrPage.getNode();
315            
316            List<Value> values = new ArrayList<>();
317            if (node.hasProperty(AmetysObjectResolver.VIRTUAL_PROPERTY))
318            {
319                values.addAll(Arrays.asList(node.getProperty(AmetysObjectResolver.VIRTUAL_PROPERTY).getValues()));
320            }
321            
322            StringValue virtualUGCPageFactoryClassName = new StringValue(VirtualUGCPageFactory.class.getName());
323            if (!values.contains(virtualUGCPageFactoryClassName))
324            {
325                values.add(virtualUGCPageFactoryClassName);
326            }
327            
328            node.setProperty(AmetysObjectResolver.VIRTUAL_PROPERTY, values.toArray(new Value[values.size()]));
329
330            // Set the ugc root property
331            if (page instanceof ModifiablePage)
332            {
333                ((ModifiablePage) page).setValue(UGCPageHandler.CONTENT_TYPE_DATA_NAME, contentType);
334                ((ModifiablePage) page).setValue(UGCPageHandler.CLASSIFICATION_ATTRIBUTE_DATA_NAME, metadata);
335                ((ModifiablePage) page).setValue(UGCPageHandler.CLASSIFICATION_PAGE_VISIBLE_DATA_NAME, classificationPageVisible);
336            }
337            
338            jcrPage.saveChanges();
339        }
340    }
341    
342    private void _updateUGCRootProperty(Page page, String contentType, String metadata, boolean classificationPageVisible)
343    {
344        if (page instanceof ModifiablePage)
345        {
346            ModifiablePage modifiablePage = (ModifiablePage) page;
347            
348            // Set the ugc root property
349            modifiablePage.setValue(UGCPageHandler.CONTENT_TYPE_DATA_NAME, contentType);
350            modifiablePage.setValue(UGCPageHandler.CLASSIFICATION_ATTRIBUTE_DATA_NAME, metadata);
351            modifiablePage.setValue(UGCPageHandler.CLASSIFICATION_PAGE_VISIBLE_DATA_NAME, classificationPageVisible);
352            
353            modifiablePage.saveChanges();
354        }
355    }
356    
357    private void _removeUGCRootProperty(Page page) throws RepositoryException
358    {
359        if (page instanceof JCRAmetysObject)
360        {
361            JCRAmetysObject jcrPage = (JCRAmetysObject) page;
362            Node node = jcrPage.getNode();
363            
364            if (node.hasProperty(AmetysObjectResolver.VIRTUAL_PROPERTY))
365            {
366                List<Value> values = new ArrayList<>(Arrays.asList(node.getProperty(AmetysObjectResolver.VIRTUAL_PROPERTY).getValues()));
367                int index = values.stream()
368                        .map(LambdaUtils.wrap(Value::getString))
369                        .collect(Collectors.toList())
370                        .indexOf(VirtualUGCPageFactory.class.getName());
371                if (index != -1)
372                {
373                    values.remove(index);
374                    node.setProperty(AmetysObjectResolver.VIRTUAL_PROPERTY, values.toArray(new Value[values.size()]));
375                }
376
377                // Remove the ugc root property
378                if (page instanceof ModifiablePage)
379                {
380                    ModifiablePage modifiablePage = (ModifiablePage) page;
381                    modifiablePage.removeValue(UGCPageHandler.CONTENT_TYPE_DATA_NAME);
382                    modifiablePage.removeValue(UGCPageHandler.CLASSIFICATION_ATTRIBUTE_DATA_NAME);
383                    modifiablePage.removeValue(UGCPageHandler.CLASSIFICATION_PAGE_VISIBLE_DATA_NAME);
384                }
385                
386                jcrPage.saveChanges();
387            }
388        }
389    }
390    
391    private boolean _isUGCRootPage (JCRAmetysObject jcrPage)
392    {
393        try
394        {
395            Node node = jcrPage.getNode();
396            
397            if (node.hasProperty(AmetysObjectResolver.VIRTUAL_PROPERTY))
398            {
399                List<Value> values = Arrays.asList(node.getProperty(AmetysObjectResolver.VIRTUAL_PROPERTY).getValues());
400                
401                return values.stream()
402                        .map(LambdaUtils.wrap(Value::getString))
403                        .anyMatch(v -> VirtualUGCPageFactory.class.getName().equals(v));
404            }
405            else
406            {
407                return false;
408            }
409        }
410        catch (RepositoryException e)
411        {
412            return false;
413        }
414    }
415    
416    private void _notifyPageUpdated(Page page)
417    {
418        Map<String, Object> eventParams = new HashMap<>();
419        eventParams.put(org.ametys.web.ObservationConstants.ARGS_PAGE, page);
420        _observationManager.notify(new Event(org.ametys.web.ObservationConstants.EVENT_PAGE_UPDATED, _currentUserProvider.getUser(), eventParams));
421    }
422}