diff --git a/application-licensing-licensor/application-licensing-licensor-api/src/main/java/com/xwiki/licensing/LicensingConfiguration.java b/application-licensing-licensor/application-licensing-licensor-api/src/main/java/com/xwiki/licensing/LicensingConfiguration.java index 4ec1f0cc..6993204c 100644 --- a/application-licensing-licensor/application-licensing-licensor-api/src/main/java/com/xwiki/licensing/LicensingConfiguration.java +++ b/application-licensing-licensor/application-licensing-licensor-api/src/main/java/com/xwiki/licensing/LicensingConfiguration.java @@ -21,8 +21,10 @@ import java.io.File; import java.util.List; +import java.util.Set; import org.xwiki.component.annotation.Role; +import org.xwiki.stability.Unstable; /** * Configuration of the licensing module. @@ -80,4 +82,25 @@ public interface LicensingConfiguration * @return the email of the licensing owner or null if the value of the property is not filled up */ String getLicensingOwnerEmail(); + + /** + * @return {@link List} with the groups whose members need to be notified about the extension + * @since 1.31 + */ + @Unstable + List getNotifiedGroups(); + + /** + * @return {@link Set} with the groups whose members need to be notified about the extension + * @since 1.31 + */ + @Unstable + Set getNotifiedGroupsSet(); + + /** + * @return {@code true} if the context user is member of the groups from {@link #getNotifiedGroups}, or + * {@code false} otherwise + * @since 1.31 + */ + boolean isMemberOfNotifiedGroups(); } diff --git a/application-licensing-licensor/application-licensing-licensor-api/src/main/java/com/xwiki/licensing/internal/DefaultLicensingConfiguration.java b/application-licensing-licensor/application-licensing-licensor-api/src/main/java/com/xwiki/licensing/internal/DefaultLicensingConfiguration.java index 3a191dfd..38b859f7 100644 --- a/application-licensing-licensor/application-licensing-licensor-api/src/main/java/com/xwiki/licensing/internal/DefaultLicensingConfiguration.java +++ b/application-licensing-licensor/application-licensing-licensor-api/src/main/java/com/xwiki/licensing/internal/DefaultLicensingConfiguration.java @@ -20,9 +20,11 @@ package com.xwiki.licensing.internal; import java.io.File; +import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Objects; +import java.util.Set; import java.util.stream.Collectors; import javax.inject.Inject; @@ -33,8 +35,13 @@ import org.xwiki.component.annotation.Component; import org.xwiki.configuration.ConfigurationSource; import org.xwiki.environment.Environment; +import org.xwiki.model.reference.DocumentReference; +import org.xwiki.model.reference.DocumentReferenceResolver; +import com.xpn.xwiki.XWiki; +import com.xpn.xwiki.XWikiContext; import com.xwiki.licensing.LicensingConfiguration; +import com.xwiki.licensing.internal.helpers.LicensingNotificationConfigurationSource; /** * Default implementation of {@link LicensingConfiguration}. @@ -74,6 +81,17 @@ public class DefaultLicensingConfiguration implements LicensingConfiguration @Named("LicensingOwnerConfigurationSource") private ConfigurationSource ownerConfig; + @Inject + @Named(LicensingNotificationConfigurationSource.HINT) + private ConfigurationSource notificationConfig; + + @Inject + @Named("current") + private DocumentReferenceResolver referenceResolver; + + @Inject + private Provider wikiContextProvider; + private File localStorePath; @Override @@ -137,6 +155,28 @@ public String getStoreRenewURL() return this.storeConfig.getProperty("storeRenewURL"); } + @Override + public List getNotifiedGroups() + { + return convertObjectToStringList(notificationConfig.getProperty("notifiedGroups", new ArrayList<>())); + } + + @Override + public Set getNotifiedGroupsSet() + { + return getNotifiedGroups().stream().map(referenceResolver::resolve).map(DocumentReference::toString) + .collect(Collectors.toSet()); + } + + @Override + public boolean isMemberOfNotifiedGroups() + { + List notifiedGroups = getNotifiedGroups(); + XWikiContext wikiContext = wikiContextProvider.get(); + XWiki wiki = wikiContext.getWiki(); + return notifiedGroups.stream().anyMatch(group -> wiki.getUser(wikiContext).isUserInGroup(group)); + } + @SuppressWarnings("unchecked") private List convertObjectToStringList(Object list) { diff --git a/application-licensing-licensor/application-licensing-licensor-api/src/main/java/com/xwiki/licensing/internal/helpers/LicensingNotificationConfigurationSource.java b/application-licensing-licensor/application-licensing-licensor-api/src/main/java/com/xwiki/licensing/internal/helpers/LicensingNotificationConfigurationSource.java new file mode 100644 index 00000000..80c0aea1 --- /dev/null +++ b/application-licensing-licensor/application-licensing-licensor-api/src/main/java/com/xwiki/licensing/internal/helpers/LicensingNotificationConfigurationSource.java @@ -0,0 +1,74 @@ +/* + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * + * This is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this software; if not, write to the Free + * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA, or see the FSF site: http://www.fsf.org. + */ +package com.xwiki.licensing.internal.helpers; + +import java.util.Arrays; +import java.util.List; + +import javax.inject.Named; +import javax.inject.Singleton; + +import org.xwiki.component.annotation.Component; +import org.xwiki.configuration.internal.AbstractDocumentConfigurationSource; +import org.xwiki.model.reference.DocumentReference; +import org.xwiki.model.reference.LocalDocumentReference; + +/** + * Configuration source for notification information. + * + * @version $Id$ + * @since 1.31 + */ +@Component +@Named(LicensingNotificationConfigurationSource.HINT) +@Singleton +public class LicensingNotificationConfigurationSource extends AbstractDocumentConfigurationSource +{ + /** + * Component hint. + */ + public static final String HINT = "LicensingNotificationConfigurationSource"; + + protected static final List CODE_SPACE = Arrays.asList("Licenses", "Code"); + + protected static final LocalDocumentReference LICENSING_CONFIG_DOC = + new LocalDocumentReference(CODE_SPACE, "LicensingConfig"); + + protected static final LocalDocumentReference OWNER_CLASS = + new LocalDocumentReference(CODE_SPACE, "LicensingNotificationClass"); + + @Override + protected DocumentReference getDocumentReference() + { + return new DocumentReference(LICENSING_CONFIG_DOC, this.getCurrentWikiReference()); + } + + @Override + protected LocalDocumentReference getClassReference() + { + return OWNER_CLASS; + } + + @Override + protected String getCacheId() + { + return "licensing.configuration.notification"; + } +} diff --git a/application-licensing-licensor/application-licensing-licensor-api/src/main/java/com/xwiki/licensing/internal/upgrades/NewExtensionVersionAvailableManager.java b/application-licensing-licensor/application-licensing-licensor-api/src/main/java/com/xwiki/licensing/internal/upgrades/NewExtensionVersionAvailableManager.java index ad1ea822..c76047ec 100644 --- a/application-licensing-licensor/application-licensing-licensor-api/src/main/java/com/xwiki/licensing/internal/upgrades/NewExtensionVersionAvailableManager.java +++ b/application-licensing-licensor/application-licensing-licensor-api/src/main/java/com/xwiki/licensing/internal/upgrades/NewExtensionVersionAvailableManager.java @@ -23,8 +23,10 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Set; import javax.inject.Inject; +import javax.inject.Provider; import javax.inject.Singleton; import org.apache.commons.lang3.exception.ExceptionUtils; @@ -34,10 +36,13 @@ import org.xwiki.extension.InstalledExtension; import org.xwiki.extension.repository.InstalledExtensionRepository; import org.xwiki.extension.version.Version; +import org.xwiki.model.reference.DocumentReference; +import org.xwiki.model.reference.EntityReferenceSerializer; import org.xwiki.observation.ObservationManager; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; +import com.xpn.xwiki.XWikiContext; import com.xwiki.licensing.LicensedExtensionManager; import com.xwiki.licensing.LicensingConfiguration; import com.xwiki.licensing.internal.upgrades.notifications.newVersion.NewExtensionVersionAvailableEvent; @@ -74,6 +79,12 @@ public class NewExtensionVersionAvailableManager @Inject private NewVersionNotificationManager newVersionNotificationManager; + @Inject + private EntityReferenceSerializer serializer; + + @Inject + private Provider contextProvider; + /** * Notify the administrators when one of the installed licensed applications has a new version available. Do nothing * for extensions that have auto upgrades enabled. @@ -118,9 +129,10 @@ private void notifyExtensionVersionAvailable(ExtensionId extensionId, String nam extensionInfo.put("extensionName", installedExtension.getName()); extensionInfo.put("namespace", namespaceName); extensionInfo.put("version", installableVersions.get(0).getValue()); + Set notifiedGroups = getTargetGroups(); this.observationManager.notify(new NewExtensionVersionAvailableEvent( - new ExtensionId(extensionId.getId(), installableVersions.get(0)), namespace), + new ExtensionId(extensionId.getId(), installableVersions.get(0)), namespace, notifiedGroups), extensionId.getId(), (new ObjectMapper()).writeValueAsString(extensionInfo)); this.newVersionNotificationManager.markNotificationAsSent(extensionId.getId(), namespaceName, installableVersions.get(0).getValue()); @@ -130,4 +142,13 @@ private void notifyExtensionVersionAvailable(ExtensionId extensionId, String nam extensionId.getId(), ExceptionUtils.getRootCauseMessage(e)); } } + + private Set getTargetGroups() + { + Set notifiedGroups = licensingConfig.getNotifiedGroupsSet(); + DocumentReference adminGroupDocument = + new DocumentReference(contextProvider.get().getWikiId(), "XWiki", "XWikiAdminGroup"); + notifiedGroups.add(serializer.serialize(adminGroupDocument)); + return notifiedGroups; + } } diff --git a/application-licensing-licensor/application-licensing-licensor-api/src/main/java/com/xwiki/licensing/internal/upgrades/UpgradeExtensionHandler.java b/application-licensing-licensor/application-licensing-licensor-api/src/main/java/com/xwiki/licensing/internal/upgrades/UpgradeExtensionHandler.java index 03231c92..446e27ff 100644 --- a/application-licensing-licensor/application-licensing-licensor-api/src/main/java/com/xwiki/licensing/internal/upgrades/UpgradeExtensionHandler.java +++ b/application-licensing-licensor/application-licensing-licensor-api/src/main/java/com/xwiki/licensing/internal/upgrades/UpgradeExtensionHandler.java @@ -22,6 +22,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Set; import javax.inject.Inject; import javax.inject.Named; @@ -46,17 +47,19 @@ import org.xwiki.job.JobException; import org.xwiki.job.JobExecutor; import org.xwiki.localization.ContextualLocalizationManager; +import org.xwiki.model.reference.DocumentReference; import org.xwiki.model.reference.DocumentReferenceResolver; import org.xwiki.model.reference.EntityReferenceSerializer; import org.xwiki.observation.ObservationManager; +import com.xwiki.licensing.LicensingConfiguration; import com.xwiki.licensing.internal.upgrades.notifications.ExtensionAutoUpgradedEvent; import com.xwiki.licensing.internal.upgrades.notifications.ExtensionAutoUpgradedFailedEvent; /** * Upgrades an extension from a namespace to the last compatible version and sends a notification. The notification will * be displayed only for users that subscribed to it and disabled the System filter since it is send by superadmin user. - * + * * @version $Id$ * @since 1.17 */ @@ -95,9 +98,13 @@ public class UpgradeExtensionHandler @Inject private EntityReferenceSerializer serializer; + @Inject + private LicensingConfiguration licensingConfig; + + /** * Try upgrading an extension inside a namespace to the last compatible version. - * + * * @param installedExtension the already installed extension * @param namespace the namespace in which the extension is installed */ @@ -115,8 +122,9 @@ public void tryUpgradeExtensionToLastVersion(InstalledExtension installedExtensi String doneUpgradeMessage = this.localization.getTranslationPlain( "licensor.notification.autoUpgrade.done", installedExtension.getName(), installedExtensionId.getVersion().getValue(), toInstallExtensionId.getVersion().getValue()); - - this.observationManager.notify(new ExtensionAutoUpgradedEvent(), LICENSOR_API_ID, doneUpgradeMessage); + Set notifiedGroups = getTargetGroups(); + this.observationManager.notify(new ExtensionAutoUpgradedEvent(notifiedGroups), LICENSOR_API_ID, + doneUpgradeMessage); // If the execution gets here, it means that the upgrade was done. break; @@ -131,9 +139,18 @@ public void tryUpgradeExtensionToLastVersion(InstalledExtension installedExtensi } } + private Set getTargetGroups() + { + Set notifiedGroups = licensingConfig.getNotifiedGroupsSet(); + DocumentReference adminGroupDoc = currentDocumentReferenceResolver.resolve("XWiki.XWikiAdminGroup"); + String adminGroup = serializer.serialize(adminGroupDoc); + notifiedGroups.add(adminGroup); + return notifiedGroups; + } + /** * Install the given extension inside a namespace. - * + * * @param extensionId extension to install * @param namespace namespace where the install is done * @throws JobException error at job execution @@ -183,7 +200,7 @@ protected InstallRequest getInstallRequest(ExtensionId extensionId, String names /** * Get the reversed list of versions that can be installed, considering the already installed version. - * + * * @param extensionId ExtensionId of the application that is needed * @return reversed list of versions until the already installed one */ diff --git a/application-licensing-licensor/application-licensing-licensor-api/src/main/java/com/xwiki/licensing/internal/upgrades/notifications/ExtensionAutoUpgradedEvent.java b/application-licensing-licensor/application-licensing-licensor-api/src/main/java/com/xwiki/licensing/internal/upgrades/notifications/ExtensionAutoUpgradedEvent.java index ddb951a1..3414cc5e 100644 --- a/application-licensing-licensor/application-licensing-licensor-api/src/main/java/com/xwiki/licensing/internal/upgrades/notifications/ExtensionAutoUpgradedEvent.java +++ b/application-licensing-licensor/application-licensing-licensor-api/src/main/java/com/xwiki/licensing/internal/upgrades/notifications/ExtensionAutoUpgradedEvent.java @@ -19,24 +19,54 @@ */ package com.xwiki.licensing.internal.upgrades.notifications; -import org.xwiki.eventstream.RecordableEvent; +import java.util.HashSet; +import java.util.Set; + +import org.xwiki.eventstream.TargetableEvent; /** * The event send when an application is automatically upgraded. Used in UpgradeExtensionHandler. - * + * * @version $Id$ * @since 1.17 */ -public class ExtensionAutoUpgradedEvent implements RecordableEvent +public class ExtensionAutoUpgradedEvent implements TargetableEvent { /** * The event type used for this component. */ public static final String EVENT_TYPE = "ExtensionAutoUpgradedEvent"; + private Set notifiedGroups; + + /** + * The default constructor. + */ + public ExtensionAutoUpgradedEvent() + { + notifiedGroups = new HashSet<>(); + } + + /** + * Created a new instance with the given data. + * + * @param notifiedGroups the groups that should be notified about the new upgrade. An empty {@link Set} means + * all users will be notified, no matter the group + */ + public ExtensionAutoUpgradedEvent(Set notifiedGroups) + { + this.notifiedGroups = notifiedGroups; + } + @Override public boolean matches(Object otherEvent) { return otherEvent instanceof ExtensionAutoUpgradedEvent; } + + @Override + public Set getTarget() + { + return notifiedGroups; + } } diff --git a/application-licensing-licensor/application-licensing-licensor-api/src/main/java/com/xwiki/licensing/internal/upgrades/notifications/newVersion/NewExtensionVersionAvailableEvent.java b/application-licensing-licensor/application-licensing-licensor-api/src/main/java/com/xwiki/licensing/internal/upgrades/notifications/newVersion/NewExtensionVersionAvailableEvent.java index fe4bd431..4a26d577 100644 --- a/application-licensing-licensor/application-licensing-licensor-api/src/main/java/com/xwiki/licensing/internal/upgrades/notifications/newVersion/NewExtensionVersionAvailableEvent.java +++ b/application-licensing-licensor/application-licensing-licensor-api/src/main/java/com/xwiki/licensing/internal/upgrades/notifications/newVersion/NewExtensionVersionAvailableEvent.java @@ -19,16 +19,19 @@ */ package com.xwiki.licensing.internal.upgrades.notifications.newVersion; -import org.xwiki.eventstream.RecordableEvent; +import java.util.Collections; +import java.util.Set; + +import org.xwiki.eventstream.TargetableEvent; import org.xwiki.extension.ExtensionId; /** * The event send when a new version of a licensed extension is available. - * + * * @version $Id$ * @since 1.23 */ -public class NewExtensionVersionAvailableEvent implements RecordableEvent +public class NewExtensionVersionAvailableEvent implements TargetableEvent { /** * The name of this component. @@ -39,6 +42,8 @@ public class NewExtensionVersionAvailableEvent implements RecordableEvent private String namespace; + private Set notifiedGroups = Collections.emptySet(); + /** * The default constructor. */ @@ -51,7 +56,7 @@ public NewExtensionVersionAvailableEvent() * * @param extensionId the extension id of the new extension version detected * @param namespace the namespace where the new extension version was detected, where {@code null} means root - * namespace (i.e. all namespaces) + * namespace (i.e. all namespaces) */ public NewExtensionVersionAvailableEvent(ExtensionId extensionId, String namespace) { @@ -59,6 +64,22 @@ public NewExtensionVersionAvailableEvent(ExtensionId extensionId, String namespa this.namespace = namespace; } + /** + * See {@link #NewExtensionVersionAvailableEvent(ExtensionId, String)}. + * + * @param extensionId the extension id of the new extension version detected + * @param namespace the namespace where the new extension version was detected, where {@code null} means root + * namespace (i.e. all namespaces) + * @param notifiedGroups the groups that should be notified about the new version. An empty {@link Set} means + * all users will be notified, no matter the group + */ + public NewExtensionVersionAvailableEvent(ExtensionId extensionId, String namespace, Set notifiedGroups) + { + this.extensionId = extensionId; + this.namespace = namespace; + this.notifiedGroups = notifiedGroups; + } + @Override public boolean matches(Object otherEvent) { @@ -75,10 +96,16 @@ public ExtensionId getExtensionId() /** * @return the namespace where the new extension version was detected. {@code null} means root namespace (i.e all - * namespaces) + * namespaces) */ public String getNamespace() { return this.namespace; } + + @Override + public Set getTarget() + { + return notifiedGroups; + } } diff --git a/application-licensing-licensor/application-licensing-licensor-api/src/main/java/com/xwiki/licensing/script/LicensorScriptService.java b/application-licensing-licensor/application-licensing-licensor-api/src/main/java/com/xwiki/licensing/script/LicensorScriptService.java index 9993652f..ffc35494 100644 --- a/application-licensing-licensor/application-licensing-licensor-api/src/main/java/com/xwiki/licensing/script/LicensorScriptService.java +++ b/application-licensing-licensor/application-licensing-licensor-api/src/main/java/com/xwiki/licensing/script/LicensorScriptService.java @@ -27,7 +27,6 @@ import javax.inject.Named; import javax.inject.Singleton; -import com.xwiki.licensing.LicensedExtensionManager; import org.apache.commons.lang3.exception.ExceptionUtils; import org.slf4j.Logger; import org.xwiki.bridge.DocumentAccessBridge; @@ -51,6 +50,8 @@ import com.xwiki.licensing.License; import com.xwiki.licensing.LicenseManager; +import com.xwiki.licensing.LicensedExtensionManager; +import com.xwiki.licensing.LicensingConfiguration; import com.xwiki.licensing.Licensor; import com.xwiki.licensing.internal.UserCounter; import com.xwiki.licensing.internal.enforcer.LicensingUtils; @@ -99,6 +100,9 @@ public class LicensorScriptService implements ScriptService, Initializable @Inject private LicensedExtensionManager licensedExtensionManager; + @Inject + private LicensingConfiguration licensingConfig; + @Override public void initialize() throws InitializationException { @@ -107,6 +111,18 @@ public void initialize() throws InitializationException } } + /** + * Check if the current user is a member of the groups targeted by licensor notifications. + * + * @return {@code true} if the user is member of the target groups, or {@code false} otherwise + * @since 1.31 + */ + @Unstable + public boolean isMemberOfNotifiedGroups() + { + return this.licensingConfig.isMemberOfNotifiedGroups(); + } + /** * Retrieve the currently applicable license for the current context document if any. Equivalent to * licensor.getLicense() call. diff --git a/application-licensing-licensor/application-licensing-licensor-api/src/main/resources/META-INF/components.txt b/application-licensing-licensor/application-licensing-licensor-api/src/main/resources/META-INF/components.txt index 742ae1fc..dd8e06f1 100644 --- a/application-licensing-licensor/application-licensing-licensor-api/src/main/resources/META-INF/components.txt +++ b/application-licensing-licensor/application-licensing-licensor-api/src/main/resources/META-INF/components.txt @@ -17,6 +17,7 @@ com.xwiki.licensing.script.LicensorScriptService com.xwiki.licensing.internal.GetTrialLicenseListener com.xwiki.licensing.internal.TrialLicenseGenerator com.xwiki.licensing.internal.LicensedDependenciesMap +com.xwiki.licensing.internal.helpers.LicensingNotificationConfigurationSource com.xwiki.licensing.internal.helpers.LicensingStoreConfigurationSource com.xwiki.licensing.internal.helpers.LicensingOwnerConfigurationSource com.xwiki.licensing.internal.helpers.HttpClientUtils diff --git a/application-licensing-licensor/application-licensing-licensor-api/src/test/java/com/xwiki/licensing/internal/DefaultLicensingConfigurationTest.java b/application-licensing-licensor/application-licensing-licensor-api/src/test/java/com/xwiki/licensing/internal/DefaultLicensingConfigurationTest.java index 76d0ed38..abc8b492 100644 --- a/application-licensing-licensor/application-licensing-licensor-api/src/test/java/com/xwiki/licensing/internal/DefaultLicensingConfigurationTest.java +++ b/application-licensing-licensor/application-licensing-licensor-api/src/test/java/com/xwiki/licensing/internal/DefaultLicensingConfigurationTest.java @@ -19,14 +19,11 @@ */ package com.xwiki.licensing.internal; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.fail; -import static org.mockito.Mockito.when; - import java.io.File; import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.Set; import javax.inject.Named; import javax.inject.Provider; @@ -36,11 +33,17 @@ import org.mockito.Mock; import org.xwiki.configuration.ConfigurationSource; import org.xwiki.environment.Environment; +import org.xwiki.model.reference.DocumentReference; +import org.xwiki.model.reference.DocumentReferenceResolver; import org.xwiki.test.junit5.mockito.ComponentTest; import org.xwiki.test.junit5.mockito.InjectMockComponents; import org.xwiki.test.junit5.mockito.MockComponent; -import com.xpn.xwiki.objects.BaseObject; +import com.xwiki.licensing.internal.helpers.LicensingNotificationConfigurationSource; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; +import static org.mockito.Mockito.when; @ComponentTest class DefaultLicensingConfigurationTest @@ -52,15 +55,29 @@ class DefaultLicensingConfigurationTest @Named("LicensedExtensionAutomaticUpgrades") private ConfigurationSource autoUpgradesConfig; + @MockComponent + @Named(LicensingNotificationConfigurationSource.HINT) + private ConfigurationSource notificationConfig; + @MockComponent private Environment environment; @MockComponent private Provider configurationSourceProvider; + @MockComponent + @Named("current") + private DocumentReferenceResolver referenceResolver; + @Mock private ConfigurationSource configurationSource; + @Mock + private DocumentReference doc1; + + @Mock + private DocumentReference doc2; + @BeforeEach void configure() { @@ -99,6 +116,51 @@ void getAllowListWithEmptyList() throws Exception assertEquals(Collections.emptyList(), this.licensingConfiguration.getAutoUpgradeAllowList()); } + @Test + void getNotifiedGroups() throws Exception + { + // Since getProperty method returns a list of objects, we check also that the conversion to string is done + // correctly. + List allowlist = Arrays.asList("testGroup1", "testGroup2"); + + when(this.notificationConfig.getProperty("notifiedGroups", List.of())).thenReturn(allowlist); + + assertEquals(Arrays.asList("testGroup1", "testGroup2"), + this.licensingConfiguration.getNotifiedGroups()); + } + + @Test + void getNotifiedGroupsWithEmptyList() throws Exception + { + when(this.notificationConfig.getProperty("notifiedGroups", List.of())).thenReturn(List.of()); + + assertEquals(Collections.emptyList(), this.licensingConfiguration.getNotifiedGroups()); + } + + @Test + void getNotifiedGroupsSet() throws Exception + { + // Since getProperty method returns a list of objects, we check also that the conversion to string is done + // correctly. + List allowlist = Arrays.asList("testGroup1", "testGroup2"); + + when(this.notificationConfig.getProperty("notifiedGroups", List.of())).thenReturn(allowlist); + when(this.referenceResolver.resolve("testGroup1")).thenReturn(doc1); + when(this.referenceResolver.resolve("testGroup2")).thenReturn(doc2); + when(this.doc1.toString()).thenReturn("serializedRef1"); + when(this.doc2.toString()).thenReturn("serializedRef2"); + assertEquals(Set.of("serializedRef1", "serializedRef2"), + this.licensingConfiguration.getNotifiedGroupsSet()); + } + + @Test + void getNotifiedGroupsSetWithEmptyList() throws Exception + { + when(this.notificationConfig.getProperty("notifiedGroups", List.of())).thenReturn(List.of()); + + assertEquals(Collections.emptySet(), this.licensingConfiguration.getNotifiedGroupsSet()); + } + @Test void getLocalStorePath() throws Exception { diff --git a/application-licensing-licensor/application-licensing-licensor-api/src/test/java/com/xwiki/licensing/internal/upgrades/NewExtensionVersionAvailableManagerTest.java b/application-licensing-licensor/application-licensing-licensor-api/src/test/java/com/xwiki/licensing/internal/upgrades/NewExtensionVersionAvailableManagerTest.java index 26d09a6e..63690ebf 100644 --- a/application-licensing-licensor/application-licensing-licensor-api/src/test/java/com/xwiki/licensing/internal/upgrades/NewExtensionVersionAvailableManagerTest.java +++ b/application-licensing-licensor/application-licensing-licensor-api/src/test/java/com/xwiki/licensing/internal/upgrades/NewExtensionVersionAvailableManagerTest.java @@ -29,6 +29,8 @@ import java.util.Arrays; import java.util.Collections; +import javax.inject.Provider; + import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.Mock; @@ -42,6 +44,7 @@ import org.xwiki.test.junit5.mockito.InjectMockComponents; import org.xwiki.test.junit5.mockito.MockComponent; +import com.xpn.xwiki.XWikiContext; import com.xwiki.licensing.LicensedExtensionManager; import com.xwiki.licensing.LicensingConfiguration; import com.xwiki.licensing.internal.upgrades.notifications.newVersion.NewExtensionVersionAvailableEvent; @@ -76,6 +79,12 @@ public class NewExtensionVersionAvailableManagerTest @MockComponent private NewVersionNotificationManager newVersionNotificationManager; + @MockComponent + private Provider wikiContextProvider; + + @MockComponent + private XWikiContext wikiContext; + @Mock private InstalledExtension installedExtension1; @@ -99,6 +108,9 @@ public void configure() throws Exception when(this.installedExtension2.getName()).thenReturn("Application 2"); when(this.installedExtension2.getId()).thenReturn(extensionId2); when(this.installedRepository.getInstalledExtension(this.extensionId2)).thenReturn(this.installedExtension2); + + when(wikiContextProvider.get()).thenReturn(wikiContext); + when(wikiContext.getWikiId()).thenReturn("xwiki"); } @Test diff --git a/application-licensing-licensor/application-licensing-licensor-ui/src/main/resources/Licenses/Code/LicensesNotificationsUIX.xml b/application-licensing-licensor/application-licensing-licensor-ui/src/main/resources/Licenses/Code/LicensesNotificationsUIX.xml index 282d44b5..17880b26 100644 --- a/application-licensing-licensor/application-licensing-licensor-ui/src/main/resources/Licenses/Code/LicensesNotificationsUIX.xml +++ b/application-licensing-licensor/application-licensing-licensor-ui/src/main/resources/Licenses/Code/LicensesNotificationsUIX.xml @@ -36,9 +36,25 @@ false xwiki/2.1 true - {{include reference="Licenses.Code.VelocityMacros" /}} - -{{velocity output="false"}} + {{velocity output="false"}} +#set ($licensorId = 'com.xwiki.licensing:application-licensing-licensor-api') +#macro (getPaidExtensions) + #set ($paidExtensionsByNamespace = $services.extension.installed.getBackwardDependencies($licensorId)) + #set ($paidExtensions = $collectiontool.set) + #foreach ($paidExtensionsOnNamespace in $paidExtensionsByNamespace.values()) + #set ($discard = $paidExtensions.addAll($paidExtensionsOnNamespace)) + #end +#end +#macro (getVisiblePaidExtensions) + #getPaidExtensions + #set ($mandatoryExtensions = $services.licensing.licensor.getLicensedExtensionManager().getMandatoryLicensedExtensions()) + #set ($visiblePaidExtensions = $collectiontool.set) + #foreach ($paidExtension in $paidExtensions) + #if ($mandatoryExtensions.contains($paidExtension.getId())) + #set ($discard = $visiblePaidExtensions.add($paidExtension)) + #end + #end +#end #macro(renderNotification $btnClass $notificationClass $translation $extensionNames $showMore) <$licensesNotificationWrapperTagName class="licenses-notification $notificationClass"> <p>$escapetool.xml($services.localization.render($translation))</p> @@ -56,7 +72,7 @@ {{/velocity}} {{velocity}} -#if ($hasAdmin && $hasProgramming) +#if ($services.licensing.licensor.isMemberOfNotifiedGroups() || $isAdvancedUser || $hasAdmin) #getVisiblePaidExtensions ## The same extension could be installed on multiple namespaces (wikis) with different versions. #set ($invalidExtensionNames = $collectiontool.sortedSet) @@ -483,17 +499,17 @@ - + 0 - + 0 {{velocity}} -#if ($services.security.authorization.hasAccess('view', 'Licenses.Code.LicensesNotificationsUIX')) +#if ($services.licensing.licensor.isMemberOfNotifiedGroups() || $isAdvancedUser || $hasAdmin) #set ($discard = $xwiki.jsx.use('Licenses.Code.LicensesNotificationsUIX')) #set ($licensesNotificationWrapperTagName = 'li') {{include reference="Licenses.Code.LicensesNotificationsUIX" /}} @@ -518,4 +534,87 @@ separator:true wiki + + Licenses.Code.LicensesNotificationsUIX + 0 + XWiki.XWikiRights + ee662140-f6d8-4bc9-b514-fc20315e4284 + + XWiki.XWikiRights + + + + + + + + + 1 + 0 + select + allow + allow + 4 + Allow/Deny + 0 + com.xpn.xwiki.objects.classes.BooleanClass + + + 0 + 0 + input + 1 + groups + 1 + 1 + Groups + 0 + + 5 + 0 + com.xpn.xwiki.objects.classes.GroupsClass + + + 0 + 0 + select + 1 + levels + 2 + Levels + 0 + + 3 + 0 + com.xpn.xwiki.objects.classes.LevelsClass + + + 0 + 0 + input + 1 + users + 3 + 1 + Users + 0 + + 5 + 0 + com.xpn.xwiki.objects.classes.UsersClass + + + + 1 + + + XWiki.XWikiAllGroup + + + view + + + + + diff --git a/application-licensing-licensor/application-licensing-licensor-ui/src/main/resources/Licenses/Code/LicensingConfig.xml b/application-licensing-licensor/application-licensing-licensor-ui/src/main/resources/Licenses/Code/LicensingConfig.xml index 401c5663..c80749a0 100644 --- a/application-licensing-licensor/application-licensing-licensor-ui/src/main/resources/Licenses/Code/LicensingConfig.xml +++ b/application-licensing-licensor/application-licensing-licensor-ui/src/main/resources/Licenses/Code/LicensingConfig.xml @@ -81,6 +81,50 @@ + + Licenses.Code.LicensingConfig + 0 + Licenses.Code.LicensingNotificationClass + 7520997e-c29d-4783-acae-4a809c71951a + + Licenses.Code.LicensingNotificationClass + + + + + + + + + 0 + + XWiki.XWikiAdminGroup + 0 + input + discouraged + + 0 + 1 + notifiedGroups + 1 + 1 + Notified groups + 0 + + + 20 + + 0 + 0 + + + com.xpn.xwiki.objects.classes.GroupsClass + + + + XWiki.XWikiAdminGroup + + Licenses.Code.LicensingConfig 0 @@ -98,6 +142,7 @@ 0 + email 3 0 @@ -111,6 +156,7 @@ 0 + firstName 1 0 @@ -124,6 +170,7 @@ 0 + lastName 2 0 diff --git a/application-licensing-licensor/application-licensing-licensor-ui/src/main/resources/Licenses/Code/LicensingNotificationClass.xml b/application-licensing-licensor/application-licensing-licensor-ui/src/main/resources/Licenses/Code/LicensingNotificationClass.xml new file mode 100644 index 00000000..382e6381 --- /dev/null +++ b/application-licensing-licensor/application-licensing-licensor-ui/src/main/resources/Licenses/Code/LicensingNotificationClass.xml @@ -0,0 +1,125 @@ + + + + + + Licenses.Code + LicensingNotificationClass + + + 0 + xwiki:XWiki.Admin + Licenses.Code.LicensingOwnerClass + xwiki:XWiki.Admin + xwiki:XWiki.Admin + 1.1 + LicensingNotificationClass + + false + xwiki/2.1 + true + {{velocity}} +## Replace the default space with the space where you want your documents to be created. +## Replace the default parent with the one of your choice and save the document. +## +#set ($defaultSpace = $doc.space) +#set ($defaultParent = $doc.fullName) +{{/velocity}} + + Licenses.Code.LicensingNotificationClass + + + + + + + + + 0 + + XWiki.XWikiAdminGroup + 0 + input + discouraged + + 0 + 1 + notifiedGroups + 1 + 1 + Notified groups + 0 + + + 20 + + 0 + 0 + + + com.xpn.xwiki.objects.classes.GroupsClass + + + + Licenses.Code.LicensingNotificationClass + 0 + XWiki.ClassSheetBinding + 1246ba4b-2a01-40ec-9bd4-67efb35b01fa + + XWiki.ClassSheetBinding + + + + + + + + + 0 + + + 0 + input + + + 0 + sheet + 1 + 1 + Sheet + 0 + + + 30 + none + + 0 + + + + com.xpn.xwiki.objects.classes.PageClass + + + + XWiki.ClassSheet + + + diff --git a/application-licensing-licensor/application-licensing-licensor-ui/src/main/resources/Licenses/Code/Translations.fr.xml b/application-licensing-licensor/application-licensing-licensor-ui/src/main/resources/Licenses/Code/Translations.fr.xml index 54cfa8bb..d6d3ab69 100644 --- a/application-licensing-licensor/application-licensing-licensor-ui/src/main/resources/Licenses/Code/Translations.fr.xml +++ b/application-licensing-licensor/application-licensing-licensor-ui/src/main/resources/Licenses/Code/Translations.fr.xml @@ -76,6 +76,9 @@ licensor.ownerDetails.instanceId.hint=Identifiant unique de vos licences associ licensor.ownerDetails.missingData=Ce champ est requis ! licensor.ownerDetails.heading=Propriétaire de la licence licensor.ownerDetails.hint=Remplissez les champs suivants afin d'acheter une licence ou d'obtenir une licence d'essai. +Licenses.Code.LicensingNotificationClass_notifiedGroups=Groupes notifiés +Licenses.Code.LicensingNotificationClass_notifiedGroups.hint=Sélectionnez les groupes d’utilisateurs qui seront notifiés des événements du licencié. Notez que les groupes sélectionnés doivent disposer à la fois des droits d’administration et de programmation.licensor.notificationSettings.heading=Paramètres de notification +licensor.notificationSettings.hint=Gérer les notifications de licences licensor.licenseManager.heading=Extensions sous licence licensor.licenseManager.hint=Voici la liste des extensions installées dans votre wiki qui nécessitent l'activation d'une licence. Vous pouvez acheter une licence ou obtenir une licence d'essai de 10 jours qui ne peut être prolongée qu'une seule fois de 10 jours. licensor.expiringLicenses=Les extensions suivantes sont sur le point d'expirer : diff --git a/application-licensing-licensor/application-licensing-licensor-ui/src/main/resources/Licenses/Code/Translations.xml b/application-licensing-licensor/application-licensing-licensor-ui/src/main/resources/Licenses/Code/Translations.xml index 269148ae..c9def931 100644 --- a/application-licensing-licensor/application-licensing-licensor-ui/src/main/resources/Licenses/Code/Translations.xml +++ b/application-licensing-licensor/application-licensing-licensor-ui/src/main/resources/Licenses/Code/Translations.xml @@ -90,6 +90,10 @@ licensor.ownerDetails.instanceId.hint=Unique identifier for your XWiki instance. licensor.ownerDetails.missingData=This field is required! licensor.ownerDetails.heading=License Ownership licensor.ownerDetails.hint=Fill in the following fields in order to buy a license or to get a trial one. +Licenses.Code.LicensingNotificationClass_notifiedGroups=Notified groups +Licenses.Code.LicensingNotificationClass_notifiedGroups.hint=Select which user groups will be notified about licensor events. Note that the selected groups need to have both admin and programming rights. +licensor.notificationSettings.heading=Notification settings +licensor.notificationSettings.hint=Manage licenses notifications licensor.licenseManager.heading=Licensed Extensions licensor.licenseManager.hint=Here's a list of extensions installed in your wiki that require a license to be activated. You can buy a license or get a 10-day trial license that can be extended only once with 10 days. A license covers all versions of the licensed extension and is shared by the main wiki and all its subwikis. A licensed extension is listed multiple times if different versions are installed on different subwikis, but all those versions will share the same license. For each extension you can also enable the automatic upgrade to the lastest available stable version. licensor.licenseManager.installedAsDependency.hint=Extensions marked by this sign were installed as dependencies of other extensions and their license will be covered by the parent license diff --git a/application-licensing-licensor/application-licensing-licensor-ui/src/main/resources/Licenses/Code/VelocityMacros.xml b/application-licensing-licensor/application-licensing-licensor-ui/src/main/resources/Licenses/Code/VelocityMacros.xml index 37d32f45..ce64b50a 100644 --- a/application-licensing-licensor/application-licensing-licensor-ui/src/main/resources/Licenses/Code/VelocityMacros.xml +++ b/application-licensing-licensor/application-licensing-licensor-ui/src/main/resources/Licenses/Code/VelocityMacros.xml @@ -44,25 +44,9 @@ #set ($storeAccessURL = $licensingConfigDoc.storeAccessURL) #set ($ownerObj = $licensingConfigDoc.getObject('Licenses.Code.LicensingOwnerClass')) #set ($userCount = $services.licensing.licensor.userCount) - -#macro (getPaidExtensions) - #set ($paidExtensionsByNamespace = $services.extension.installed.getBackwardDependencies($licensorId)) - #set ($paidExtensions = $collectiontool.set) - #foreach ($paidExtensionsOnNamespace in $paidExtensionsByNamespace.values()) - #set ($discard = $paidExtensions.addAll($paidExtensionsOnNamespace)) - #end -#end - -#macro (getVisiblePaidExtensions) - #getPaidExtensions - #set ($mandatoryExtensions = $services.licensing.licensor.getLicensedExtensionManager().getMandatoryLicensedExtensions()) - #set ($visiblePaidExtensions = $collectiontool.set) - #foreach ($paidExtension in $paidExtensions) - #if ($mandatoryExtensions.contains($paidExtension.getId())) - #set ($discard = $visiblePaidExtensions.add($paidExtension)) - #end - #end -#end +#set ($notificationClass = 'Licenses.Code.LicensingNotificationClass') +#includeMacros("XWiki.AdminFieldsDisplaySheet") +#includeMacros("Licenses.Code.LicensesNotificationsUIX") #macro (getLicensesAdminSectionURL) ## We target the main wiki explicitly because licenses are managed globally. @@ -127,6 +111,57 @@ {{/html}} #end +#macro (getLicensingNotificationClassFields) + #set ($obj = $licensingConfigDoc.getObject($notificationClass)) + #foreach ($prop in $obj.properties) + #set ($field = $obj.xWikiClass.get($prop.name)) + #__displayXProperty($field) + #end +#end + +#macro (displayNotificationsConfiguration) + #set ($section = $request.section) + #if ($section == 'Licenses') + #set($formId = "${section.toLowerCase()}_${notificationClass}") + {{html clean=false}} + <form id="$escapetool.xml($formId)" method="post" + action="$escapetool.xml($xwiki.getURL($licensingConfigDoc, 'saveandcontinue'))" + class="xform"> + <fieldset class="licensingnotification"> + <legend> + $escapetool.xml($services.localization.render('licensor.notificationSettings.heading')) + </legend> + <p>$escapetool.xml($services.localization.render('licensor.notificationSettings.hint'))</p> + <input type="hidden" name="form_token" value="$!escapetool.xml($services.csrf.token)" /> + <dl> + #getLicensingNotificationClassFields() + </dl> + </fieldset> + #notificationSubmitButton() + </form> + {{/html}} + #end +#end + +#macro (notificationSubmitButton) + <div class="hidden"> + <input type="hidden" name="form_token" value="$!{services.csrf.getToken()}" /> + <input type="hidden" name="xcontinue" value="$xwiki.getURL($currentDoc, 'admin', + "editor=${escapetool.url(${editor})}&section=${escapetool.url(${section})}&" + + "space=${escapetool.url(${currentSpace})}")" /> + <input type="hidden" name="xredirect" value="$xwiki.getURL($currentDoc, 'admin', + "editor=${escapetool.url(${editor})}&section=${escapetool.url(${section})}&" + + "space=${escapetool.url(${currentSpace})}")" /> + <input type="hidden" name="classname" value="$escapetool.xml($configClassName)" /> + </div> + <div class="bottombuttons"> + <p class="admin-buttons"> + <span class="buttonwrapper"><input class="button" type="submit" name="formactionsac" + value="$escapetool.xml($services.localization.render('admin.save'))" /></span> + </p> + </div> +#end + #macro (licenseButton $licenseType $cssClass $iconName $key $jsonData) #set ($jsonData.licenseType = $licenseType) #set ($label = $escapetool.xml($services.localization.render("licensor.${key}License.label"))) diff --git a/application-licensing-licensor/application-licensing-licensor-ui/src/main/resources/Licenses/WebHome.xml b/application-licensing-licensor/application-licensing-licensor-ui/src/main/resources/Licenses/WebHome.xml index faa8240e..ab3fd9af 100644 --- a/application-licensing-licensor/application-licensing-licensor-ui/src/main/resources/Licenses/WebHome.xml +++ b/application-licensing-licensor/application-licensing-licensor-ui/src/main/resources/Licenses/WebHome.xml @@ -108,6 +108,7 @@ #else #set ($discard = $xwiki.ssx.use('Licenses.WebHome')) #set ($discard = $xwiki.jsx.use('Licenses.WebHome')) + #displayNotificationsConfiguration #displayOwnerDetailsForm #displayLicensesLiveTable #displayAddLicenseForm