/* * #%L * ACS AEM Commons Bundle * %% * Copyright (C) 2015 Adobe * %% * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * #L% */ package com.adobe.acs.commons.wcm.notifications.impl; import com.adobe.acs.commons.http.injectors.AbstractHtmlRequestInjector; import com.adobe.acs.commons.util.CookieUtil; import com.adobe.acs.commons.wcm.notifications.SystemNotifications; import com.day.cq.wcm.api.Page; import com.day.cq.wcm.api.PageManager; import org.apache.commons.codec.digest.DigestUtils; import org.apache.commons.lang.StringUtils; import org.apache.felix.scr.annotations.Activate; import org.apache.felix.scr.annotations.Component; import org.apache.felix.scr.annotations.Deactivate; import org.apache.felix.scr.annotations.Reference; import org.apache.felix.scr.annotations.Service; import org.apache.jackrabbit.JcrConstants; import org.apache.sling.api.SlingConstants; import org.apache.sling.api.SlingHttpServletRequest; import org.apache.sling.api.SlingHttpServletResponse; import org.apache.sling.api.resource.LoginException; import org.apache.sling.api.resource.Resource; import org.apache.sling.api.resource.ResourceResolver; import org.apache.sling.api.resource.ResourceResolverFactory; import org.apache.sling.api.resource.ValueMap; import org.apache.sling.settings.SlingSettingsService; import org.osgi.framework.ServiceRegistration; import org.osgi.service.component.ComponentContext; import org.osgi.service.event.Event; import org.osgi.service.event.EventConstants; import org.osgi.service.event.EventHandler; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.PrintWriter; import java.util.ArrayList; import java.util.Calendar; import java.util.Collections; import java.util.Hashtable; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.concurrent.atomic.AtomicBoolean; @Component(immediate = true) @Service(value = SystemNotifications.class) public class SystemNotificationsImpl extends AbstractHtmlRequestInjector implements SystemNotifications, EventHandler { private static final Logger log = LoggerFactory.getLogger(SystemNotificationsImpl.class); public static final String COOKIE_NAME = "acs-commons-system-notifications"; private static final String PATH_NOTIFICATIONS = "/etc/acs-commons/notifications"; private static final String PN_ON_TIME = "onTime"; private static final String PN_OFF_TIME = "offTime"; private static final String PN_ENABLED = "enabled"; private static final String REP_POLICY = "rep:policy"; private static final String INJECT_TEXT = "<script>" + "if(window === top) {" + " window.jQuery || document.write('<script src=\"%s\"><\\/script>');" + " document.write('<script src=\"%s\"><\\/script>');" + "}" + "</script>"; private static final String SERVICE_NAME = "system-notifications"; private static final Map<String, Object> AUTH_INFO; static { AUTH_INFO = Collections.singletonMap(ResourceResolverFactory.SUBSERVICE, (Object) SERVICE_NAME); } private AtomicBoolean isFilter = new AtomicBoolean(false); private ComponentContext osgiComponentContext; private ServiceRegistration eventHandlerRegistration; @Reference private ResourceResolverFactory resourceResolverFactory; @Reference private SlingSettingsService slingSettings; @Override protected void inject(HttpServletRequest servletRequest, HttpServletResponse servletResponse, PrintWriter printWriter) { final String jquerySrc = servletRequest.getContextPath() + "/etc/clientlibs/granite/jquery.js"; final String notificationsSrc = servletRequest.getContextPath() + "/apps/acs-commons/components/utilities/system-notifications/notification/clientlibs.js"; printWriter.println(String.format(INJECT_TEXT, jquerySrc, notificationsSrc)); } @Override protected boolean accepts(final ServletRequest servletRequest, final ServletResponse servletResponse) { if (!(servletRequest instanceof SlingHttpServletRequest) || !(servletResponse instanceof SlingHttpServletResponse)) { return false; } final SlingHttpServletRequest slingRequest = (SlingHttpServletRequest) servletRequest; if (StringUtils.startsWith(slingRequest.getResource().getPath(), PATH_NOTIFICATIONS)) { // Do NOT inject on the notifications Authoring pages return false; } final Resource notificationsFolder = slingRequest.getResourceResolver().getResource(PATH_NOTIFICATIONS); if (notificationsFolder == null || this.getNotifications(slingRequest, notificationsFolder).size() < 1) { // If no notifications folder or no active notifications; do not inject JS return false; } return super.accepts(servletRequest, servletResponse); } @Override protected int getInjectIndex(String originalContents) { // Inject immediately before the ending body tag return StringUtils.indexOf(originalContents, "</body>"); } @Override public List<Resource> getNotifications(final SlingHttpServletRequest request, final Resource notificationsFolder) { final List<Resource> notifications = new ArrayList<Resource>(); final Iterator<Resource> itr = notificationsFolder.listChildren(); while (itr.hasNext()) { final Resource notification = itr.next(); if (this.isActiveNotification(request, notification)) { notifications.add(notification); } } return notifications; } @Override public String getNotificationId(final Page notificationPage) { final String path = notificationPage.getPath(); final String lastModified = String.valueOf(notificationPage.getLastModified().getTimeInMillis()); return "uid-" + DigestUtils.shaHex(path + lastModified); } @Override public String getMessage(String message, String onTime, String offTime) { if (StringUtils.isBlank(message)) { return message; } message = StringUtils.trimToEmpty(message); boolean allowHTML = false; if (StringUtils.startsWith(message, "html:")) { allowHTML = true; message = StringUtils.removeStart(message, "html:"); } if (onTime != null) { message = StringUtils.replace(message, "{{ onTime }}", onTime); } if (offTime != null) { message = StringUtils.replace(message, "{{ offTime }}", offTime); } if (!allowHTML) { message = message.replaceAll("(\r\n|\n)", "<br />"); } return message; } private boolean isActiveNotification(final SlingHttpServletRequest request, final Resource resource) { if (JcrConstants.JCR_CONTENT.equals(resource.getName()) || REP_POLICY.equals(resource.getName())) { return false; } final PageManager pageManager = request.getResourceResolver().adaptTo(PageManager.class); final Page notificationPage = pageManager.getContainingPage(resource); if (notificationPage == null) { log.warn("Trying to get a invalid System Notification page at [ {} ]", resource.getPath()); return false; } else if (this.isDismissed(request, notificationPage)) { // System Notification previously dismissed by the user return false; } // Looks like a valid Notification Page; now check if the properties are valid final ValueMap properties = notificationPage.getProperties(); final boolean enabled = properties.get(PN_ENABLED, false); if (!enabled) { // Disabled return false; } else { final Calendar onTime = properties.get(PN_ON_TIME, Calendar.class); final Calendar offTime = properties.get(PN_OFF_TIME, Calendar.class); if (onTime == null && offTime == null) { // No on time or off time is set, but is enabled so always show return true; } final Calendar now = Calendar.getInstance(); if (onTime != null && now.before(onTime)) { return false; } if (offTime != null && now.after(offTime)) { return false; } return true; } } private boolean isDismissed(final SlingHttpServletRequest request, final Page notificationPage) { final Cookie cookie = CookieUtil.getCookie(request, COOKIE_NAME); if (cookie != null) { return StringUtils.contains(cookie.getValue(), this.getNotificationId(notificationPage)); } else { // No cookie has been set, so nothing has been dismissed return false; } } private boolean hasNotifications() { ResourceResolver resourceResolver = null; try { resourceResolver = resourceResolverFactory.getServiceResourceResolver(AUTH_INFO); final Resource notificationsFolder = resourceResolver.getResource(PATH_NOTIFICATIONS); if (notificationsFolder != null) { final Iterator<Resource> resources = notificationsFolder.listChildren(); while (resources.hasNext()) { final Resource resource = resources.next(); if (!JcrConstants.JCR_CONTENT.equals(resource.getName()) && !REP_POLICY.equals(resource.getName())) { return true; } } } } catch (LoginException e) { log.error("Could not get an service ResourceResolver", e); } finally { if (resourceResolver != null) { resourceResolver.close(); } } return false; } private void registerAsFilter() { super.registerAsSlingFilter(this.osgiComponentContext, 0, ".*"); log.debug("Registered System Notifications as Sling Filter"); } private void registerAsEventHandler() { final Hashtable filterProps = new Hashtable<String, String>(); // Listen on Add and Remove under /etc/acs-commons/notifications filterProps.put(EventConstants.EVENT_TOPIC, new String[]{ SlingConstants.TOPIC_RESOURCE_ADDED, SlingConstants.TOPIC_RESOURCE_REMOVED }); filterProps.put(EventConstants.EVENT_FILTER, "(&" + "(" + SlingConstants.PROPERTY_PATH + "=" + SystemNotificationsImpl.PATH_NOTIFICATIONS + "/*)" + ")"); this.eventHandlerRegistration = this.osgiComponentContext.getBundleContext().registerService(EventHandler.class.getName(), this, filterProps); log.debug("Registered System Notifications as Event Handler"); } @Override public void handleEvent(final Event event) { long start = System.currentTimeMillis(); if (!this.isAuthor()) { log.warn("This event handler should ONLY run on AEM Author."); return; } /** The following code will ONLY execute on AEM Author **/ final String path = (String) event.getProperty(SlingConstants.PROPERTY_PATH); if (StringUtils.endsWith(path, JcrConstants.JCR_CONTENT)) { // Ignore jcr:content nodes; Only handle events for cq:Page return; } if (this.hasNotifications()) { if (!this.isFilter.getAndSet(true)) { this.registerAsFilter(); } } else { if (this.isFilter.getAndSet(false)) { this.unregisterFilter(); log.debug("Unregistered System Notifications Sling Filter"); } } if (System.currentTimeMillis() - start > 2500) { log.warn("Event handling for System notifications took [ {} ] ms. Event blacklisting occurs after 5000 ms.", System.currentTimeMillis() - start); } } @Activate protected void activate(ComponentContext ctx) { this.osgiComponentContext = ctx; if (this.isAuthor()) { this.registerAsEventHandler(); if (this.hasNotifications()) { this.isFilter.set(true); this.registerAsFilter(); } } } @Deactivate protected void deactivate(ComponentContext ctx) { super.deactivate(ctx); // Unregister the event handler is was registered if (eventHandlerRegistration != null) { eventHandlerRegistration.unregister(); eventHandlerRegistration = null; } this.osgiComponentContext = null; } private boolean isAuthor() { return slingSettings.getRunModes().contains("author"); } }