View Javadoc

1   /**
2    * This file Copyright (c) 2013 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 groovy.lang.Binding;
37  import groovy.lang.GroovySystem;
38  import groovy.lang.MissingPropertyException;
39  
40  import info.magnolia.cms.security.User;
41  import info.magnolia.cms.util.NodeDataUtil;
42  import info.magnolia.context.Context;
43  import info.magnolia.context.MgnlContext;
44  import info.magnolia.context.MgnlContext.Op;
45  import info.magnolia.i18nsystem.SimpleTranslator;
46  import info.magnolia.module.groovy.console.MgnlGroovyConsole;
47  import info.magnolia.module.groovy.console.MgnlGroovyConsoleContext;
48  
49  import java.io.ByteArrayInputStream;
50  import java.io.FileInputStream;
51  import java.io.FileNotFoundException;
52  import java.io.InputStream;
53  import java.io.Serializable;
54  import java.io.StringWriter;
55  import java.util.Collection;
56  
57  import javax.inject.Inject;
58  import javax.jcr.RepositoryException;
59  
60  import org.apache.commons.io.IOUtils;
61  import org.apache.commons.lang.StringUtils;
62  import org.codehaus.groovy.control.CompilationFailedException;
63  import org.codehaus.groovy.runtime.InvokerInvocationException;
64  import org.json.JSONArray;
65  import org.json.JSONException;
66  import org.slf4j.Logger;
67  import org.slf4j.LoggerFactory;
68  
69  import com.vaadin.annotations.JavaScript;
70  import com.vaadin.annotations.StyleSheet;
71  import com.vaadin.ui.AbstractJavaScriptComponent;
72  import com.vaadin.ui.JavaScriptFunction;
73  
74  /**
75   * 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.
76   */
77  @JavaScript({ "https://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js", "jquery.terminal-0.7.7.min.js", "terminal_connector.js" })
78  @StyleSheet("jquery.terminal.css")
79  public class Terminal extends AbstractJavaScriptComponent {
80  
81      private static final Logger log = LoggerFactory.getLogger(Terminal.class);
82      private SimpleTranslator simpleTranslator;
83      private boolean useSystemContext;
84  
85      public static final String BINDING_SESSION_ATTRIBUTE = "info.magnolia.module.groovy.console.binding.session.attribute";
86  
87      /**
88       * @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}).
89       */
90      @Inject
91      public Terminal(final SimpleTranslator simpleTranslator, final boolean useSystemContext) {
92          this.simpleTranslator = simpleTranslator;
93          this.useSystemContext = useSystemContext;
94          this.getState().greetings = simpleTranslator.translate("console.greetings", GroovySystem.getVersion());
95  
96          addFunction("executeCommand", new JavaScriptFunction() {
97              @Override
98              public void call(JSONArray arguments) throws JSONException {
99                  getState().command = arguments.getString(0);
100                 resetState();
101                 try {
102                     if (isUseSystemContext()) {
103                         MgnlContext.doInSystemContext(new Op<Void, Exception>() {
104 
105                             @Override
106                             public Void exec() throws Exception {
107                                 execute(getCommand());
108                                 return null;
109                             }
110                         });
111                     } else {
112                         execute(getCommand());
113                     }
114                 } catch (Exception e) {
115                     getState().output = e.getMessage();
116                 }
117             }
118         });
119 
120         addFunction("saveStatus", new JavaScriptFunction() {
121             @Override
122             public void call(JSONArray arguments) throws JSONException {
123                 resetState();
124                 getState().view = arguments.getString(0);
125                 getState().history = arguments.getString(1);
126                 log.debug("saved status [view:{}, history{}]", getState().view, getState().history);
127             }
128         });
129 
130         setSizeFull();
131     }
132 
133     public boolean isUseSystemContext() {
134         return useSystemContext;
135     }
136 
137     public String getCommand() {
138         return getState().command;
139     }
140 
141     @Override
142     public TerminalState getState() {
143         return (TerminalState) super.getState();
144     }
145 
146     public void execute(String command) throws Exception {
147         User currentUser = MgnlContext.getUser();
148         if (!isAuthorized(currentUser)) {
149             String msg = simpleTranslator.translate("console.user.unauthorized", currentUser.getName());
150             log.warn(msg);
151             getState().output = msg;
152             return;
153         }
154         log.debug("executing command [{}]", command);
155         StringWriter sw = new StringWriter();
156         if (handleCommand(command, sw)) {
157             getState().output = sw.toString();
158         } else {
159             getState().output = evaluate(new ByteArrayInputStream(command.getBytes()));
160         }
161     }
162 
163     private String evaluate(final InputStream is) {
164 
165         Binding binding = MgnlContext.getAttribute(BINDING_SESSION_ATTRIBUTE, Context.SESSION_SCOPE);
166         MgnlGroovyConsole console = null;
167         if (binding == null) {
168             Binding newBinding = new SerializableBinding();
169             MgnlContext.setAttribute(BINDING_SESSION_ATTRIBUTE, newBinding, Context.SESSION_SCOPE);
170             console = new MgnlGroovyConsole(newBinding);
171         } else {
172             console = new MgnlGroovyConsole(binding);
173         }
174 
175         Object lastResult;
176         Context originalCtx = MgnlContext.getInstance();
177         MgnlGroovyConsoleContext groovyCtx = new MgnlGroovyConsoleContext(originalCtx);
178         MgnlContext.setInstance(groovyCtx);
179         StringWriter sw = new StringWriter();
180         try {
181             lastResult = console.evaluate(is, console.generateScriptName(), sw);
182             // Shows the result of the evaluated code
183             sw.write("===> \n");
184             sw.write(lastResult != null ? lastResult.toString() : "");
185         } catch (CompilationFailedException e) {
186             sw.write(e.getMessage());
187         } catch (Throwable e) {
188             // Unroll invoker exceptions
189             if (e instanceof InvokerInvocationException) {
190                 e = e.getCause();
191             }
192             if (e instanceof MissingPropertyException) {
193                 sw.write(e.getMessage());
194             } else {
195                 log.error("Error while evaluating script: ", e);
196                 sw.write(e.getClass().getSimpleName() + ": " + e.getMessage());
197             }
198         } finally {
199             MgnlContext.setInstance(originalCtx);
200         }
201         return sw.toString();
202     }
203 
204     /**
205      * SerializableBinding. Need this otherwise adding Binding to the session fails with
206      * java.lang.IllegalArgumentException: setAttribute: Non-serializable attribute
207      * at org.apache.catalina.session.StandardSession.setAttribute(..)
208      */
209     public static final class SerializableBinding extends Binding implements Serializable {
210 
211         public static final long serialVersionUID = 42L;
212     }
213 
214     /**
215      * @return <code>true</code> if the command could be processed, <code>false</code> otherwise.
216      */
217     private boolean handleCommand(final String command, final StringWriter out) throws RepositoryException {
218         if (StringUtils.isEmpty(command)) {
219             // just ignore it
220             return true;
221         }
222         String[] tokens = command.trim().split("\\s+");
223         if ("help".equals(tokens[0]) || "?".equals(tokens[0])) {
224             out.write(simpleTranslator.translate("console.help"));
225             return true;
226         } else if ("clear".equals(tokens[0])) {
227             return true;
228         } else if ("clean".equals(tokens[0])) {
229             MgnlContext.setAttribute(BINDING_SESSION_ATTRIBUTE, new SerializableBinding(), Context.SESSION_SCOPE);
230             return true;
231         } else if ("run".equals(tokens[0])) {
232             if (tokens.length != 2) {
233                 out.write(simpleTranslator.translate("console.commands.run.usage"));
234                 return true;
235             }
236             InputStream is = null;
237             String path = tokens[1];
238             // first try to locate the script in the scripts workspace
239             String source = NodeDataUtil.getString("scripts", path.endsWith("/") ? path + "text" : path + "/text");
240             try {
241                 if (StringUtils.isNotEmpty(source)) {
242                     is = new ByteArrayInputStream(source.getBytes());
243                 }
244                 else {
245                     // if not found in the workspace, try with the file system
246                     is = new FileInputStream(path);
247                 }
248                 evaluate(is);
249                 return true;
250             } catch (FileNotFoundException e) {
251                 out.write(e.getMessage());
252                 return true;
253             } finally {
254                 IOUtils.closeQuietly(is);
255             }
256         }
257         return false;
258     }
259 
260     private void resetState() {
261         getState().output = "";
262         getState().view = "";
263         getState().history = "";
264     }
265 
266     protected boolean isAuthorized(User currentUser) {
267         final Collection<String> roles = currentUser.getRoles();
268         return roles.contains("superuser") || roles.contains("scripter");
269     }
270 
271 }