diff --git a/README.md b/README.md index f2a52265..6737bf00 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ The AEM Groovy Console provides an interface for running [Groovy](http://www.gro ## Requirements -* AEM author instance running on localhost:4502 +* AEM author instance running on [localhost:4502](http://localhost:4502/) * [Maven](http://maven.apache.org/) 3.x ## Compatibility @@ -18,7 +18,7 @@ The AEM Groovy Console provides an interface for running [Groovy](http://www.gro Groovy Console Version(s) | AEM Version ------------ | ------------- 11.x.x | 6.3 -10.x.x, 9.x.x | 6.2 +10.x.x, 9.x.x | 6.2 8.x.x | 6.1 7.x.x | 6.0 6.x.x, 5.x.x | 5.6 (CQ) @@ -30,9 +30,9 @@ Groovy Console Version(s) | AEM Version 2. [Verify](http://localhost:4502/etc/groovyconsole.html) the installation. -Additional build profiles may be added in the project's pom.xml to support deployment to non-localhost AEM servers. +Additional build profiles may be added in the project's `pom.xml` to support deployment to non-localhost AEM servers. -AEM 6.0 no longer allows vanity paths for pages in /etc by default. To enable access to the Groovy Console from /groovyconsole as in previous versions, the Apache Sling Resource Resolver Factory OSGi configuration must be updated to allow vanity paths from /etc. The Groovy Console Configuration Service can then be updated to enable the vanity path if so desired. +AEM 6.0 no longer allows vanity paths for pages in `/etc` by default. To enable access to the Groovy Console from `/groovyconsole` as in previous versions, the **Apache Sling Resource Resolver Factory** OSGi configuration must be updated to allow vanity paths from `/etc`. The **Groovy Console Configuration Service** can then be updated to enable the vanity path if so desired. ## Excluding the Groovy OSGi Bundle @@ -46,6 +46,31 @@ If you are running AEM with a context path, set the Maven property `aem.context. mvn install -P local -Daem.context.path=/context +## OSGi Configuration + +Navigate to the [OSGi console configuration page](http://localhost:4502/system/console/configMgr) and select the **Groovy Console Configuration Service**. + +Property | Description | Default Value +------------ | ------------- | ---------- +Email Enabled? | Check to enable email notification on completion of script execution. | False +Email Recipients | Email addresses to receive notification. | [] +Allowed Groups | List of group names that are authorized to use the console. If empty, no authorization check is performed. | [] +Vanity Path Enabled? | Enables `/groovyconsole` vanity path. **Apache Sling Resource Resolver Factory** OSGi configuration must also be updated to allow vanity paths from `/etc`. | False +Audit Disabled? | Disables auditing of script execution history. | False +Display All Audit Records? | If enabled, all audit records (including records for other users) will be displayed in the console history. | False + +## Batch Script Execution + +Saved scripts can be remotely executed by sending a POST request to the console servlet with either the `scriptPath` or `scriptPaths` query parameter. + +### Single Script + + curl -d "scriptPath=/etc/groovyconsole/scripts/samples/JcrSearch.groovy" -X POST -u admin:admin http://localhost:4502/bin/groovyconsole/post.json + +### Multiple Scripts + + curl -d "scriptPaths=/etc/groovyconsole/scripts/samples/JcrSearch.groovy&scriptPaths=/etc/groovyconsole/scripts/samples/FulltextQuery.groovy" -X POST -u admin:admin http://localhost:4502/bin/groovyconsole/post.json + ## Extensions Beginning in version 7.0.0, the Groovy Console provides extension hooks to further customize script execution. The console exposes an API containing three extension provider interfaces that can be implemented as OSGi services in any bundle deployed to an AEM instance. See the default extension providers in the `com.icfolson.aem.groovy.console.extension.impl` package for examples of how a bundle can implement these services to supply additional script bindings, metaclasses, and star imports. diff --git a/pom.xml b/pom.xml index 71619ae8..080d39aa 100644 --- a/pom.xml +++ b/pom.xml @@ -11,7 +11,7 @@ com.icfolson.aem.groovy.console aem-groovy-console jar - 11.1.0 + 11.2.0 AEM Groovy Console The AEM Groovy Console provides an interface for running Groovy scripts in the AEM container. Scripts can be diff --git a/src/main/groovy/com/icfolson/aem/groovy/console/GroovyConsoleService.groovy b/src/main/groovy/com/icfolson/aem/groovy/console/GroovyConsoleService.groovy index 07deee8b..b4132a8c 100644 --- a/src/main/groovy/com/icfolson/aem/groovy/console/GroovyConsoleService.groovy +++ b/src/main/groovy/com/icfolson/aem/groovy/console/GroovyConsoleService.groovy @@ -8,5 +8,9 @@ interface GroovyConsoleService { RunScriptResponse runScript(SlingHttpServletRequest request) + RunScriptResponse runScript(SlingHttpServletRequest request, String scriptPath) + + List runScripts(SlingHttpServletRequest request, List scriptPaths) + SaveScriptResponse saveScript(SlingHttpServletRequest request) } \ No newline at end of file diff --git a/src/main/groovy/com/icfolson/aem/groovy/console/audit/impl/DefaultAuditService.groovy b/src/main/groovy/com/icfolson/aem/groovy/console/audit/impl/DefaultAuditService.groovy index ca6d12dc..1a12fd41 100644 --- a/src/main/groovy/com/icfolson/aem/groovy/console/audit/impl/DefaultAuditService.groovy +++ b/src/main/groovy/com/icfolson/aem/groovy/console/audit/impl/DefaultAuditService.groovy @@ -55,7 +55,10 @@ class DefaultAuditService implements AuditService { def auditRecordNode = addAuditRecordNode(session) auditRecordNode.setProperty(AuditRecord.PROPERTY_SCRIPT, response.script) - auditRecordNode.setProperty(AuditRecord.PROPERTY_DATA, response.data) + + if (response.data) { + auditRecordNode.setProperty(AuditRecord.PROPERTY_DATA, response.data) + } if (response.exceptionStackTrace) { auditRecordNode.setProperty(AuditRecord.PROPERTY_EXCEPTION_STACK_TRACE, response.exceptionStackTrace) diff --git a/src/main/groovy/com/icfolson/aem/groovy/console/constants/GroovyConsoleConstants.groovy b/src/main/groovy/com/icfolson/aem/groovy/console/constants/GroovyConsoleConstants.groovy index 6184c33f..211c3b90 100644 --- a/src/main/groovy/com/icfolson/aem/groovy/console/constants/GroovyConsoleConstants.groovy +++ b/src/main/groovy/com/icfolson/aem/groovy/console/constants/GroovyConsoleConstants.groovy @@ -8,6 +8,10 @@ class GroovyConsoleConstants { public static final String EXTENSION_GROOVY = ".groovy" + public static final String PARAMETER_SCRIPT_PATH = "scriptPath" + + public static final String PARAMETER_SCRIPT_PATHS = "scriptPaths" + public static final String PARAMETER_SCRIPT = "script" public static final String PARAMETER_USER_ID = "userId" diff --git a/src/main/groovy/com/icfolson/aem/groovy/console/impl/DefaultGroovyConsoleService.groovy b/src/main/groovy/com/icfolson/aem/groovy/console/impl/DefaultGroovyConsoleService.groovy index a0c36795..45b45bf6 100755 --- a/src/main/groovy/com/icfolson/aem/groovy/console/impl/DefaultGroovyConsoleService.groovy +++ b/src/main/groovy/com/icfolson/aem/groovy/console/impl/DefaultGroovyConsoleService.groovy @@ -29,6 +29,7 @@ import javax.jcr.Node import javax.jcr.Session import java.util.concurrent.CopyOnWriteArrayList +import static com.google.common.base.Preconditions.checkNotNull import static com.icfolson.aem.groovy.console.constants.GroovyConsoleConstants.EXTENSION_GROOVY import static com.icfolson.aem.groovy.console.constants.GroovyConsoleConstants.PARAMETER_DATA import static com.icfolson.aem.groovy.console.constants.GroovyConsoleConstants.PATH_CONSOLE_ROOT @@ -75,12 +76,25 @@ class DefaultGroovyConsoleService implements GroovyConsoleService { @Override RunScriptResponse runScript(SlingHttpServletRequest request) { - def scriptContent = request.getRequestParameter(PARAMETER_SCRIPT)?.getString(CharEncoding.UTF_8) - def data = request.getRequestParameter(PARAMETER_DATA)?.getString(CharEncoding.UTF_8) + runScript(request, null) + } - def stream = new ByteArrayOutputStream() + @Override + RunScriptResponse runScript(SlingHttpServletRequest request, String scriptPath) { def session = request.resourceResolver.adaptTo(Session) + def scriptContent + + if (scriptPath) { + scriptContent = loadScriptContent(session, scriptPath) + } else { + scriptContent = request.getRequestParameter(PARAMETER_SCRIPT)?.getString(CharEncoding.UTF_8) + } + + checkNotNull(scriptContent, "Script content cannot be empty.") + + def data = request.getRequestParameter(PARAMETER_DATA)?.getString(CharEncoding.UTF_8) + def stream = new ByteArrayOutputStream() def response = null def binding = getBinding(extensionService.getBinding(request), data, stream) @@ -121,6 +135,13 @@ class DefaultGroovyConsoleService implements GroovyConsoleService { response } + @Override + List runScripts(SlingHttpServletRequest request, List scriptPaths) { + scriptPaths.collect { scriptPath -> + runScript(request, scriptPath) + } + } + @Override @Synchronized SaveScriptResponse saveScript(SlingHttpServletRequest request) { @@ -192,6 +213,19 @@ class DefaultGroovyConsoleService implements GroovyConsoleService { } } + private String loadScriptContent(Session session, String scriptPath) { + def binary = session.getNode(scriptPath) + .getNode(JcrConstants.JCR_CONTENT) + .getProperty(JcrConstants.JCR_DATA) + .binary + + def scriptContent = binary.stream.text + + binary.dispose() + + scriptContent + } + private void saveFile(Session session, Node folderNode, String script, String fileName, Date date, String mimeType) { def fileNode = folderNode.addNode(Text.escapeIllegalJcrChars(fileName), JcrConstants.NT_FILE) @@ -200,13 +234,11 @@ class DefaultGroovyConsoleService implements GroovyConsoleService { def stream = new ByteArrayInputStream(script.getBytes(CharEncoding.UTF_8)) def binary = session.valueFactory.createBinary(stream) - resourceNode.with { - setProperty(JcrConstants.JCR_MIMETYPE, mimeType) - setProperty(JcrConstants.JCR_ENCODING, CharEncoding.UTF_8) - setProperty(JcrConstants.JCR_DATA, binary) - setProperty(JcrConstants.JCR_LASTMODIFIED, date.time) - setProperty(JcrConstants.JCR_LAST_MODIFIED_BY, session.userID) - } + resourceNode.setProperty(JcrConstants.JCR_MIMETYPE, mimeType) + resourceNode.setProperty(JcrConstants.JCR_ENCODING, CharEncoding.UTF_8) + resourceNode.setProperty(JcrConstants.JCR_DATA, binary) + resourceNode.setProperty(JcrConstants.JCR_LASTMODIFIED, date.time) + resourceNode.setProperty(JcrConstants.JCR_LAST_MODIFIED_BY, session.userID) session.save() binary.dispose() diff --git a/src/main/groovy/com/icfolson/aem/groovy/console/servlets/ScriptPostServlet.groovy b/src/main/groovy/com/icfolson/aem/groovy/console/servlets/ScriptPostServlet.groovy index cd41bbf2..aa7331d3 100755 --- a/src/main/groovy/com/icfolson/aem/groovy/console/servlets/ScriptPostServlet.groovy +++ b/src/main/groovy/com/icfolson/aem/groovy/console/servlets/ScriptPostServlet.groovy @@ -2,6 +2,7 @@ package com.icfolson.aem.groovy.console.servlets import com.icfolson.aem.groovy.console.GroovyConsoleService import com.icfolson.aem.groovy.console.configuration.ConfigurationService +import com.icfolson.aem.groovy.console.constants.GroovyConsoleConstants import groovy.util.logging.Slf4j import org.apache.felix.scr.annotations.Reference import org.apache.felix.scr.annotations.sling.SlingServlet @@ -26,7 +27,23 @@ class ScriptPostServlet extends AbstractJsonResponseServlet { protected void doPost(SlingHttpServletRequest request, SlingHttpServletResponse response) throws ServletException, IOException { if (configurationService.hasPermission(request)) { - writeJsonResponse(response, consoleService.runScript(request)) + def scriptPaths = request.getParameterValues(GroovyConsoleConstants.PARAMETER_SCRIPT_PATHS) + + if (scriptPaths) { + LOG.debug("running scripts for paths = {}", scriptPaths) + + writeJsonResponse(response, consoleService.runScripts(request, scriptPaths as List)) + } else { + def scriptPath = request.getParameter(GroovyConsoleConstants.PARAMETER_SCRIPT_PATH) + + if (scriptPath) { + LOG.debug("running script for path = {}", scriptPath) + + writeJsonResponse(response, consoleService.runScript(request, scriptPath)) + } else { + writeJsonResponse(response, consoleService.runScript(request)) + } + } } else { response.status = SC_FORBIDDEN }