001/*
002 *  Copyright 2012 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.usermanagement;
017
018import java.io.IOException;
019import java.net.MalformedURLException;
020import java.util.Arrays;
021import java.util.Collection;
022
023import javax.jcr.Repository;
024import javax.jcr.RepositoryException;
025import javax.jcr.Session;
026
027import org.apache.avalon.framework.service.ServiceException;
028import org.apache.avalon.framework.service.ServiceManager;
029import org.apache.cocoon.ProcessingException;
030import org.apache.cocoon.components.source.impl.SitemapSource;
031import org.apache.cocoon.environment.ObjectModelHelper;
032import org.apache.cocoon.environment.Request;
033import org.apache.cocoon.generation.ServiceableGenerator;
034import org.apache.cocoon.xml.AttributesImpl;
035import org.apache.cocoon.xml.XMLUtils;
036import org.apache.commons.lang.StringUtils;
037import org.apache.excalibur.source.SourceResolver;
038import org.xml.sax.SAXException;
039
040import org.ametys.cms.CmsConstants;
041import org.ametys.cms.content.ContentHelper;
042import org.ametys.cms.repository.Content;
043import org.ametys.core.right.RightManager;
044import org.ametys.core.user.CurrentUserProvider;
045import org.ametys.core.user.UserIdentity;
046import org.ametys.core.user.directory.UserDirectory;
047import org.ametys.core.util.DateUtils;
048import org.ametys.core.util.IgnoreRootHandler;
049import org.ametys.plugins.core.impl.user.directory.JdbcUserDirectory;
050import org.ametys.plugins.repository.AmetysObjectResolver;
051import org.ametys.plugins.repository.UnknownAmetysObjectException;
052import org.ametys.plugins.repository.version.VersionableAmetysObject;
053import org.ametys.runtime.i18n.I18nizableText;
054import org.ametys.web.WebConstants;
055import org.ametys.web.cache.PageHelper;
056import org.ametys.web.repository.page.Page;
057import org.ametys.web.repository.page.ZoneItem;
058import org.ametys.web.repository.site.SiteManager;
059import org.ametys.web.skin.Skin;
060import org.ametys.web.skin.SkinsManager;
061import org.ametys.web.synchronization.SynchronizeComponent;
062import org.ametys.web.usermanagement.UserSignupManager.SignupType;
063
064import com.google.common.collect.Multimap;
065
066/**
067 * Generate information to render the user signup service.
068 */
069public class UserSignupGenerator extends ServiceableGenerator
070{
071    /** The user signup manager. */
072    protected UserSignupManager _userSignupManager;
073    /** The site Manager. */
074    protected SiteManager _siteManager;
075    /** The ametys object resolver. */
076    protected AmetysObjectResolver _resolver;
077    /** The component for live synchronization */
078    protected SynchronizeComponent _synchronizeComponent;
079    /** The {@link SkinsManager}*/
080    protected SkinsManager _skinManager;
081    /** The repository */
082    protected Repository _repository;
083    /** Page helper */
084    protected PageHelper _pageHelper;
085    /** The content helper */
086    protected ContentHelper _contentHelper;
087    /** The current user provider */
088    protected CurrentUserProvider _currentUserProvider;
089    /** The source resolver */
090    protected SourceResolver _srcResolver;
091    /** Right manager */
092    protected RightManager _rightManager;
093    
094    
095    @Override
096    public void service(ServiceManager serviceManager) throws ServiceException
097    {
098        super.service(serviceManager);
099        _userSignupManager = (UserSignupManager) serviceManager.lookup(UserSignupManager.ROLE);
100        _rightManager = (RightManager) serviceManager.lookup(RightManager.ROLE);
101        _siteManager = (SiteManager) serviceManager.lookup(SiteManager.ROLE);
102        _srcResolver = (SourceResolver) serviceManager.lookup(SourceResolver.ROLE);
103        _resolver = (AmetysObjectResolver) serviceManager.lookup(AmetysObjectResolver.ROLE);
104        _skinManager = (SkinsManager) serviceManager.lookup(SkinsManager.ROLE);
105        _synchronizeComponent = (SynchronizeComponent) serviceManager.lookup(SynchronizeComponent.ROLE);
106        _repository = (Repository) serviceManager.lookup(Repository.class.getName());
107        _pageHelper = (PageHelper) serviceManager.lookup(PageHelper.ROLE);
108        _contentHelper = (ContentHelper) serviceManager.lookup(ContentHelper.ROLE);
109        _currentUserProvider = (CurrentUserProvider) serviceManager.lookup(CurrentUserProvider.ROLE);
110    }
111    
112    @Override
113    public void generate() throws IOException, SAXException, ProcessingException
114    {
115        Request request = ObjectModelHelper.getRequest(objectModel);
116        String siteName = (String) request.getAttribute(WebConstants.REQUEST_ATTR_SITE_NAME);
117        String language = (String) request.getAttribute(WebConstants.REQUEST_ATTR_SITEMAP_NAME);
118        Page page = (Page) request.getAttribute(Page.class.getName());
119        ZoneItem zoneItem = (ZoneItem) request.getAttribute(WebConstants.REQUEST_ATTR_ZONEITEM);
120        
121        SignupType signupType = _userSignupManager.getSignupType(siteName);
122        Page signupPage = _userSignupManager.getSignupPage(siteName, language);
123        Page pwdChangePage = _userSignupManager.getPwdChangePage(siteName, language);
124        
125        @SuppressWarnings("unchecked")
126        Multimap<String, I18nizableText> errors = (Multimap<String, I18nizableText>) request.getAttribute("errors");
127        String firstName = request.getParameter("firstname");
128        String lastName = request.getParameter("lastname");
129        String email = request.getParameter("email");
130        String token = request.getParameter("token");
131        
132        contentHandler.startDocument();
133        
134        AttributesImpl attrs = new AttributesImpl();
135        if (page != null)
136        {
137            _addNotEmptyAttribute (attrs, "current-page", page.getId());
138        }
139        
140        _addNotEmptyAttribute (attrs, "public-signup", String.valueOf(signupType == SignupType.PUBLIC));
141        
142        if (signupPage != null)
143        {
144            _addNotEmptyAttribute (attrs, "signup-page-id", signupPage.getId());
145        }
146        if (pwdChangePage != null)
147        {
148            _addNotEmptyAttribute (attrs, "password-change-page-id", pwdChangePage.getId());
149        }
150        
151        UserIdentity userIdentity = _currentUserProvider.getUser();
152        if (userIdentity != null)
153        {
154            _addNotEmptyAttribute (attrs, "fo-user-login", userIdentity.getLogin());
155            _addNotEmptyAttribute (attrs, "fo-user-population", userIdentity.getPopulationId());
156        }
157        
158        // Keep for compatibity purpose but already sax in #saxInputs
159        _addNotEmptyAttribute (attrs, "firstname", firstName);
160        _addNotEmptyAttribute (attrs, "lastname", lastName);
161        _addNotEmptyAttribute (attrs, "email", email);
162        _addNotEmptyAttribute (attrs, "token", token);
163        
164        XMLUtils.startElement(contentHandler, "user-signup", attrs);
165        
166        if (errors != null)
167        {
168            saxErrors(errors);
169        }
170        
171        Page tosPage = _userSignupManager.getGTUPage(zoneItem);
172        Page successPage = _userSignupManager.getSuccessPage(zoneItem);
173        Content tosContent = _userSignupManager.getGTUContent(zoneItem);
174        Content successContent = _userSignupManager.getSuccessContent(zoneItem);
175        
176        UserDirectory userDirectory = _userSignupManager.getUserDirectory(zoneItem);
177        if (userDirectory instanceof JdbcUserDirectory jdbcUserDirectory && jdbcUserDirectory.useStrongPassword())
178        {
179            jdbcUserDirectory.getStrongPasswordRequirements().toSAX(contentHandler, "password-requirements");
180        }
181        
182        XMLUtils.startElement(contentHandler, "user-inputs");
183        saxUserInputs(request, zoneItem);
184        XMLUtils.endElement(contentHandler, "user-inputs");
185        
186        saxWarnings(signupType, signupPage, pwdChangePage, tosPage, successPage, tosContent, successContent);
187        
188        XMLUtils.createElement(contentHandler, "has-captcha", String.valueOf(_pageHelper.isCaptchaRequired(page)));
189        
190        saxTOSIfNeeded(zoneItem);
191        saxSuccessContentIfNeeded(zoneItem);
192        
193        saxAdditionalInformation(siteName, language, zoneItem);
194        
195        XMLUtils.endElement(contentHandler, "user-signup");
196        
197        contentHandler.endDocument();
198    }
199    
200    /**
201     * SAX the user inputs
202     * @param request the request
203     * @param zoneItem the zone item holding the service
204     * @throws SAXException if an error occurs
205     */
206    protected void saxUserInputs(Request request, ZoneItem zoneItem) throws SAXException
207    {
208        _saxNotEmptyParameter(request, "firstname");
209        _saxNotEmptyParameter(request, "lastname");
210        _saxNotEmptyParameter(request, "email");
211        _saxNotEmptyParameter(request, "token");
212    }
213    
214    /**
215     * Sax a non-empty request parameter
216     * @param request the request
217     * @param paramName the parameter name
218     * @throws SAXException  if an error occurs
219     */
220    protected void _saxNotEmptyParameter(Request request, String paramName) throws SAXException
221    {
222        _saxNotEmptyValue(paramName, request.getParameter(paramName));
223    }
224    
225    /**
226     * Sax a non-empty value
227     * @param tagName the tag name
228     * @param value the value
229     * @throws SAXException  if an error occurs
230     */
231    protected void _saxNotEmptyValue(String tagName, String value) throws SAXException
232    {
233        if (StringUtils.isNotEmpty(value))
234        {
235            XMLUtils.createElement(contentHandler, tagName, value);
236        }
237    }
238    
239    /**
240     * SAX additional information
241     * @param siteName The site name
242     * @param lang The content language
243     * @param zoneItem The zone item holding the service
244     * @throws SAXException if an error occurs
245     */
246    protected void saxAdditionalInformation(String siteName, String lang, ZoneItem zoneItem) throws SAXException
247    {
248        // Nothing
249    }
250    
251    /**
252     * SAX the general term of use content if needed
253     * @param zoneItem The current zone item
254     * @throws MalformedURLException if an error occurs
255     * @throws SAXException if an error occurs
256     * @throws IOException if an error occurs
257     */
258    protected void saxTOSIfNeeded(ZoneItem zoneItem) throws MalformedURLException, SAXException, IOException
259    {
260        String tosMode = zoneItem.getServiceParameters().getValue("terms-of-service-mode");
261        if ("CONTENT".equals(tosMode))
262        {
263            String contentId = zoneItem.getServiceParameters().getValue("terms-of-service-content");
264            saxContent(contentId, "tos");
265        }
266    }
267    
268    /**
269     * SAX the success content if needed
270     * @param zoneItem The current zone item
271     * @throws MalformedURLException if an error occurs
272     * @throws SAXException if an error occurs
273     * @throws IOException if an error occurs
274     */
275    protected void saxSuccessContentIfNeeded(ZoneItem zoneItem) throws MalformedURLException, SAXException, IOException
276    {
277        String status = parameters.getParameter("status", null);
278        String step = parameters.getParameter("step", null);
279        if ("signup".equals(step) && "success".equals(status))
280        {
281            String successMode = zoneItem.getServiceParameters().getValue("success-mode");
282            if ("CONTENT".equals(successMode))
283            {
284                String contentId = zoneItem.getServiceParameters().getValue("success-content");
285                saxContent(contentId, "success");
286            }
287        }
288    }
289    
290    private void _addNotEmptyAttribute (AttributesImpl attrs, String name, String value)
291    {
292        if (StringUtils.isNotEmpty(value))
293        {
294            attrs.addCDATAAttribute(name, value);
295        }
296    }
297    
298    /**
299     * SAX the content of GTU
300     * @param contentId the id of GTU content
301     * @param rootTag The root XML tag
302     * @throws SAXException if an error occurs
303     * @throws IOException if failed to sax TGU content
304     * @throws MalformedURLException if failed to sax TGU content
305     */
306    protected void saxContent(String contentId, String rootTag) throws SAXException, MalformedURLException, IOException
307    {
308        try
309        {
310            Content gtu = _resolver.resolveById(contentId);
311                    
312            XMLUtils.startElement(contentHandler, rootTag);
313            
314            AttributesImpl attrs = new AttributesImpl();
315            attrs.addCDATAAttribute("id", gtu.getId());
316            attrs.addCDATAAttribute("name", gtu.getName());
317            attrs.addCDATAAttribute("title", gtu.getTitle(null));
318            attrs.addCDATAAttribute("lastModifiedAt", DateUtils.zonedDateTimeToString(gtu.getLastModified()));
319            
320            XMLUtils.startElement(contentHandler, "content", attrs);
321            
322            String uri = _contentHelper.getContentHtmlViewUrl(gtu, "main");
323            SitemapSource src = null;
324            
325            try
326            {
327                src = (SitemapSource) _srcResolver.resolveURI(uri);
328                src.toSAX(new IgnoreRootHandler(contentHandler));
329            }
330            finally
331            {
332                _srcResolver.release(src);
333            }
334            
335            XMLUtils.endElement(contentHandler, "content");
336            
337            XMLUtils.endElement(contentHandler, rootTag);
338        }
339        catch (UnknownAmetysObjectException e)
340        {
341            getLogger().error("The GTU content with id '" + contentId + "' does not exist anymore");
342        }
343        
344    }
345    
346    /**
347     * Generate errors.
348     * @param errors the error list.
349     * @throws SAXException if an error occurs.
350     */
351    protected void saxErrors(Multimap<String, I18nizableText> errors) throws SAXException
352    {
353        XMLUtils.startElement(contentHandler, "errors");
354        
355        if (errors.containsKey("global"))
356        {
357            Collection<I18nizableText> globalErrors = errors.get("global");
358            
359            XMLUtils.startElement(contentHandler, "global");
360            
361            for (I18nizableText error : globalErrors)
362            {
363                AttributesImpl errorAttrs = new AttributesImpl();
364                if (!error.isI18n())
365                {
366                    errorAttrs.addCDATAAttribute("type", error.getLabel());
367                }
368                XMLUtils.createElement(contentHandler, "error", errorAttrs);
369            }
370            
371            XMLUtils.endElement(contentHandler, "global");
372        }
373        
374        for (String field : errors.keySet())
375        {
376            if (!field.equals("global"))
377            {
378                Collection<I18nizableText> fieldErrors = errors.get(field);
379                
380                AttributesImpl attrs = new AttributesImpl();
381                attrs.addCDATAAttribute("name", field);
382                
383                XMLUtils.startElement(contentHandler, "field", attrs);
384                
385                for (I18nizableText error : fieldErrors)
386                {
387                    error.toSAX(contentHandler, "error");
388                }
389                
390                XMLUtils.endElement(contentHandler, "field");
391            }
392        }
393        
394        XMLUtils.endElement(contentHandler, "errors");
395    }
396    
397    /**
398     * Generate errors which could prevent the service from working.
399     * @param signupType the type of signup enabled for the site
400     * @param signupPage the signup page.
401     * @param pwdChangePage the lost password page. Can be null.
402     * @param tosPage the page of terms of use. Can be null.
403     * @param successPage the page of success. Can be null.
404     * @param tosContent the content for terms of use. Can be null.
405     * @param successContent the content for success. Can be null.
406     * @throws SAXException if an error occurs.
407     */
408    protected void saxWarnings(SignupType signupType, Page signupPage, Page pwdChangePage, Page tosPage, Page successPage, Content tosContent, Content successContent) throws SAXException
409    {
410        
411        XMLUtils.startElement(contentHandler, "warnings");
412        
413        if (signupType == null || signupType == SignupType.UNAUTHORIZED)
414        {
415            XMLUtils.startElement(contentHandler, "warning");
416            new I18nizableText("plugin.web", "PLUGINS_WEB_USER_SIGNUP_ERROR_NOT_AUTHORIZED").toSAX(contentHandler);
417            XMLUtils.endElement(contentHandler, "warning");
418        }
419
420        if (signupPage == null)
421        {
422            XMLUtils.startElement(contentHandler, "warning");
423            new I18nizableText("plugin.web", "PLUGINS_WEB_USER_SIGNUP_ERROR_NO_SIGNUP_PAGE").toSAX(contentHandler);
424            XMLUtils.endElement(contentHandler, "warning");
425        }
426        else if (!_rightManager.hasAnonymousReadAccess(signupPage))
427        {
428            XMLUtils.startElement(contentHandler, "warning");
429            new I18nizableText("plugin.web", "PLUGINS_WEB_USER_SIGNUP_ERROR_SIGNUP_PAGE_READ_ACCESS").toSAX(contentHandler);
430            XMLUtils.endElement(contentHandler, "warning");
431        }
432        else if (!_isValid(signupPage))
433        {
434            XMLUtils.startElement(contentHandler, "warning");
435            new I18nizableText("plugin.web", "PLUGINS_WEB_USER_SIGNUP_ERROR_INVALID_SIGNUP_PAGE").toSAX(contentHandler);
436            XMLUtils.endElement(contentHandler, "warning");
437        }
438        
439        if (pwdChangePage == null)
440        {
441            XMLUtils.startElement(contentHandler, "warning");
442            new I18nizableText("plugin.web", "PLUGINS_WEB_USER_SIGNUP_ERROR_NO_PASSWORD_CHANGE_PAGE").toSAX(contentHandler);
443            XMLUtils.endElement(contentHandler, "warning");
444        }
445        else if (!_isValid(pwdChangePage))
446        {
447            XMLUtils.startElement(contentHandler, "warning");
448            new I18nizableText("plugin.web", "PLUGINS_WEB_USER_SIGNUP_ERROR_INVALID_PASSWORD_CHANGE_PAGE").toSAX(contentHandler);
449            XMLUtils.endElement(contentHandler, "warning");
450        }
451        
452        if (tosPage != null && !_isValid(tosPage))
453        {
454            XMLUtils.startElement(contentHandler, "warning");
455            new I18nizableText("plugin.web", "PLUGINS_WEB_USER_SIGNUP_ERROR_INVALID_TOS_PAGE").toSAX(contentHandler);
456            XMLUtils.endElement(contentHandler, "warning");
457        }
458        
459        if (successPage != null && !_isValid(successPage))
460        {
461            XMLUtils.startElement(contentHandler, "warning");
462            new I18nizableText("plugin.web", "PLUGINS_WEB_USER_SIGNUP_ERROR_INVALID_SUCCESS_PAGE").toSAX(contentHandler);
463            XMLUtils.endElement(contentHandler, "warning");
464        }
465        
466        if (tosContent != null && !_isValid(tosContent))
467        {
468            XMLUtils.startElement(contentHandler, "warning");
469            new I18nizableText("plugin.web", "PLUGINS_WEB_USER_SIGNUP_ERROR_INVALID_TOS_CONTENT").toSAX(contentHandler);
470            XMLUtils.endElement(contentHandler, "warning");
471        }
472        
473        if (successContent != null && !_isValid(successContent))
474        {
475            XMLUtils.startElement(contentHandler, "warning");
476            new I18nizableText("plugin.web", "PLUGINS_WEB_USER_SIGNUP_ERROR_INVALID_SUCCESS_CONTENT").toSAX(contentHandler);
477            XMLUtils.endElement(contentHandler, "warning");
478        }
479        
480        XMLUtils.endElement(contentHandler, "warnings");
481    }
482    
483    private boolean _isValid(Page page)
484    {
485        Skin skin = _skinManager.getSkin(page.getSite().getSkinId());
486        
487        Session liveSession = null;
488        try
489        {
490            liveSession = _repository.login(WebConstants.LIVE_WORKSPACE);
491            
492            if (!_synchronizeComponent.isPageValid(page, skin) || !_synchronizeComponent.isHierarchyValid(page, liveSession))
493            {
494                return false;
495            }
496            return true;
497        }
498        catch (RepositoryException e)
499        {
500            throw new RuntimeException("Unable to check live workspace", e);
501        }
502        finally
503        {
504            if (liveSession != null)
505            {
506                liveSession.logout();
507            }
508        }
509    }
510    
511    private boolean _isValid(Content content)
512    {
513        if (content instanceof VersionableAmetysObject)
514        {
515            return Arrays.asList(((VersionableAmetysObject) content).getAllLabels()).contains(CmsConstants.LIVE_LABEL);
516        }
517        
518        return false;
519    }
520}