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.Notification;
66  import com.vaadin.ui.UI;
67  
68  /**
69   * Resurface UI presenter.
70   */
71  public class ResurfaceUIPresenter {
72  
73      private static final Logger log = LoggerFactory.getLogger(ResurfaceUIPresenter.class);
74  
75      private final MessagesManager messagesManager;
76      private final LocalMessageDispatcher messageDispatcher;
77      private final LocalTaskDispatcher taskDispatcher;
78      private final LocalTaskDispatcherManager taskDispatcherManager;
79      private final LocationController locationController;
80      private final PrivacyNotice privacyNotice;
81      private final SimpleTranslator i18n;
82      private final AppLauncherLayout userLayout;
83      private final String userName;
84  
85      @Inject
86      public ResurfaceUIPresenter(ComponentProvider componentProvider, AppLauncherLayoutManager appLauncherLayoutManager,
87                                  MessagesManager messagesManager, LocalTaskDispatcherManager taskDispatcherManager,
88                                  SimpleTranslator i18n, Context context, LocationController locationController,
89                                  PrivacyNotice privacyNotice, @Named(AdmincentralEventBus.NAME) EventBus eventBus, UI ui) {
90  
91          this.userName = context.getUser().getName();
92  
93          this.messagesManager = messagesManager;
94          this.messageDispatcher = componentProvider.newInstance(LocalMessageDispatcher.class, ui);
95          this.messagesManager.registerMessagesListener(userName, messageDispatcher);
96  
97          this.taskDispatcherManager = taskDispatcherManager;
98          this.taskDispatcher = componentProvider.newInstance(LocalTaskDispatcher.class, ui);
99          this.taskDispatcherManager.registerLocalTasksListener(userName, taskDispatcher);
100 
101         this.locationController = locationController;
102         this.userLayout = appLauncherLayoutManager.getLayoutForUser(context.getUser());
103 
104         this.privacyNotice = privacyNotice;
105         this.i18n = i18n;
106 
107         UI.getCurrent().setErrorHandler(new AdmincentralErrorHandler(messagesManager, i18n));
108         handleLocationChange(eventBus);
109     }
110 
111     void start() {
112         String initialFragment = UI.getCurrent().getPage().getUriFragment();
113         checkAndNavigate(initialFragment);
114         this.showOnFirstLogin();
115     }
116 
117     /**
118      * Typically called when the UI is being detached.
119      * Performs some necessary cleanup of resources, e.g. unregister listeners.
120      */
121     void stop() {
122         messagesManager.unregisterMessagesListener(userName, messageDispatcher);
123         taskDispatcherManager.unregisterLocalTasksListener(userName, taskDispatcher);
124     }
125 
126     // Fragment-based location control to/from UI
127     // * below we listen to external changes (e.g. initiated by the client)
128     // * updating the fragment programmatically via Page#setUriFragment (`Shell#setFragment` does this too for compatibility with AppController/AppInstanceController)
129     // TODO restore user-based location control (see DefaultLocationHistoryMapper); ideally without AppLauncherLayoutManager.getLayoutForUser :X
130     private void handleLocationChange(EventBus eventBus) {
131         Page.getCurrent().addPopStateListener(event -> {
132             String fragment = event.getPage().getUriFragment();
133             checkAndNavigate(fragment);
134         });
135 
136         eventBus.addHandler(LocationChangedEvent.class, event -> {
137             // empty location will occur when going from Magnolia "home page" to an app and back with browser button
138             // we can't set the uri fragment there or a # (dash) will be appended to the uri preventing back navigation from happening
139             if (StringUtils.isBlank(event.getNewLocation().toString())) {
140                 Page.getCurrent().reload();
141             } else {
142                 Page.getCurrent().setUriFragment(event.getNewLocation().toString());
143             }
144         });
145     }
146 
147     private void checkAndNavigate(String fragment) {
148         DefaultLocation newLocation = fragment != null ? new DefaultLocation(fragment) : new DefaultLocation();
149 
150         // verify app & group permissions for incoming navigation changes
151         if (userLayout.containsApp(newLocation.getAppName()) || isHeaderApp(newLocation.getAppName())) {
152             locationController.goTo(newLocation);
153         } else {
154             log.debug("Denying user {} request to open app {} without permissions", userName, newLocation.getAppName());
155         }
156     }
157 
158     private boolean isHeaderApp(String appName) {
159         // header apps are hidden in launcher, so layout-based security check doesn't cut it
160         // somewhat equivalent to AppControllerImpl#isAllowedToUser former check for shell apps
161         return Arrays.stream(HEADER_APPS)
162                 .anyMatch(headerAppName -> headerAppName.equalsIgnoreCase(appName));
163     }
164 
165     private void showOnFirstLogin() {
166         if (!privacyNotice.currentUserHasAckedPrivacyNotice()) {
167             // our i18n mechanism filters out the target attribute and can't be tweaked.
168             // for the sake of UX let's add it to the string after translation :disappear:
169             String aOpeningTag = "<a href=\"http://mgnl.io/privacy\" target=\"_blank\">";
170             AlertBuilder.alert(i18n.translate("admincentral.privacy.notice.title"))
171                     .withLevel(Notification.Type.HUMANIZED_MESSAGE)
172                     .withBody(i18n.translate("admincentral.privacy.notice.body", aOpeningTag, "</a>"))
173                     .withOkButtonCaption("OK")
174                     .buildAndOpen();
175         }
176         privacyNotice.storePrivacyNoticeAcknowledgment();
177     }
178 }