1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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
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
99
100
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
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
125
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
212 sw.write(lastResult != null ? lastResult.toString() : "");
213 } catch (CompilationFailedException e) {
214 sw.write(e.getMessage());
215 } catch (Throwable e) {
216
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
231 Binding context = console.getContext();
232 updateCodeSuggestions(context);
233 updateBindingSuggestions(context);
234
235 return sw.toString();
236 }
237
238
239
240
241 private boolean handleCommand(final String command, final StringWriter out) throws RepositoryException {
242 if (StringUtils.isEmpty(command)) {
243
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
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
281 try {
282 is = new FileInputStream(path);
283 } catch (FileNotFoundException e) {
284
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 }