001/*
002 * (C) Copyright 2013-2015 Nuxeo SA (http://nuxeo.com/) and others.
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 *
016 * Contributors:
017 *     Nuxeo - initial API and implementation
018 */
019package org.nuxeo.ecm.platform.ui.web.auth.service;
020
021import java.io.UnsupportedEncodingException;
022import java.net.URLDecoder;
023import java.util.ArrayList;
024import java.util.HashMap;
025import java.util.HashSet;
026import java.util.List;
027import java.util.Map;
028import java.util.Set;
029
030import javax.ws.rs.core.UriBuilder;
031
032import org.apache.commons.lang3.StringUtils;
033import org.nuxeo.common.Environment;
034import org.nuxeo.common.xmap.XMap;
035import org.nuxeo.common.xmap.annotation.XNode;
036import org.nuxeo.common.xmap.annotation.XNodeList;
037import org.nuxeo.common.xmap.annotation.XNodeMap;
038import org.nuxeo.common.xmap.annotation.XObject;
039import org.nuxeo.ecm.core.api.NuxeoException;
040import org.nuxeo.runtime.api.Framework;
041
042/**
043 * {@link XMap} object to manage configuration of the login screen (login.jsp)
044 *
045 * @author <a href="mailto:tdelprat@nuxeo.com">Tiry</a>
046 * @since 5.7
047 */
048@XObject("loginScreenConfig")
049public class LoginScreenConfig {
050
051    public static final String NUXEO_NEWS_URL = "//www.nuxeo.com/login-page-embedded-1/";
052
053    /**
054     * @since 8.4
055     */
056    @XNodeMap(value = "startupPages/startupPage", key = "@id", type = HashMap.class, componentType = LoginStartupPage.class)
057    protected Map<String, LoginStartupPage> startupPages = new HashMap<>();
058
059    @XNodeList(value = "loginProviders/loginProvider", type = ArrayList.class, componentType = LoginProviderLink.class)
060    protected List<LoginProviderLink> providers;
061
062    /**
063     * @since 7.10
064     */
065    @XNodeList(value = "videos/video", type = ArrayList.class, componentType = LoginVideo.class, nullByDefault = true)
066    protected List<LoginVideo> videos;
067
068    /**
069     * @since 7.10
070     */
071    @XNode("videos@muted")
072    protected Boolean muted;
073
074    /**
075     * @since 7.10
076     */
077    @XNode("videos@loop")
078    protected Boolean loop;
079
080    /**
081     * @since 7.10
082     */
083    protected String backgroundImage;
084
085    @XNode("removeNews")
086    protected Boolean removeNews = false;
087
088    protected String headerStyle;
089
090    protected String footerStyle;
091
092    protected String newsIframeUrl = NUXEO_NEWS_URL;
093
094    protected String newsIframeFullUrl = null;
095
096    protected String bodyBackgroundStyle;
097
098    protected String loginBoxBackgroundStyle;
099
100    @XNode("loginBoxWidth")
101    protected String loginBoxWidth;
102
103    protected String logoUrl;
104
105    @XNode("logoAlt")
106    protected String logoAlt;
107
108    @XNode("logoWidth")
109    protected String logoWidth;
110
111    @XNode("logoHeight")
112    protected String logoHeight;
113
114    /**
115     * @since 7.10
116     */
117    @XNode("fieldAutocomplete")
118    protected Boolean fieldAutocomplete;
119
120    /**
121     * Boolean to disable background-cover CSS behavior on login page background, as it may not be compliant with all
122     * browsers (see NXP-12972/NXP-12978).
123     *
124     * @since 5.8
125     */
126    @XNode("disableBackgroundSizeCover")
127    protected Boolean disableBackgroundSizeCover;
128
129    /**
130     * @since 7.10
131     */
132    @XNode("loginButtonBackgroundColor")
133    protected String loginButtonBackgroundColor;
134
135    /**
136     * @since 8.4
137     */
138    @XNode("defaultLocale")
139    protected String defaultLocale;
140
141    /**
142     * @since 8.4
143     */
144    @XNode("supportedLocales@append")
145    Boolean appendSupportedLocales;
146
147    /**
148     * @since 8.4
149     */
150    @XNodeList(value = "supportedLocales/locale", type = ArrayList.class, componentType = String.class)
151    List<String> supportedLocales;
152
153    public LoginScreenConfig() {
154    }
155
156    /**
157     * Instantiates a login screen configuration with the given login provider.
158     *
159     * @since 10.10
160     */
161    public LoginScreenConfig(LoginProviderLink provider) {
162        providers = new ArrayList<>();
163        providers.add(provider);
164    }
165
166    public List<LoginProviderLink> getProviders() {
167        return providers;
168    }
169
170    public void setProviders(List<LoginProviderLink> providers) {
171        this.providers = providers;
172    }
173
174    public LoginProviderLink getProvider(String name) {
175        if (getProviders() == null) {
176            return null;
177        }
178        for (LoginProviderLink provider : getProviders()) {
179            if (name.equals(provider.getName())) {
180                return provider;
181            }
182        }
183        return null;
184    }
185
186    /**
187     * @deprecated since 10.10, use {@link #LoginScreenConfig(LoginProviderLink)} instead
188     */
189    @Deprecated
190    public void registerLoginProvider(String name, String iconUrl, String link, String label, String description,
191            LoginProviderLinkComputer computer) {
192        LoginProviderLink newProvider = new LoginProviderLink();
193        newProvider.name = name;
194        newProvider.iconPath = iconUrl;
195        newProvider.link = link;
196        newProvider.label = label;
197        newProvider.description = description;
198        if (computer != null) {
199            newProvider.urlComputer = computer;
200        }
201
202        LoginProviderLink existingProvider = getProvider(name);
203        if (existingProvider != null) {
204            existingProvider.merge(newProvider);
205        } else {
206            if (providers == null) {
207                providers = new ArrayList<>();
208            }
209            providers.add(newProvider);
210        }
211    }
212
213    /**
214     * @since 8.4
215     */
216    public Map<String, LoginStartupPage> getStartupPages() {
217        return startupPages;
218    }
219
220    public String getHeaderStyle() {
221        return headerStyle;
222    }
223
224    public String getFooterStyle() {
225        return footerStyle;
226    }
227
228    public String getBodyBackgroundStyle() {
229        return bodyBackgroundStyle;
230    }
231
232    public String getLoginBoxBackgroundStyle() {
233        return loginBoxBackgroundStyle;
234    }
235
236    public String getLoginBoxWidth() {
237        return loginBoxWidth;
238    }
239
240    public String getLogoUrl() {
241        return logoUrl;
242    }
243
244    public String getLogoAlt() {
245        return logoAlt;
246    }
247
248    public String getLogoWidth() {
249        return logoWidth;
250    }
251
252    public String getLogoHeight() {
253        return logoHeight;
254    }
255
256    public List<LoginVideo> getVideos() {
257        return videos;
258    }
259
260    public Boolean getVideoMuted() {
261        return muted == null ? false : muted;
262    }
263
264    public Boolean getVideoLoop() {
265        return loop == null ? true : loop;
266    }
267
268    public boolean hasVideos() {
269        return videos != null && !videos.isEmpty();
270    }
271
272    public boolean getDisplayNews() {
273        return !(removeNews || StringUtils.isBlank(newsIframeUrl));
274    }
275
276    public Boolean getFieldAutocomplete() {
277        return fieldAutocomplete == null ? true : fieldAutocomplete;
278    }
279
280    @XNode("headerStyle")
281    public void setHeaderStyle(String headerStyle) {
282        this.headerStyle = Framework.expandVars(headerStyle);
283    }
284
285    @XNode("footerStyle")
286    public void setFooterStyle(String footerStyle) {
287        this.footerStyle = Framework.expandVars(footerStyle);
288    }
289
290    @XNode("bodyBackgroundStyle")
291    public void setBodyBackgroundStyle(String bodyBackgroundStyle) {
292        this.bodyBackgroundStyle = Framework.expandVars(bodyBackgroundStyle);
293    }
294
295    @XNode("backgroundImage")
296    public void setBackgroundImage(String backgroundImage) {
297        this.backgroundImage = Framework.expandVars(backgroundImage);
298    }
299
300    public String getBackgroundImage() {
301        return this.backgroundImage;
302    }
303
304    public String getLoginButtonBackgroundColor() {
305        return loginButtonBackgroundColor;
306    }
307
308    @XNode("loginBoxBackgroundStyle")
309    public void setLoginBoxBackgroundStyle(String loginBoxBackgroundStyle) {
310        this.loginBoxBackgroundStyle = Framework.expandVars(loginBoxBackgroundStyle);
311    }
312
313    @XNode("logoUrl")
314    public void setLogoUrl(String logoUrl) {
315        this.logoUrl = Framework.expandVars(logoUrl);
316    }
317
318    /**
319     * @since 7.10
320     */
321    @XNode("newsIframeUrl")
322    public void setNewsIframeUrl(String newsIframeUrl) {
323        this.newsIframeUrl = newsIframeUrl;
324        newsIframeFullUrl = null;
325    }
326
327    public String getNewsIframeUrl() {
328        if (newsIframeFullUrl == null) {
329            UriBuilder newsIFrameBuilder = UriBuilder.fromPath(newsIframeUrl);
330            if (NUXEO_NEWS_URL.equals(newsIframeUrl)) {
331                newsIFrameBuilder.queryParam(Environment.PRODUCT_VERSION,
332                        Framework.getProperty(Environment.PRODUCT_VERSION))
333                                 .queryParam(Environment.DISTRIBUTION_VERSION,
334                                         Framework.getProperty(Environment.DISTRIBUTION_VERSION))
335                                 .queryParam(Environment.DISTRIBUTION_PACKAGE,
336                                         Framework.getProperty(Environment.DISTRIBUTION_PACKAGE));
337            }
338            newsIframeFullUrl = newsIFrameBuilder.build().toString();
339        }
340        try {
341            return URLDecoder.decode(newsIframeFullUrl, "UTF-8");
342        } catch (UnsupportedEncodingException e) {
343            throw new NuxeoException("Cannot decode login iframe URL " + newsIframeFullUrl);
344        }
345    }
346
347    /**
348     * @since 5.8
349     * @see #disableBackgroundSizeCover
350     */
351    public Boolean getDisableBackgroundSizeCover() {
352        return disableBackgroundSizeCover;
353    }
354
355    /**
356     * @since 8.4
357     */
358    public String getDefaultLocale() {
359        return defaultLocale;
360    }
361
362    /**
363     * @since 8.4
364     */
365    public Boolean isAppendSupportedLocales() {
366        return appendSupportedLocales;
367    }
368
369    /**
370     * @since 8.4
371     */
372    public List<String> getSupportedLocales() {
373        List<String> res = new ArrayList<>();
374        if (supportedLocales != null) {
375            res.addAll(supportedLocales);
376        }
377        String defaultLocale = getDefaultLocale();
378        if (defaultLocale != null && !res.contains(defaultLocale)) {
379            res.add(defaultLocale);
380        }
381        return res;
382    }
383
384    protected void merge(LoginScreenConfig newConfig) {
385        if (newConfig.newsIframeUrl != null) {
386            setNewsIframeUrl(newConfig.newsIframeUrl);
387        }
388        if (newConfig.headerStyle != null) {
389            headerStyle = newConfig.headerStyle;
390        }
391        if (newConfig.footerStyle != null) {
392            footerStyle = newConfig.footerStyle;
393        }
394        if (newConfig.bodyBackgroundStyle != null) {
395            bodyBackgroundStyle = newConfig.bodyBackgroundStyle;
396        }
397        if (newConfig.loginBoxBackgroundStyle != null) {
398            loginBoxBackgroundStyle = newConfig.loginBoxBackgroundStyle;
399        }
400        if (newConfig.loginBoxWidth != null) {
401            loginBoxWidth = newConfig.loginBoxWidth;
402        }
403        if (newConfig.disableBackgroundSizeCover != null) {
404            disableBackgroundSizeCover = newConfig.disableBackgroundSizeCover;
405        }
406        if (newConfig.logoAlt != null) {
407            logoAlt = newConfig.logoAlt;
408        }
409        if (newConfig.logoHeight != null) {
410            logoHeight = newConfig.logoHeight;
411        }
412        if (newConfig.logoUrl != null) {
413            logoUrl = newConfig.logoUrl;
414        }
415        if (newConfig.logoWidth != null) {
416            logoWidth = newConfig.logoWidth;
417        }
418        if (newConfig.fieldAutocomplete != null) {
419            fieldAutocomplete = newConfig.fieldAutocomplete;
420        }
421        if (newConfig.videos != null) {
422            videos = newConfig.videos;
423        }
424        if (newConfig.loop != null) {
425            loop = newConfig.loop;
426        }
427        if (newConfig.removeNews) {
428            removeNews = newConfig.removeNews;
429        }
430        if (newConfig.muted != null) {
431            muted = newConfig.muted;
432        }
433        if (newConfig.loginButtonBackgroundColor != null) {
434            loginButtonBackgroundColor = newConfig.loginButtonBackgroundColor;
435        }
436        if (newConfig.backgroundImage != null) {
437            backgroundImage = newConfig.backgroundImage;
438        }
439
440        if (providers == null) {
441            providers = newConfig.providers;
442        } else if (newConfig.providers != null && newConfig.providers.size() > 0) {
443            for (LoginProviderLink link : newConfig.providers) {
444
445                int idx = providers.indexOf(link);
446                if (idx >= 0) {
447                    if (link.remove) {
448                        providers.remove(idx);
449                    } else {
450                        providers.get(idx).merge(link);
451                    }
452                } else {
453                    providers.add(link);
454                }
455            }
456        }
457
458        if (startupPages == null) {
459            startupPages = newConfig.startupPages;
460        } else if (newConfig.startupPages != null && !newConfig.startupPages.isEmpty()) {
461            for (Map.Entry<String, LoginStartupPage> startupPage : newConfig.startupPages.entrySet()) {
462                if (startupPages.containsKey(startupPage.getKey())) {
463                    startupPages.get(startupPage.getKey()).merge(startupPage.getValue());
464                } else {
465                    startupPages.put(startupPage.getKey(), startupPage.getValue());
466                }
467            }
468        }
469
470        if (newConfig.defaultLocale != null) {
471            defaultLocale = newConfig.defaultLocale;
472        }
473
474        Boolean append = newConfig.isAppendSupportedLocales();
475        List<String> newLocales = newConfig.getSupportedLocales();
476        Set<String> mergedLocales = new HashSet<>();
477        if (!Boolean.FALSE.equals(append) && supportedLocales != null) {
478            mergedLocales.addAll(supportedLocales);
479        }
480        if (newLocales != null) {
481            mergedLocales.addAll(newLocales);
482        }
483        supportedLocales = new ArrayList<>(mergedLocales);
484    }
485
486    /**
487     * @since 7.10
488     */
489    @Override
490    public LoginScreenConfig clone() {
491        LoginScreenConfig clone = new LoginScreenConfig();
492        clone.bodyBackgroundStyle = bodyBackgroundStyle;
493        clone.disableBackgroundSizeCover = disableBackgroundSizeCover;
494        clone.fieldAutocomplete = fieldAutocomplete;
495        clone.footerStyle = footerStyle;
496        clone.headerStyle = headerStyle;
497        clone.loginBoxBackgroundStyle = loginBoxBackgroundStyle;
498        clone.loginBoxWidth = loginBoxWidth;
499        clone.loginButtonBackgroundColor = loginButtonBackgroundColor;
500        clone.logoAlt = logoAlt;
501        clone.logoHeight = logoHeight;
502        clone.logoUrl = logoUrl;
503        clone.logoWidth = logoWidth;
504        clone.loop = loop;
505        clone.muted = muted;
506        clone.newsIframeUrl = newsIframeUrl;
507        if (providers != null) {
508            clone.providers = new ArrayList<>();
509            for (LoginProviderLink l : providers) {
510                clone.providers.add(l.clone());
511            }
512        }
513        if (startupPages != null) {
514            clone.startupPages = new HashMap<>();
515            for (Map.Entry<String, LoginStartupPage> startupPage : startupPages.entrySet()) {
516                clone.startupPages.put(startupPage.getKey(), startupPage.getValue().clone());
517            }
518        }
519        clone.removeNews = removeNews;
520        if (videos != null) {
521            clone.videos = new ArrayList<>();
522            for (LoginVideo v : videos) {
523                clone.videos.add(v.clone());
524            }
525        }
526        clone.defaultLocale = defaultLocale;
527        clone.appendSupportedLocales = appendSupportedLocales;
528        if (supportedLocales != null) {
529            clone.supportedLocales = new ArrayList<>(supportedLocales);
530        }
531        return clone;
532    }
533
534}