package com.atlassian.labs.speakeasy.manager; import com.atlassian.event.api.EventPublisher; import com.atlassian.labs.speakeasy.SpeakeasyServiceImpl; import com.atlassian.labs.speakeasy.data.SpeakeasyData; import com.atlassian.labs.speakeasy.descriptor.DescriptorGeneratorManagerImpl; import com.atlassian.labs.speakeasy.event.ExtensionEnabledEvent; import com.atlassian.labs.speakeasy.event.ExtensionEnabledGloballyEvent; import com.atlassian.labs.speakeasy.event.ExtensionFavoritedEvent; import com.atlassian.labs.speakeasy.event.ExtensionForkedEvent; import com.atlassian.labs.speakeasy.event.ExtensionInstalledEvent; import com.atlassian.labs.speakeasy.event.ExtensionUnfavoritedEvent; import com.atlassian.labs.speakeasy.event.ExtensionUninstalledEvent; import com.atlassian.labs.speakeasy.event.ExtensionUpdatedEvent; import com.atlassian.labs.speakeasy.external.UnauthorizedAccessException; import com.atlassian.labs.speakeasy.model.Extension; import com.atlassian.labs.speakeasy.model.Feedback; import com.atlassian.labs.speakeasy.model.UserExtension; import com.atlassian.labs.speakeasy.product.EmailOptions; import com.atlassian.labs.speakeasy.product.ProductAccessor; import com.atlassian.labs.speakeasy.util.exec.Operation; import com.atlassian.plugin.Plugin; import com.atlassian.plugin.PluginAccessor; import com.atlassian.sal.api.user.UserManager; import com.atlassian.sal.api.user.UserProfile; import com.google.common.base.Function; import com.google.common.collect.Lists; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import java.io.File; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Set; import static com.google.common.collect.Lists.newArrayList; import static com.google.common.collect.Lists.transform; /** * */ @Component public class ExtensionOperationManager { private final PluginAccessor pluginAccessor; private final SpeakeasyData data; private final PluginSystemManager pluginSystemManager; private final ProductAccessor productAccessor; private final DescriptorGeneratorManagerImpl descriptorGeneratorManager; private final UserManager userManager; private final EventPublisher eventPublisher; private final ExtensionBuilder extensionBuilder; private final ExtensionManager extensionManager; private static final Logger log = LoggerFactory.getLogger(SpeakeasyServiceImpl.class); @Autowired public ExtensionOperationManager(PluginAccessor pluginAccessor, SpeakeasyData data, PluginSystemManager pluginSystemManager, ProductAccessor productAccessor, DescriptorGeneratorManagerImpl descriptorGeneratorManager, UserManager userManager, EventPublisher eventPublisher, ExtensionBuilder extensionBuilder, ExtensionManager extensionManager) { this.pluginAccessor = pluginAccessor; this.data = data; this.pluginSystemManager = pluginSystemManager; this.productAccessor = productAccessor; this.descriptorGeneratorManager = descriptorGeneratorManager; this.userManager = userManager; this.eventPublisher = eventPublisher; this.extensionBuilder = extensionBuilder; this.extensionManager = extensionManager; } public List<String> enable(Extension enabledPlugin, String user, boolean sendNotification) throws UnauthorizedAccessException { List<String> affectedPluginKeys = new ArrayList<String>(); String pluginKey = enabledPlugin.getKey(); List<String> accessList = data.getUsersList(pluginKey); // don't allow enabling of forks of global extensions if (data.isGlobalExtension(enabledPlugin.getForkedPluginKey())) { return affectedPluginKeys; } // only enable if not already enabled if (!accessList.contains(user)) { accessList.add(user); data.saveUsersList(pluginKey, accessList); descriptorGeneratorManager.refreshGeneratedDescriptorsForPlugin(pluginKey); affectedPluginKeys.add(pluginKey); } // clear other allowed forks clearEnabledForks(enabledPlugin, user, affectedPluginKeys); if (sendNotification) { sendEnabledEmail(enabledPlugin, user); } eventPublisher.publish(new ExtensionEnabledEvent(pluginKey) .setUserName(user) .setUserEmail(userManager.getUserProfile(user).getEmail())); return affectedPluginKeys; } private void clearEnabledForks(Extension enabledPlugin, String user, List<String> affectedPluginKeys) { String parentKey = enabledPlugin.getForkedPluginKey() != null ? enabledPlugin.getForkedPluginKey() : enabledPlugin.getKey(); for (Plugin plugin : extensionManager.getAllExtensionPlugins()) { if (!plugin.getKey().equals(enabledPlugin.getKey()) && (plugin.getKey().equals(parentKey) || parentKey.equals(Extension.getForkedPluginKey(plugin.getKey())))) { if (removeFromAccessList(plugin.getKey(), user) != null) { affectedPluginKeys.add(plugin.getKey()); } } } } public String disable(Extension repo, String user) { String key = removeFromAccessList(repo.getKey(), user); eventPublisher.publish(new ExtensionEnabledEvent(key) .setUserName(user) .setUserEmail(userManager.getUserProfile(user).getEmail())); return key; } public List<String> findAllEnabledExtensions(String user) { List<String> result = newArrayList(); for (Plugin plugin : pluginAccessor.getEnabledPlugins()) { if (data.getUsersList(plugin.getKey()).contains(user)) { result.add(plugin.getKey()); } } return result; } public List<String> uninstallExtension(Extension plugin, String user, Operation<String, Void> enableCallback) throws Exception { List<String> keysModified = new ArrayList<String>(); String pluginKey = plugin.getKey(); String originalKey = plugin.getForkedPluginKey(); if (originalKey != null && pluginAccessor.getPlugin(originalKey) != null && !data.isGlobalExtension(originalKey)) { if (hasAccess(pluginKey, user)) { keysModified.add(originalKey); enableCallback.operateOn(originalKey); } } disallowAllPluginAccess(pluginKey); data.clearFavorites(pluginKey); pluginSystemManager.uninstall(pluginKey, user); eventPublisher.publish(new ExtensionUninstalledEvent(pluginKey) .setUserName(user) .setUserEmail(plugin.getAuthorEmail()) .setMessage("Uninstalled from the UI")); return keysModified; } public List<String> forkExtension(Extension plugin, String user, String description) throws Exception { String pluginKey = plugin.getKey(); String forkedPluginKey = createForkPluginKey(pluginKey, user); pluginSystemManager.forkAndInstall(pluginKey, forkedPluginKey, plugin.getPluginType(), user, description); List<String> modifiedKeys = new ArrayList<String>(); modifiedKeys.add(forkedPluginKey); if (hasAccess(pluginKey, user)) { modifiedKeys.add(pluginKey); enable(extensionManager.getExtension(forkedPluginKey), user, false); } if (forkedPluginKey != null) { sendForkedEmail(plugin, forkedPluginKey, user); } eventPublisher.publish(new ExtensionForkedEvent(pluginKey, forkedPluginKey).setUserName(user).setUserEmail(userManager.getUserProfile(user).getEmail()).setMessage("Forked from the UI")); return modifiedKeys; } public String saveAndRebuild(Extension plugin, String fileName, String contents, String user) { String pluginKey = plugin.getKey(); String installedPluginKey = pluginSystemManager.saveAndRebuild(pluginKey, plugin.getPluginType(), fileName, contents, user); eventPublisher.publish(new ExtensionUpdatedEvent(pluginKey) .setUserName(user) .setUserEmail(plugin.getAuthorEmail()) .addUpdatedFile(fileName) .setMessage("Edit from the UI")); return installedPluginKey; } public String favorite(Extension ex, String user) { String pluginKey = ex.getKey(); data.favorite(pluginKey, user); sendFavoritedEmail(ex, user); eventPublisher.publish(new ExtensionFavoritedEvent(pluginKey) .setUserName(user) .setUserEmail(userManager.getUserProfile(user).getEmail())); return pluginKey; } public String unfavorite(Extension ex, String user) { String pluginKey = ex.getKey(); data.unfavorite(pluginKey, user); eventPublisher.publish(new ExtensionUnfavoritedEvent(pluginKey) .setUserName(user) .setUserEmail(userManager.getUserProfile(user).getEmail())); return pluginKey; } public List<String> enableGlobally(UserExtension enabledPlugin, String user) { data.addGlobalExtension(enabledPlugin.getKey()); descriptorGeneratorManager.refreshGeneratedDescriptorsForPlugin(enabledPlugin.getKey()); // clear all forks of this plugin String parentKey = enabledPlugin.getForkedPluginKey() != null ? enabledPlugin.getForkedPluginKey() : enabledPlugin.getKey(); List<String> affectedPluginKeys = newArrayList(); for (Plugin plugin : extensionManager.getAllExtensionPlugins()) { if (!plugin.getKey().equals(enabledPlugin.getKey()) && (plugin.getKey().equals(parentKey) || parentKey.equals(Extension.getForkedPluginKey(plugin.getKey())))) { if (data.getUsersList(plugin.getKey()).contains(user)) { affectedPluginKeys.add(plugin.getKey()); } disallowAllPluginAccess(plugin.getKey()); } } // TODO: send notification email? eventPublisher.publish(new ExtensionEnabledGloballyEvent(enabledPlugin.getKey()) .setUserName(user) .setUserEmail(userManager.getUserProfile(user).getEmail())); return affectedPluginKeys; } public void disableGlobally(UserExtension repo, String user) { String pluginKey = repo.getKey(); data.removeGlobalExtension(pluginKey); descriptorGeneratorManager.refreshGeneratedDescriptorsForPlugin(pluginKey); eventPublisher.publish(new ExtensionEnabledGloballyEvent(pluginKey).setUserName(user) .setUserEmail(userManager.getUserProfile(user).getEmail())); } public void sendFeedback(final Extension extension, final Feedback feedback, final String user) { sendFeedbackType(extension, feedback, "feedback", user); } public void reportBroken(final Extension extension, final Feedback feedback, final String user) { sendFeedbackType(extension, feedback, "broken", user); } private void sendFeedbackType(final Extension extension, final Feedback feedback, final String feedbackType, final String user) { final UserProfile sender = userManager.getUserProfile(user); if (sender == null) { log.warn("Unable to send feedback from '" + user + "' due to no profile found"); return; } String pluginAuthor = extension.getAuthor(); if (pluginAuthor != null && userManager.getUserProfile(pluginAuthor) != null) { productAccessor.sendEmail(new EmailOptions() .toUsername(pluginAuthor) .subjectTemplate("email/" + feedbackType + "-subject.vm") .bodyTemplate("email/" + feedbackType + "-body.vm") .replyToEmail(sender.getEmail()) .context(new HashMap<String, Object>() {{ put("plugin", extension); put("sender", sender.getUsername()); put("senderFullName", sender.getFullName()); put("feedback", feedback); put("nl", "\n"); }})); } } public String install(Extension ext, File uploadedFile, String user) { String pluginKey = pluginSystemManager.install(uploadedFile, ext != null ? ext.getKey() : null, user); eventPublisher.publish(new ExtensionInstalledEvent(pluginKey) .setUserName(user) .setUserEmail(userManager.getUserProfile(user).getEmail()) .setMessage("Installed from a JAR upload")); return pluginKey; } private void sendFavoritedEmail(final Extension extension, final String user) { final String userFullName = userManager.getUserProfile(user).getFullName(); final String pluginKey = extension.getKey(); String pluginAuthor = extension.getAuthor(); if (pluginAuthor != null && !user.equals(pluginAuthor) && userManager.getUserProfile(pluginAuthor) != null) { final Set<Extension> commonExtensions = new HashSet<Extension>(); final Set<Extension> suggestedExtensions = new HashSet<Extension>(); for (Extension plugin : getAllRemoteSpeakeasyPlugins(user)) { if (plugin.getKey().equals(pluginKey)) { continue; } List<String> favoritedKeys = data.getFavorites(plugin.getKey()); if (favoritedKeys.contains(user)) { if (favoritedKeys.contains(pluginAuthor)) { commonExtensions.add(plugin); } else { suggestedExtensions.add(plugin); } } } productAccessor.sendEmail(new EmailOptions() .toUsername(pluginAuthor) .subjectTemplate("email/favorited-subject.vm") .bodyTemplate("email/favorited-body.vm") .context(new HashMap<String, Object>() {{ put("plugin", extension); put("favoriteMarkerFullName", userFullName); put("favoriteMarker", user); put("commonExtensions", commonExtensions); put("suggestedExtensions", suggestedExtensions); put("favoriteTotal", data.getFavorites(pluginKey).size()); }})); } } private void sendForkedEmail(final Extension extension, final String forkedPluginKey, final String user) { final String userFullName = userManager.getUserProfile(user).getFullName(); String pluginAuthor = extension.getAuthor(); if (pluginAuthor != null && !user.equals(pluginAuthor) && userManager.getUserProfile(pluginAuthor) != null) { final Set<Extension> otherForkedExtensions = new HashSet<Extension>(); for (Extension plugin : getAllRemoteSpeakeasyPlugins(user)) { if (user.equals(plugin.getAuthor()) && plugin.getForkedPluginKey() != null && !forkedPluginKey.equals(plugin.getKey())) { otherForkedExtensions.add(extensionManager.getExtension(plugin.getForkedPluginKey())); } } final FullNameResolver resolver = new FullNameResolver() { public String getFullName(String userName) { UserProfile profile = userManager.getUserProfile(userName); return profile != null ? profile.getFullName() : userName; } }; productAccessor.sendEmail(new EmailOptions().toUsername(pluginAuthor).subjectTemplate("email/forked-subject.vm").bodyTemplate("email/forked-body.vm").context(new HashMap<String, Object>() {{ put("plugin", extension); put("userResolver", resolver); put("forkedPlugin", extensionManager.getExtension(forkedPluginKey)); put("forkerFullName", userFullName); put("forker", user); put("otherForkedExtensions", otherForkedExtensions); }})); } } private String createForkPluginKey(String pluginKey, String remoteUser) { StringBuilder safeName = new StringBuilder(); for (char c : remoteUser.toCharArray()) { if (Character.isLetterOrDigit(c)) { safeName.append(c); } } return pluginKey + "-fork-" + safeName; } private void disallowAllPluginAccess(String pluginKey) { data.saveUsersList(pluginKey, Lists.<String>newArrayList()); descriptorGeneratorManager.refreshGeneratedDescriptorsForPlugin(pluginKey); } private void sendEnabledEmail(final Extension enabledPlugin, final String user) { final String userFullName = userManager.getUserProfile(user).getFullName(); String pluginAuthor = enabledPlugin.getAuthor(); if (pluginAuthor != null && !user.equals(pluginAuthor) && userManager.getUserProfile(pluginAuthor) != null) { final Set<Extension> commonExtensions = new HashSet<Extension>(); final Set<Extension> suggestedExtensions = new HashSet<Extension>(); for (UserExtension plugin : getAllRemoteSpeakeasyPlugins(user)) { if (plugin.isEnabled()) { List<String> accessList = data.getUsersList(plugin.getKey()); if (accessList.contains(pluginAuthor)) { commonExtensions.add(plugin); } else { suggestedExtensions.add(plugin); } } } productAccessor.sendEmail(new EmailOptions() .toUsername(pluginAuthor) .subjectTemplate("email/enabled-subject.vm") .bodyTemplate("email/enabled-body.vm") .context(new HashMap<String, Object>() {{ put("plugin", enabledPlugin); put("enablerFullName", userFullName); put("enabler", user); put("commonExtensions", commonExtensions); put("suggestedExtensions", suggestedExtensions); put("enabledTotal", data.getUsersList(enabledPlugin.getKey()).size()); }})); } } private String removeFromAccessList(String pluginKey, String user) { List<String> accessList = data.getUsersList(pluginKey); if (accessList.contains(user)) { accessList.remove(user); data.saveUsersList(pluginKey, accessList); descriptorGeneratorManager.refreshGeneratedDescriptorsForPlugin(pluginKey); return pluginKey; } return null; } private List<UserExtension> getAllRemoteSpeakeasyPlugins(final String userName) { final List<Plugin> rawPlugins = extensionManager.getAllExtensionPlugins(); return newArrayList(transform(rawPlugins, new Function<Plugin, UserExtension>() { public UserExtension apply(Plugin from) { try { return extensionBuilder.build(from, userName, rawPlugins); } catch (RuntimeException ex) { log.error("Unable to load plugin '" + from.getKey() + "'", ex); UserExtension plugin = new UserExtension(from); plugin.setDescription("Unable to load due to " + ex.getMessage()); return plugin; } } })); } private boolean hasAccess(String pluginKey, String remoteUser) { return data.getUsersList(pluginKey).contains(remoteUser); } public void saveEnabledPlugins(List<String> enabledKeys, String user) { data.saveEnabledPlugins(enabledKeys, user); } public List<String> getEnabledPlugins(String user) { return data.getEnabledPlugins(user); } public interface FullNameResolver { String getFullName(String userName); } }