001/* 002 * Copyright 2025 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.plugins.linkdirectory.tracking; 017 018import java.util.ArrayList; 019import java.util.List; 020import java.util.Map; 021 022import org.apache.avalon.framework.service.ServiceException; 023import org.apache.avalon.framework.service.ServiceManager; 024import org.apache.avalon.framework.service.Serviceable; 025import org.apache.commons.lang3.StringUtils; 026 027import org.ametys.cms.tag.Tag; 028import org.ametys.core.userpref.UserPreferencesException; 029import org.ametys.core.userpref.UserPreferencesManager; 030import org.ametys.core.util.I18nUtils; 031import org.ametys.plugins.linkdirectory.DirectoryHelper; 032import org.ametys.plugins.linkdirectory.LinkDirectoryThemesInputDataHelper; 033import org.ametys.plugins.linkdirectory.LinkDirectoryThemesInputDataHelper.ConfiguredThemesInputData; 034import org.ametys.plugins.linkdirectory.LinkDirectoryThemesInputDataHelper.ThemesInputData; 035import org.ametys.plugins.linkdirectory.theme.ThemesDAO; 036import org.ametys.plugins.repository.AmetysObjectIterable; 037import org.ametys.plugins.repository.AmetysObjectResolver; 038import org.ametys.plugins.repository.data.holder.ModelAwareDataHolder; 039import org.ametys.plugins.webanalytics.matomo.tracking.AbstractMatomoEventTrackingProvider; 040import org.ametys.web.repository.page.ModifiableZoneItem; 041import org.ametys.web.repository.site.Site; 042import org.ametys.web.repository.sitemap.Sitemap; 043import org.ametys.web.userpref.FOUserPreferencesConstants; 044 045/** 046 * The Matomo tracking provider for the link directory 047 */ 048public class LinkDirectoryMatomoTrackingProvider extends AbstractMatomoEventTrackingProvider implements Serviceable 049{ 050 /** The user preferences manager. */ 051 protected UserPreferencesManager _userPreferencesManager; 052 053 /** The link directory themes input data helper */ 054 protected LinkDirectoryThemesInputDataHelper _linkDirectoryThemesInputDataHelper; 055 056 /** Themes DAO */ 057 protected ThemesDAO _themesDAO; 058 059 /** The i18n utils */ 060 protected I18nUtils _i18nUtils; 061 062 /** The ametys object resolver */ 063 protected AmetysObjectResolver _resolver; 064 065 public void service(ServiceManager manager) throws ServiceException 066 { 067 _userPreferencesManager = (UserPreferencesManager) manager.lookup(UserPreferencesManager.ROLE + ".FO"); 068 _linkDirectoryThemesInputDataHelper = (LinkDirectoryThemesInputDataHelper) manager.lookup(LinkDirectoryThemesInputDataHelper.ROLE); 069 _themesDAO = (ThemesDAO) manager.lookup(ThemesDAO.ROLE); 070 _i18nUtils = (I18nUtils) manager.lookup(I18nUtils.ROLE); 071 _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE); 072 } 073 074 @Override 075 protected List<MatomoEvent> getEvents(Site site) 076 { 077 List<MatomoEvent> requests = new ArrayList<>(); 078 079 AmetysObjectIterable<Sitemap> sitemaps = site.getSitemaps(); 080 for (Sitemap sitemap : sitemaps) 081 { 082 requests.addAll(_getConfigurableLinkServiceEvents(site, sitemap)); 083 requests.addAll(_getConfigurableLinkInputDataEvents(site, sitemap)); 084 } 085 086 return requests; 087 } 088 089 private List<MatomoEvent> _getConfigurableLinkServiceEvents(Site site, Sitemap sitemap) 090 { 091 List<MatomoEvent> requests = new ArrayList<>(); 092 093 String siteName = site.getName(); 094 String language = sitemap.getSitemapName(); 095 096 for (ModifiableZoneItem zoneItem : _getLinkDirectoryServiceZoneItems(siteName)) 097 { 098 ModelAwareDataHolder serviceParameters = zoneItem.getServiceParameters(); 099 String[] themeIds = serviceParameters.getValue("themes"); 100 for (String themeId : themeIds) 101 { 102 try 103 { 104 String themeLabel = _getThemeLabel(themeId, siteName, language); 105 if (StringUtils.isNotBlank(themeLabel)) 106 { 107 requests.add(new MatomoEvent( 108 language + "/" + themeLabel, 109 _getNbUsersWithCustomLinks(siteName, language, zoneItem.getId()) 110 )); 111 } 112 } 113 catch (UserPreferencesException e) 114 { 115 getLogger().error("An error occurred getting user preferences for link directories. No request is sent to Matomo.", e); 116 } 117 } 118 119 } 120 121 return requests; 122 } 123 124 private AmetysObjectIterable<ModifiableZoneItem> _getLinkDirectoryServiceZoneItems(String siteName) 125 { 126 String xpathQuery = "//element(" + siteName + ", ametys:site)//element(*, ametys:zoneItem)[@ametys-internal:service = 'org.ametys.plugins.linkdirectory.DirectoryService' and ametys:service_parameters/@ametys:configurable = 'true']"; 127 return _resolver.query(xpathQuery); 128 } 129 130 private List<MatomoEvent> _getConfigurableLinkInputDataEvents(Site site, Sitemap sitemap) 131 { 132 List<MatomoEvent> requests = new ArrayList<>(); 133 134 String siteName = site.getName(); 135 String language = sitemap.getSitemapName(); 136 137 // Be careful ! Only themes defined in default configuration file are tracked (skin://conf/link-directory.xml) 138 ConfiguredThemesInputData configuredThemes = _linkDirectoryThemesInputDataHelper.getThemesInputData(site.getSkinId()); 139 String error = configuredThemes.error(); 140 if (StringUtils.isBlank(error) && configuredThemes.themesInputDatas() != null) 141 { 142 for (ThemesInputData themesInputData : configuredThemes.themesInputDatas()) 143 { 144 try 145 { 146 if (themesInputData.configurable()) // Send only configurable themes 147 { 148 String themeId = themesInputData.themes() 149 .stream() 150 .filter(t -> t.get("lang") == null || t.get("lang").equals(language)) 151 .map(t -> t.get("id")) 152 .findFirst() 153 .orElse(null); 154 155 String themeLabel = _getThemeLabel(themeId, siteName, language); 156 if (StringUtils.isNotBlank(themeLabel)) 157 { 158 requests.add(new MatomoEvent( 159 language + "/" + themeLabel, 160 _getNbUsersWithCustomLinks(siteName, language, themesInputData.id()) 161 )); 162 } 163 } 164 } 165 catch (UserPreferencesException e) 166 { 167 getLogger().error("An error occurred getting user preferences for link directories. No request is sent to Matomo.", e); 168 } 169 } 170 } 171 172 return requests; 173 } 174 175 private String _getThemeLabel(String themeId, String siteName, String language) 176 { 177 Tag theme = StringUtils.isNotBlank(themeId) 178 ? _themesDAO.getTag(themeId, Map.of("language", language, "siteName", siteName)) 179 : null; 180 181 return theme != null ? _i18nUtils.translate(theme.getTitle(), language) : null; 182 } 183 184 private long _getNbUsersWithCustomLinks(String siteName, String language, String thematicId) throws UserPreferencesException 185 { 186 String storageContext = siteName + "/" + language + "/" + thematicId; 187 Map<String, String> contextVars = Map.of( 188 FOUserPreferencesConstants.CONTEXT_VAR_SITENAME, siteName, 189 FOUserPreferencesConstants.CONTEXT_VAR_LANGUAGE, language 190 ); 191 192 return _userPreferencesManager.getAllUnTypedUserPrefs(storageContext, contextVars) 193 .values() 194 .stream() 195 .filter(this::_hasCustomLinks) 196 .count(); 197 } 198 199 private boolean _hasCustomLinks(Map<String, String> userPrefs) 200 { 201 String orderedLinksAsString = userPrefs.get(DirectoryHelper.USER_PREF_ORDERED_LINK_ATTR); 202 String[] orderedLinks = StringUtils.split(orderedLinksAsString, ","); 203 204 String hiddenLinksAsString = userPrefs.get(DirectoryHelper.USER_PREF_HIDDEN_LINK_ATTR); 205 String[] hiddenLinks = StringUtils.split(hiddenLinksAsString, ","); 206 207 // Links are customized if there is some ordered links or some hidden links in the users preferences 208 return orderedLinks != null && orderedLinks.length > 0 || hiddenLinks != null && hiddenLinks.length > 0; 209 } 210}