View Javadoc
1   /**
2    * This file Copyright (c) 2013-2017 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 info.magnolia.cms.security.User;
37  import info.magnolia.context.Context;
38  import info.magnolia.context.MgnlContext;
39  import info.magnolia.context.MgnlContext.Op;
40  import info.magnolia.i18nsystem.SimpleTranslator;
41  import info.magnolia.jcr.util.PropertyUtil;
42  import info.magnolia.jcr.util.SessionUtil;
43  import info.magnolia.module.groovy.console.MgnlGroovyConsole;
44  import info.magnolia.module.groovy.console.MgnlGroovyConsole.ScriptCallback;
45  import info.magnolia.module.groovy.console.MgnlGroovyConsoleContext;
46  import info.magnolia.objectfactory.Components;
47  import info.magnolia.ui.framework.message.MessagesManager;
48  
49  import java.io.IOException;
50  import java.io.Serializable;
51  import java.io.StringWriter;
52  import java.io.Writer;
53  import java.lang.reflect.Method;
54  import java.nio.file.Files;
55  import java.nio.file.Paths;
56  import java.util.Arrays;
57  import java.util.Collection;
58  import java.util.HashMap;
59  import java.util.HashSet;
60  import java.util.List;
61  import java.util.Map;
62  import java.util.Map.Entry;
63  import java.util.Set;
64  
65  import javax.jcr.Node;
66  
67  import org.apache.commons.lang3.StringUtils;
68  import org.codehaus.groovy.control.CompilationFailedException;
69  import org.slf4j.Logger;
70  import org.slf4j.LoggerFactory;
71  
72  import com.google.gson.Gson;
73  import com.vaadin.annotations.JavaScript;
74  import com.vaadin.annotations.StyleSheet;
75  import com.vaadin.ui.AbstractJavaScriptComponent;
76  import com.vaadin.ui.JavaScriptFunction;
77  import com.vaadin.ui.UI;
78  
79  import elemental.json.JsonArray;
80  import groovy.lang.Binding;
81  import groovy.lang.GroovyObject;
82  import groovy.lang.GroovySystem;
83  import groovy.lang.MetaMethod;
84  
85  /**
86   * 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.
87   */
88  @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" })
89  @StyleSheet("jquery.terminal.custom.css")
90  public class Terminal extends AbstractJavaScriptComponent {
91  
92      public static final String BINDING_SESSION_ATTRIBUTE = "info.magnolia.module.groovy.console.binding.session.attribute";
93  
94      private final Gson gson = new Gson();
95  
96      /**
97       * SerializableBinding. Need this otherwise adding Binding to the session fails with
98       * java.lang.IllegalArgumentException: setAttribute: Non-serializable attribute
99       * at org.apache.catalina.session.StandardSession.setAttribute(..)
100      */
101     public static final class SerializableBinding extends Binding implements Serializable {
102         public static final long serialVersionUID = 42L;
103     }
104 
105     private static final Logger log = LoggerFactory.getLogger(Terminal.class);
106     private static final List<String> METHODS_BLACK_LIST = Arrays.asList(new String[] { "clone", "equals", "finalize", "hashCode", "getClass", "notify", "notifyAll", "toString", "wait" });
107     private static final List<String> PREDEFINED_VARS = Arrays.asList(new String[] { "clean", "clear", "ctx", "err", "help", "out", "run" });
108 
109     private SimpleTranslator simpleTranslator;
110     private boolean useSystemContext;
111     private Map<String, Set<String>> suggestions = new HashMap<String, Set<String>>();
112     private boolean requiresNotificationMessageUponCompletion;
113     private MessagesManager messagesManager;
114 
115     /**
116      * @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}).
117      * <p>
118      */
119     public Terminal(final SimpleTranslator simpleTranslator, final boolean useSystemContext, final boolean requiresNotificationMessageUponCompletion, final MessagesManager messagesManager) {
120         this.simpleTranslator = simpleTranslator;
121         this.useSystemContext = useSystemContext;
122         this.messagesManager = messagesManager;
123         this.getState().greetings = simpleTranslator.translate("console.greetings", GroovySystem.getVersion());
124         this.requiresNotificationMessageUponCompletion = requiresNotificationMessageUponCompletion;
125 
126         setPredefinedVariables();
127         // ctx object is available when first starting the terminal, therefore we set its suggestions now
128         // so that they're available without first submitting a command.
129         createSuggestionsPerClass("ctx", new MgnlGroovyConsoleContext(MgnlContext.getInstance()));
130 
131         this.getState().suggestions = gson.toJson(suggestions);
132 
133         setSizeFull();
134 
135 
136         addFunction("executeCommand", new JavaScriptFunction() {
137             @Override
138             public void call(JsonArray arguments) {
139                 getState().command = arguments.getString(0);
140                 resetState();
141                 try {
142                     if (isUseSystemContext()) {
143                         MgnlContext.doInSystemContext(new Op<Void, Exception>() {
144 
145                             @Override
146                             public Void exec() throws Exception {
147                                 execute(getCommand());
148                                 return null;
149                             }
150                         });
151                     } else {
152                         execute(getCommand());
153                     }
154                 } catch (Exception e) {
155                     getState().output = e.getMessage();
156                 }
157             }
158         });
159 
160         addFunction("saveStatus", new JavaScriptFunction() {
161             @Override
162             public void call(JsonArray arguments) {
163                 resetState();
164                 getState().view = arguments.get(0).toJson();
165                 getState().history = arguments.get(1).toJson();
166                 log.debug("saved status [view:{}, history{}]", getState().view, getState().history);
167             }
168         });
169     }
170 
171     /**
172      * @deprecated since 2.4.6, please use {@link Terminal#Terminal(SimpleTranslator, boolean, boolean, MessagesManager)}
173      */
174     @Deprecated
175     public Terminal(final SimpleTranslator simpleTranslator, final boolean useSystemContext) {
176         this(simpleTranslator, useSystemContext, true, Components.getComponent(MessagesManager.class));
177     }
178 
179     public boolean isUseSystemContext() {
180         return useSystemContext;
181     }
182 
183     public String getCommand() {
184         return getState().command;
185     }
186 
187     @Override
188     public TerminalState getState() {
189         return (TerminalState) super.getState();
190     }
191 
192     public void execute(String command) throws Exception {
193         User currentUser = MgnlContext.getUser();
194         if (!isAuthorized(currentUser)) {
195             String msg = simpleTranslator.translate("console.user.unauthorized", currentUser.getName());
196             log.warn(msg);
197             getState().output = msg;
198             return;
199         }
200 
201         log.debug("executing command [{}]", command);
202         StringWriter sw = new StringWriter();
203 
204         if (handleDefaultCommand(command, sw)) {
205             getState().output = sw.toString();
206         } else {
207             getState().output = simpleTranslator.translate("groovy.script.consoleOutput.run.wait");
208             runAsync(command);
209         }
210     }
211 
212     private void runAsync(final String command) throws CompilationFailedException, IOException, InterruptedException {
213         final MgnlGroovyConsole console = getOrCreateMgnlGroovyConsole();
214         console.runAsync(command, UI.getCurrent(), new ScriptCallback() {
215             @Override
216             public void onSuccess(String result) {
217                 getState().output = result;
218                 getState().inProgress = false;
219                 // update suggestions
220                 Binding context = console.getContext();
221                 updateCodeSuggestions(context);
222                 updateBindingSuggestions(context);
223             }
224 
225             @Override
226             public void onFailure(Throwable e) {
227                 getState().output = e.getMessage();
228                 getState().inProgress = false;
229             }
230 
231             @Override
232             public void onProgress(Writer out) {
233                 getState().inProgress = true;
234             }
235 
236             @Override
237             public boolean requiresNotificationMessageUponCompletion() {
238                 return requiresNotificationMessageUponCompletion;
239             }
240         });
241     }
242 
243     /**
244      * @return <code>true</code> if the command could be processed, <code>false</code> otherwise.
245      * @throws Exception
246      */
247     private boolean handleDefaultCommand(final String command, final StringWriter out) throws Exception {
248         if (StringUtils.isEmpty(command)) {
249             // just ignore it
250             return true;
251         }
252 
253         String[] tokens = command.trim().split("\\s+");
254 
255         if ("help".equals(tokens[0]) || "?".equals(tokens[0])) {
256             out.write(simpleTranslator.translate("console.help"));
257             return true;
258         } else if ("clear".equals(tokens[0])) {
259             return true;
260         } else if ("clean".equals(tokens[0])) {
261             MgnlContext.setAttribute(BINDING_SESSION_ATTRIBUTE, new SerializableBinding(), Context.SESSION_SCOPE);
262             setPredefinedVariables();
263             return true;
264         } else if ("run".equals(tokens[0])) {
265             out.write(simpleTranslator.translate("groovy.script.consoleOutput.run.wait"));
266             return runScript(tokens, out);
267         }
268         return false;
269     }
270 
271     private boolean runScript(final String[] tokens, final StringWriter out) throws Exception {
272         if (tokens.length != 2) {
273             out.write(simpleTranslator.translate("console.commands.run.usage"));
274             return true;
275         }
276 
277         String source = null;
278         String path = tokens[1];
279         // first try to locate the script in the scripts workspace
280         Node scriptNode = SessionUtil.getNode("scripts", path);
281         if (scriptNode != null) {
282             source = PropertyUtil.getString(scriptNode, "text", "");
283         } else {
284             // try with the file system
285             try {
286                 byte[] bytes = Files.readAllBytes(Paths.get(path));
287                 source = new String(bytes);
288             } catch (IOException e) {
289                 // ignore here
290             }
291         }
292         if (source == null) {
293             out.write(simpleTranslator.translate("console.commands.run.notFound", path) + "\n");
294             out.write(simpleTranslator.translate("console.commands.run.usage"));
295         } else {
296             runAsync(source);
297         }
298         return true;
299     }
300 
301     private void resetState() {
302         getState().output = "";
303         getState().view = "";
304         getState().history = "";
305         getState().inProgress = false;
306     }
307 
308     protected boolean isAuthorized(User currentUser) {
309         final Collection<String> roles = currentUser.getRoles();
310         return roles.contains("superuser") || roles.contains("scripter");
311     }
312 
313     private MgnlGroovyConsole getOrCreateMgnlGroovyConsole() {
314         Binding binding = MgnlContext.getAttribute(BINDING_SESSION_ATTRIBUTE, Context.SESSION_SCOPE);
315         MgnlGroovyConsole console = null;
316         if (binding != null) {
317             console = new MgnlGroovyConsole(binding, messagesManager, simpleTranslator);
318         } else {
319             Binding newBinding = new SerializableBinding();
320             MgnlContext.setAttribute(BINDING_SESSION_ATTRIBUTE, newBinding, Context.SESSION_SCOPE);
321             console = new MgnlGroovyConsole(newBinding, messagesManager, simpleTranslator);
322         }
323         return console;
324     }
325 
326     private void setPredefinedVariables() {
327         this.getState().binding = gson.toJson(PREDEFINED_VARS);
328     }
329 
330     private void updateBindingSuggestions(final Binding context) {
331         Set<String> predefined = new HashSet<String>();
332         predefined.addAll(PREDEFINED_VARS);
333         predefined.addAll(context.getVariables().keySet());
334         getState().binding = gson.toJson(predefined);
335     }
336 
337     private void updateCodeSuggestions(final Binding context) {
338         Map<String, Object> variables = context.getVariables();
339         for (Entry<String, Object> entry : variables.entrySet()) {
340             String key = entry.getKey();
341             if (suggestions.containsKey(key)) {
342                 continue;
343             }
344             createSuggestionsPerClass(key, entry.getValue());
345         }
346         getState().suggestions = gson.toJson(suggestions);
347     }
348 
349     private void createSuggestionsPerClass(String key, Object value) {
350         Set<String> data = new HashSet<String>();
351         for (Method method : value.getClass().getMethods()) {
352             data.add(method.getName());
353         }
354 
355         if (value instanceof GroovyObject) {
356             GroovyObject obj = (GroovyObject) value;
357             for (MetaMethod method : obj.getMetaClass().getMethods()) {
358                 data.add(method.getName());
359             }
360         }
361         if (!data.isEmpty()) {
362             data.removeAll(METHODS_BLACK_LIST);
363             suggestions.put(key, data);
364         }
365     }
366 }