View Javadoc
1   /**
2    * This file Copyright (c) 2018 Magnolia International
3    * Ltd.  (http://www.magnolia-cms.com). All rights reserved.
4    *
5    *
6    * This file is dual-licensed under both the Magnolia
7    * Network Agreement and the GNU General Public License.
8    * You may elect to use one or the other of these licenses.
9    *
10   * This file is distributed in the hope that it will be
11   * useful, but AS-IS and WITHOUT ANY WARRANTY; without even the
12   * implied warranty of MERCHANTABILITY or FITNESS FOR A
13   * PARTICULAR PURPOSE, TITLE, or NONINFRINGEMENT.
14   * Redistribution, except as permitted by whichever of the GPL
15   * or MNA you select, is prohibited.
16   *
17   * 1. For the GPL license (GPL), you can redistribute and/or
18   * modify this file under the terms of the GNU General
19   * Public License, Version 3, as published by the Free Software
20   * Foundation.  You should have received a copy of the GNU
21   * General Public License, Version 3 along with this program;
22   * if not, write to the Free Software Foundation, Inc., 51
23   * Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
24   *
25   * 2. For the Magnolia Network Agreement (MNA), this file
26   * and the accompanying materials are made available under the
27   * terms of the MNA which accompanies this distribution, and
28   * is available at http://www.magnolia-cms.com/mna.html
29   *
30   * Any modifications to this file must keep this entire header
31   * intact.
32   *
33   */
34  package info.magnolia.admincentral;
35  
36  import static info.magnolia.ui.api.app.AppInstanceController.HEADER_APPS;
37  
38  import info.magnolia.context.Context;
39  import info.magnolia.event.EventBus;
40  import info.magnolia.i18nsystem.SimpleTranslator;
41  import info.magnolia.objectfactory.ComponentProvider;
42  import info.magnolia.ui.AlertBuilder;
43  import info.magnolia.ui.api.app.launcherlayout.AppLauncherLayout;
44  import info.magnolia.ui.api.app.launcherlayout.AppLauncherLayoutManager;
45  import info.magnolia.ui.api.event.AdmincentralEventBus;
46  import info.magnolia.ui.api.location.DefaultLocation;
47  import info.magnolia.ui.api.location.LocationChangedEvent;
48  import info.magnolia.ui.api.location.LocationController;
49  import info.magnolia.ui.framework.message.LocalMessageDispatcher;
50  import info.magnolia.ui.framework.message.MessagesManager;
51  import info.magnolia.ui.framework.task.LocalTaskDispatcher;
52  import info.magnolia.ui.framework.task.LocalTaskDispatcherManager;
53  import info.magnolia.usagemetrics.PrivacyNotice;
54  
55  import java.util.Arrays;
56  
57  import javax.inject.Inject;
58  import javax.inject.Named;
59  
60  import org.apache.commons.lang3.StringUtils;
61  import org.slf4j.Logger;
62  import org.slf4j.LoggerFactory;
63  
64  import com.vaadin.server.Page;
65  import com.vaadin.ui.Layout;
66  import com.vaadin.ui.Notification;
67  import com.vaadin.ui.UI;
68  
69  /**
70   * Resurface UI presenter.
71   */
72  public class ResurfaceUIPresenter {
73  
74      private static final Logger log = LoggerFactory.getLogger(ResurfaceUIPresenter.class);
75  
76      private final MessagesManager messagesManager;
77      private final LocalMessageDispatcher messageDispatcher;
78      private final LocalTaskDispatcher taskDispatcher;
79      private final LocalTaskDispatcherManager taskDispatcherManager;
80      private final Context context;
81      private final LocationController locationController;
82      private final ResurfaceUILayout resurfaceUILayout;
83      private final PrivacyNotice privacyNotice;
84      private final SimpleTranslator i18n;
85      private final AppLauncherLayout userLayout;
86      private final String userName;
87  
88      @Inject
89      public ResurfaceUIPresenter(ComponentProvider componentProvider, AppLauncherLayoutManager appLauncherLayoutManager,
90                                  MessagesManager messagesManager, LocalTaskDispatcherManager taskDispatcherManager,
91                                  SimpleTranslator i18n, Context context, LocationController locationController,
92                                  ResurfaceUILayout resurfaceUILayout, PrivacyNotice privacyNotice,
93                                  @Named(AdmincentralEventBus.NAME) EventBus eventBus, UI ui) {
94  
95          this.context = context;
96          this.userName = this.context.getUser().getName();
97  
98          this.messagesManager = messagesManager;
99          this.messageDispatcher = componentProvider.newInstance(LocalMessageDispatcher.class, ui);
100         this.messagesManager.registerMessagesListener(userName, messageDispatcher);
101 
102         this.taskDispatcherManager = taskDispatcherManager;
103         this.taskDispatcher = componentProvider.newInstance(LocalTaskDispatcher.class, ui);
104         this.taskDispatcherManager.registerLocalTasksListener(userName, taskDispatcher);
105 
106         this.locationController = locationController;
107         this.userLayout = appLauncherLayoutManager.getLayoutForUser(this.context.getUser());
108 
109         this.resurfaceUILayout = resurfaceUILayout;
110         this.privacyNotice = privacyNotice;
111         this.i18n = i18n;
112 
113         UI.getCurrent().setErrorHandler(new AdmincentralErrorHandler(messagesManager, i18n));
114         handleLocationChange(eventBus);
115     }
116 
117     Layout start() {
118         String initialFragment = UI.getCurrent().getPage().getUriFragment();
119         checkAndNavigate(initialFragment);
120         this.showOnFirstLogin();
121         return resurfaceUILayout;
122     }
123 
124     /**
125      * Typically called when the UI is being detached.
126      * Performs some necessary cleanup of resources, e.g. unregister listeners.
127      */
128     void stop() {
129         messagesManager.unregisterMessagesListener(userName, messageDispatcher);
130         taskDispatcherManager.unregisterLocalTasksListener(userName, taskDispatcher);
131     }
132 
133     // Fragment-based location control to/from UI
134     // * below we listen to external changes (e.g. initiated by the client)
135     // * updating the fragment programmatically via Page#setUriFragment (`Shell#setFragment` does this too for compatibility with AppController/AppInstanceController)
136     // TODO restore user-based location control (see DefaultLocationHistoryMapper); ideally without AppLauncherLayoutManager.getLayoutForUser :X
137     private void handleLocationChange(EventBus eventBus) {
138         Page.getCurrent().addPopStateListener(event -> {
139             String fragment = event.getPage().getUriFragment();
140             checkAndNavigate(fragment);
141         });
142 
143         eventBus.addHandler(LocationChangedEvent.class, event -> {
144             // empty location will occur when going from Magnolia "home page" to an app and back with browser button
145             // we can't set the uri fragment there or a # (dash) will be appended to the uri preventing back navigation from happening
146             if (StringUtils.isBlank(event.getNewLocation().toString())) {
147                 Page.getCurrent().reload();
148             } else {
149                 Page.getCurrent().setUriFragment(event.getNewLocation().toString());
150             }
151         });
152     }
153 
154     private void checkAndNavigate(String fragment) {
155         DefaultLocation newLocation = fragment != null ? new DefaultLocation(fragment) : new DefaultLocation();
156 
157         // verify app & group permissions for incoming navigation changes
158         if (userLayout.containsApp(newLocation.getAppName()) || isHeaderApp(newLocation.getAppName())) {
159             locationController.goTo(newLocation);
160         } else {
161             log.debug("Denying user {} request to open app {} without permissions", userName, newLocation.getAppName());
162         }
163     }
164 
165     private boolean isHeaderApp(String appName) {
166         // header apps are hidden in launcher, so layout-based security check doesn't cut it
167         // somewhat equivalent to AppControllerImpl#isAllowedToUser former check for shell apps
168         return Arrays.stream(HEADER_APPS)
169                 .anyMatch(headerAppName -> headerAppName.equalsIgnoreCase(appName));
170     }
171 
172     private void showOnFirstLogin() {
173         if (!privacyNotice.currentUserHasAckedPrivacyNotice()) {
174             // our i18n mechanism filters out the target attribute and can't be tweaked.
175             // for the sake of UX let's add it to the string after translation :disappear:
176             String aOpeningTag = "<a href=\"http://mgnl.io/privacy\" target=\"_blank\">";
177             AlertBuilder.alert(i18n.translate("admincentral.privacy.notice.title"))
178                     .withLevel(Notification.Type.HUMANIZED_MESSAGE)
179                     .withBody(i18n.translate("admincentral.privacy.notice.body", aOpeningTag, "</a>"))
180                     .withOkButtonCaption("OK")
181                     .buildAndOpen();
182         }
183         privacyNotice.storePrivacyNoticeAcknowledgment();
184     }
185 }