View Javadoc
1   /**
2    * This file Copyright (c) 2013-2015 Magnolia International
3    * Ltd.  (http://www.magnolia-cms.com). All rights reserved.
4    *
5    *
6    * This file is dual-licensed under both the Magnolia
7    * Network Agreement and the GNU General Public License.
8    * You may elect to use one or the other of these licenses.
9    *
10   * This file is distributed in the hope that it will be
11   * useful, but AS-IS and WITHOUT ANY WARRANTY; without even the
12   * implied warranty of MERCHANTABILITY or FITNESS FOR A
13   * PARTICULAR PURPOSE, TITLE, or NONINFRINGEMENT.
14   * Redistribution, except as permitted by whichever of the GPL
15   * or MNA you select, is prohibited.
16   *
17   * 1. For the GPL license (GPL), you can redistribute and/or
18   * modify this file under the terms of the GNU General
19   * Public License, Version 3, as published by the Free Software
20   * Foundation.  You should have received a copy of the GNU
21   * General Public License, Version 3 along with this program;
22   * if not, write to the Free Software Foundation, Inc., 51
23   * Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
24   *
25   * 2. For the Magnolia Network Agreement (MNA), this file
26   * and the accompanying materials are made available under the
27   * terms of the MNA which accompanies this distribution, and
28   * is available at http://www.magnolia-cms.com/mna.html
29   *
30   * Any modifications to this file must keep this entire header
31   * intact.
32   *
33   */
34  package info.magnolia.module.groovy.terminal;
35  
36  import elemental.json.JsonArray;
37  import groovy.lang.Binding;
38  import groovy.lang.GroovyObject;
39  import groovy.lang.GroovySystem;
40  import groovy.lang.MetaMethod;
41  import groovy.lang.MissingPropertyException;
42  
43  import info.magnolia.cms.security.User;
44  import info.magnolia.context.Context;
45  import info.magnolia.context.MgnlContext;
46  import info.magnolia.context.MgnlContext.Op;
47  import info.magnolia.i18nsystem.SimpleTranslator;
48  import info.magnolia.jcr.util.PropertyUtil;
49  import info.magnolia.jcr.util.SessionUtil;
50  import info.magnolia.module.groovy.console.MgnlGroovyConsole;
51  import info.magnolia.module.groovy.console.MgnlGroovyConsoleContext;
52  
53  import java.io.ByteArrayInputStream;
54  import java.io.FileInputStream;
55  import java.io.FileNotFoundException;
56  import java.io.InputStream;
57  import java.io.Serializable;
58  import java.io.StringWriter;
59  import java.lang.reflect.Method;
60  import java.util.Arrays;
61  import java.util.Collection;
62  import java.util.HashMap;
63  import java.util.HashSet;
64  import java.util.List;
65  import java.util.Map;
66  import java.util.Map.Entry;
67  import java.util.Set;
68  
69  import javax.inject.Inject;
70  import javax.jcr.Node;
71  import javax.jcr.RepositoryException;
72  
73  import org.apache.commons.io.IOUtils;
74  import org.apache.commons.lang.StringUtils;
75  import org.codehaus.groovy.control.CompilationFailedException;
76  import org.codehaus.groovy.runtime.InvokerInvocationException;
77  import org.slf4j.Logger;
78  import org.slf4j.LoggerFactory;
79  
80  import com.google.gson.Gson;
81  import com.vaadin.annotations.JavaScript;
82  import com.vaadin.annotations.StyleSheet;
83  import com.vaadin.ui.AbstractJavaScriptComponent;
84  import com.vaadin.ui.JavaScriptFunction;
85  
86  /**
87   * Wraps JQuery Terminal Emulator plugin (http://terminal.jcubic.pl) as Vaadin component. Loading jQuery from Google apis is needed when using the terminal component in the {@link info.magnolia.module.groovy.rescue.MgnlGroovyRescueApp} where jQuery can't be provided by Magnolia 5 framework.
88   */
89  @JavaScript({ "https://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js", "jquery.terminal-0.7.12.custom.min.js", "terminal_connector.js" })
90  @StyleSheet("jquery.terminal.custom.css")
91  public class Terminal extends AbstractJavaScriptComponent {
92  
93      public static final String BINDING_SESSION_ATTRIBUTE = "info.magnolia.module.groovy.console.binding.session.attribute";
94  
95      private final Gson gson = new Gson();
96  
97      /**
98       * SerializableBinding. Need this otherwise adding Binding to the session fails with
99       * java.lang.IllegalArgumentException: setAttribute: Non-serializable attribute
100      * at org.apache.catalina.session.StandardSession.setAttribute(..)
101      */
102     public static final class SerializableBinding extends Binding implements Serializable {
103         public static final long serialVersionUID = 42L;
104     }
105 
106     private static final Logger log = LoggerFactory.getLogger(Terminal.class);
107     private static final List<String> METHODS_BLACK_LIST = Arrays.asList(new String[] { "clone", "equals", "finalize", "hashCode", "getClass", "notify", "notifyAll", "toString", "wait" });
108     private static final List<String> PREDEFINED_VARS = Arrays.asList(new String[] { "clean", "clear", "ctx", "err", "help", "out", "run" });
109 
110     private SimpleTranslator simpleTranslator;
111     private boolean useSystemContext;
112     private Map<String, Set<String>> suggestions = new HashMap<String, Set<String>>();
113 
114     /**
115      * @param useSystemContext if <code>true</code> will run {@link #execute(String)} method in system context (needed especially by {@link info.magnolia.module.groovy.rescue.MgnlGroovyRescueApp}).
116      */
117     @Inject
118     public Terminal(final SimpleTranslator simpleTranslator, final boolean useSystemContext) {
119         this.simpleTranslator = simpleTranslator;
120         this.useSystemContext = useSystemContext;
121         this.getState().greetings = simpleTranslator.translate("console.greetings", GroovySystem.getVersion());
122 
123         setPredefinedVariables();
124         // ctx object is available when first starting the terminal, therefore we set its suggestions now
125         // so that they're available without first submitting a command.
126         createSuggestionsPerClass("ctx", new MgnlGroovyConsoleContext(MgnlContext.getInstance()));
127 
128         this.getState().suggestions = gson.toJson(suggestions);
129 
130         setSizeFull();
131 
132         addFunction("executeCommand", new JavaScriptFunction() {
133             @Override
134             public void call(JsonArray arguments) {
135                 getState().command = arguments.getString(0);
136                 resetState();
137                 try {
138                     if (isUseSystemContext()) {
139                         MgnlContext.doInSystemContext(new Op<Void, Exception>() {
140 
141                             @Override
142                             public Void exec() throws Exception {
143                                 execute(getCommand());
144                                 return null;
145                             }
146                         });
147                     } else {
148                         execute(getCommand());
149                     }
150                 } catch (Exception e) {
151                     getState().output = e.getMessage();
152                 }
153             }
154         });
155 
156         addFunction("saveStatus", new JavaScriptFunction() {
157             @Override
158             public void call(JsonArray arguments) {
159                 resetState();
160                 getState().view = arguments.get(0).toJson();
161                 getState().history = arguments.get(1).toJson();
162                 log.debug("saved status [view:{}, history{}]", getState().view, getState().history);
163             }
164         });
165     }
166 
167     public boolean isUseSystemContext() {
168         return useSystemContext;
169     }
170 
171     public String getCommand() {
172         return getState().command;
173     }
174 
175     @Override
176     public TerminalState getState() {
177         return (TerminalState) super.getState();
178     }
179 
180     public void execute(String command) throws Exception {
181         User currentUser = MgnlContext.getUser();
182         if (!isAuthorized(currentUser)) {
183             String msg = simpleTranslator.translate("console.user.unauthorized", currentUser.getName());
184             log.warn(msg);
185             getState().output = msg;
186             return;
187         }
188 
189         log.debug("executing command [{}]", command);
190         StringWriter sw = new StringWriter();
191         if (handleCommand(command, sw)) {
192             getState().output = sw.toString();
193         } else {
194             getState().output = evaluate(new ByteArrayInputStream(command.getBytes()));
195         }
196     }
197 
198     private String evaluate(final InputStream is) {
199 
200         MgnlGroovyConsole console = getOrCreateMgnlGroovyConsole();
201 
202         Context originalCtx = MgnlContext.getInstance();
203         MgnlGroovyConsoleContext groovyCtx = new MgnlGroovyConsoleContext(originalCtx);
204         MgnlContext.setInstance(groovyCtx);
205 
206         StringWriter sw = new StringWriter();
207 
208         Object lastResult;
209         try {
210             lastResult = console.evaluate(is, console.generateScriptName(), sw);
211             // Shows the result of the evaluated code
212             sw.write(lastResult != null ? lastResult.toString() : "");
213         } catch (CompilationFailedException e) {
214             sw.write(e.getMessage());
215         } catch (Throwable e) {
216             // Unroll invoker exceptions
217             if (e instanceof InvokerInvocationException) {
218                 e = e.getCause();
219             }
220             if (e instanceof MissingPropertyException) {
221                 sw.write(e.getMessage());
222             } else {
223                 log.error("Error while evaluating script: ", e);
224                 sw.write(e.getClass().getSimpleName() + ": " + e.getMessage());
225             }
226         } finally {
227             MgnlContext.setInstance(originalCtx);
228         }
229 
230         // update suggestions
231         Binding context = console.getContext();
232         updateCodeSuggestions(context);
233         updateBindingSuggestions(context);
234 
235         return sw.toString();
236     }
237 
238     /**
239      * @return <code>true</code> if the command could be processed, <code>false</code> otherwise.
240      */
241     private boolean handleCommand(final String command, final StringWriter out) throws RepositoryException {
242         if (StringUtils.isEmpty(command)) {
243             // just ignore it
244             return true;
245         }
246 
247         String[] tokens = command.trim().split("\\s+");
248 
249         if ("help".equals(tokens[0]) || "?".equals(tokens[0])) {
250             out.write(simpleTranslator.translate("console.help"));
251             return true;
252         } else if ("clear".equals(tokens[0])) {
253             return true;
254         } else if ("clean".equals(tokens[0])) {
255             MgnlContext.setAttribute(BINDING_SESSION_ATTRIBUTE, new SerializableBinding(), Context.SESSION_SCOPE);
256             setPredefinedVariables();
257             return true;
258         } else if ("run".equals(tokens[0])) {
259             return runScript(tokens, out);
260         }
261         return false;
262     }
263 
264     private boolean runScript(final String[] tokens, final StringWriter out) {
265         if (tokens.length != 2) {
266             out.write(simpleTranslator.translate("console.commands.run.usage"));
267             return true;
268         }
269 
270         InputStream is = null;
271         String path = tokens[1];
272         // first try to locate the script in the scripts workspace
273         Node scriptNode = SessionUtil.getNode("scripts", path);
274         if (scriptNode != null) {
275             String source = PropertyUtil.getString(scriptNode, "text");
276             if (StringUtils.isNotEmpty(source)) {
277                 is = new ByteArrayInputStream(source.getBytes());
278             }
279         } else {
280             // try with the file system
281             try {
282                 is = new FileInputStream(path);
283             } catch (FileNotFoundException e) {
284                 // ignore here
285             }
286         }
287         if (is == null) {
288             out.write(simpleTranslator.translate("console.commands.run.notFound", path) + "\n");
289             out.write(simpleTranslator.translate("console.commands.run.usage"));
290         } else {
291             out.write(evaluate(is));
292             IOUtils.closeQuietly(is);
293         }
294         return true;
295     }
296 
297     private void resetState() {
298         getState().output = "";
299         getState().view = "";
300         getState().history = "";
301     }
302 
303     protected boolean isAuthorized(User currentUser) {
304         final Collection<String> roles = currentUser.getRoles();
305         return roles.contains("superuser") || roles.contains("scripter");
306     }
307 
308     private MgnlGroovyConsole getOrCreateMgnlGroovyConsole() {
309         Binding binding = MgnlContext.getAttribute(BINDING_SESSION_ATTRIBUTE, Context.SESSION_SCOPE);
310         MgnlGroovyConsole console = null;
311         if (binding != null) {
312             console = new MgnlGroovyConsole(binding);
313         } else {
314             Binding newBinding = new SerializableBinding();
315             MgnlContext.setAttribute(BINDING_SESSION_ATTRIBUTE, newBinding, Context.SESSION_SCOPE);
316             console = new MgnlGroovyConsole(newBinding);
317         }
318         return console;
319     }
320 
321     private void setPredefinedVariables() {
322         this.getState().binding = gson.toJson(PREDEFINED_VARS);
323     }
324 
325     private void updateBindingSuggestions(final Binding context) {
326         Set<String> predefined = new HashSet<String>();
327         predefined.addAll(PREDEFINED_VARS);
328         predefined.addAll(context.getVariables().keySet());
329         getState().binding = gson.toJson(predefined);
330     }
331 
332     private void updateCodeSuggestions(final Binding context) {
333         Map<String, Object> variables = context.getVariables();
334         for (Entry<String, Object> entry : variables.entrySet()) {
335             String key = entry.getKey();
336             if (suggestions.containsKey(key)) {
337                 continue;
338             }
339             createSuggestionsPerClass(key, entry.getValue());
340         }
341         getState().suggestions = gson.toJson(suggestions);
342     }
343 
344     private void createSuggestionsPerClass(String key, Object value) {
345         Set<String> data = new HashSet<String>();
346         for (Method method : value.getClass().getMethods()) {
347             data.add(method.getName());
348         }
349 
350         if (value instanceof GroovyObject) {
351             GroovyObject obj = (GroovyObject) value;
352             for (MetaMethod method : obj.getMetaClass().getMethods()) {
353                 data.add(method.getName());
354             }
355         }
356         if (!data.isEmpty()) {
357             data.removeAll(METHODS_BLACK_LIST);
358             suggestions.put(key, data);
359         }
360     }
361 }