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