/*
* Freeplane - A Program for creating and viewing MindmapsCopyright (C) 2000-2006
* Joerg Mueller, Daniel Polansky, Christian Foltin and others.See COPYING for
* DetailsThis program is free software; you can redistribute it and/ormodify it
* under the terms of the GNU General Public Licenseas published by the Free
* Software Foundation; either version 2of the License, or (at your option) any
* later version.This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty ofMERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See theGNU General Public License for
* more details.You should have received a copy of the GNU General Public
* Licensealong with this program; if not, write to the Free SoftwareFoundation,
* Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. Created on
* 02.09.2006
*/
/*
* $Id: ScriptingEngine.java,v 1.1.2.20 2008/04/18 21:18:26 christianfoltin Exp
* $
*/
package org.freeplane.plugin.script;
import groovy.lang.Binding;
import groovy.lang.GroovyCodeSource;
import groovy.lang.GroovyRuntimeException;
import groovy.lang.GroovyShell;
import groovy.lang.Script;
import java.io.File;
import java.io.PrintStream;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.regex.Matcher;
import javax.swing.JOptionPane;
import org.apache.commons.lang.WordUtils;
import org.codehaus.groovy.ast.ASTNode;
import org.codehaus.groovy.ast.ModuleNode;
import org.codehaus.groovy.control.CompilationFailedException;
import org.codehaus.groovy.control.CompilerConfiguration;
import org.codehaus.groovy.runtime.InvokerHelper;
import org.freeplane.core.resources.ResourceController;
import org.freeplane.core.ui.components.OptionalDontShowMeAgainDialog;
import org.freeplane.core.ui.components.UITools;
import org.freeplane.core.util.LogUtils;
import org.freeplane.core.util.TextUtils;
import org.freeplane.features.attribute.NodeAttributeTableModel;
import org.freeplane.features.map.NodeModel;
import org.freeplane.features.mode.Controller;
import org.freeplane.features.mode.ModeController;
import org.freeplane.main.application.FreeplaneSecurityManager;
import org.freeplane.plugin.script.proxy.ProxyFactory;
/**
* @author foltin
*/
public class ScriptingEngine {
public interface IErrorHandler {
void gotoLine(int pLineNumber);
}
public static final String RESOURCES_SCRIPT_DIRECTORIES = "script_directories";
public static final String RESOURCES_SCRIPT_CLASSPATH = "script_classpath";
public static final String SCRIPT_PREFIX = "script";
private static final HashMap<String, Object> sScriptCookies = new HashMap<String, Object>();
private static List<String> classpath;
private static final IErrorHandler scriptErrorHandler = new IErrorHandler() {
public void gotoLine(final int pLineNumber) {
}
};
/**
* @param permissions if null use default scripting permissions.
* @return the result of the script, or null, if the user has cancelled.
* @throws ExecuteScriptException on errors
*/
static Object executeScript(final NodeModel node, final String script, final IErrorHandler pErrorHandler,
final PrintStream pOutStream, final ScriptContext scriptContext,
ScriptingPermissions permissions) {
return executeScript(node, (Object)script, pErrorHandler,
pOutStream, scriptContext,
permissions);
}
static Object executeScript(final NodeModel node, final File script, final IErrorHandler pErrorHandler,
final PrintStream pOutStream, final ScriptContext scriptContext,
ScriptingPermissions permissions) {
return executeScript(node, (Object)script, pErrorHandler,
pOutStream, scriptContext,
permissions);
}
static private Object executeScript(final NodeModel node, final Object script, final IErrorHandler pErrorHandler,
final PrintStream pOutStream, final ScriptContext scriptContext,
ScriptingPermissions permissions) {
final Binding binding = new Binding();
binding.setVariable("c", ProxyFactory.createController(scriptContext));
binding.setVariable("node", ProxyFactory.createNode(node, scriptContext));
binding.setVariable("cookies", ScriptingEngine.sScriptCookies);
final PrintStream oldOut = System.out;
//
// == Security stuff ==
//
final FreeplaneSecurityManager securityManager = (FreeplaneSecurityManager) System.getSecurityManager();
final ScriptingSecurityManager scriptingSecurityManager;
final boolean needsSecurityManager = securityManager.needsFinalSecurityManager();
// get preferences (and store them again after the script execution,
// such that the scripts are not able to change them).
if (needsSecurityManager) {
if (permissions == null){
permissions = new ScriptingPermissions(ResourceController.getResourceController().getProperties());
}
if (!permissions.executeScriptsWithoutAsking()) {
final int showResult = OptionalDontShowMeAgainDialog.show("really_execute_script", "confirmation",
ScriptingPermissions.RESOURCES_EXECUTE_SCRIPTS_WITHOUT_ASKING,
OptionalDontShowMeAgainDialog.BOTH_OK_AND_CANCEL_OPTIONS_ARE_STORED);
if (showResult != JOptionPane.OK_OPTION) {
throw new ExecuteScriptException(new SecurityException(TextUtils.getText("script_execution_disabled")));
}
}
final boolean executeSignedScripts = permissions.isExecuteSignedScriptsWithoutRestriction();
final String scriptContent;
if(script instanceof String)
scriptContent = (String) script;
else
scriptContent = null;
if (executeSignedScripts && scriptContent != null && new SignedScriptHandler().isScriptSigned(scriptContent, pOutStream)) {
scriptingSecurityManager = permissions.getPermissiveScriptingSecurityManager();
}
else
scriptingSecurityManager = permissions.getScriptingSecurityManager();
}
else {
// will not be used
scriptingSecurityManager = null;
}
//
// == execute ==
//
ScriptingPermissions originalScriptingPermissions = new ScriptingPermissions(ResourceController.getResourceController().getProperties());
try {
System.setOut(pOutStream);
final ClassLoader classLoader = ScriptingEngine.class.getClassLoader();
final GroovyShell shell = new GroovyShell(classLoader, binding, createCompilerConfiguration()) {
/**
* Evaluates some script against the current Binding and returns the result
*
* @param in the stream reading the script
* @param fileName is the logical file name of the script (which is used to create the class name of the script)
*/
@Override
public Object evaluate(GroovyCodeSource codeSource) throws CompilationFailedException {
Script script = null;
try {
script = parse(codeSource);
script.setBinding(getContext());
if (needsSecurityManager)
securityManager.setFinalSecurityManager(scriptingSecurityManager);
return script.run();
}
finally {
if (script != null) {
InvokerHelper.removeClass(script.getClass());
if (needsSecurityManager)
securityManager.removeFinalSecurityManager(scriptingSecurityManager);
}
}
}
};
if(script instanceof String)
return shell.evaluate((String)script);
if(script instanceof File)
return shell.evaluate((File)script);
throw new IllegalArgumentException();
}
catch (final GroovyRuntimeException e) {
/*
* Cover exceptions in normal security context (ie. no problem with
* (log) file writing etc.)
*/
// LogUtils.warn(e);
final String resultString = e.getMessage();
pOutStream.print("message: " + resultString);
final ModuleNode module = e.getModule();
final ASTNode astNode = e.getNode();
int lineNumber = -1;
if (module != null) {
lineNumber = module.getLineNumber();
}
else if (astNode != null) {
lineNumber = astNode.getLineNumber();
}
else {
lineNumber = ScriptingEngine.findLineNumberInString(resultString, lineNumber);
}
pOutStream.print("Line number: " + lineNumber);
pErrorHandler.gotoLine(lineNumber);
throw new ExecuteScriptException(e.getMessage() + " at line " + lineNumber, e);
}
catch (final Throwable e) {
if (Controller.getCurrentController().getSelection() != null)
Controller.getCurrentModeController().getMapController().select(node);
throw new ExecuteScriptException(e.getMessage(), e);
}
finally {
System.setOut(oldOut);
/* restore preferences (and assure that the values are unchanged!). */
originalScriptingPermissions.restorePermissions();
}
}
private static CompilerConfiguration createCompilerConfiguration() {
CompilerConfiguration config = new CompilerConfiguration();
config.setScriptBaseClass(FreeplaneScriptBaseClass.class.getName());
if (!(classpath == null || classpath.isEmpty())) {
config.setClasspathList(classpath);
}
return config;
}
public static int findLineNumberInString(final String resultString, int lineNumber) {
final java.util.regex.Pattern pattern = java.util.regex.Pattern.compile(".*@ line ([0-9]+).*",
java.util.regex.Pattern.DOTALL);
final Matcher matcher = pattern.matcher(resultString);
if (matcher.matches()) {
lineNumber = Integer.parseInt(matcher.group(1));
}
return lineNumber;
}
public static Object executeScript(final NodeModel node, final String script) {
return ScriptingEngine.executeScript(node, script, null, null);
}
public static Object executeScript(NodeModel node, File script, ScriptingPermissions permissions) {
return ScriptingEngine.executeScript(node, script, ScriptingEngine.scriptErrorHandler, System.out, null, permissions);
}
public static Object executeScript(NodeModel node, String script, ScriptingPermissions permissions) {
return ScriptingEngine.executeScript(node, script, ScriptingEngine.scriptErrorHandler, System.out, null, permissions);
}
public static Object executeScript(NodeModel node, String script, PrintStream printStream) {
return ScriptingEngine.executeScript(node, script, ScriptingEngine.scriptErrorHandler, printStream, null, null);
}
public static Object executeScript(final NodeModel node, final String script, final ScriptContext scriptContext,
final ScriptingPermissions permissions) {
return ScriptingEngine.executeScript(node, script, scriptErrorHandler, System.out, scriptContext, permissions);
}
static Object executeScriptRecursive(final NodeModel node, final File script,
final ScriptingPermissions permissions) {
ModeController modeController = Controller.getCurrentModeController();
final NodeModel[] children = modeController.getMapController().childrenUnfolded(node)
.toArray(new NodeModel[] {});
for (final NodeModel child : children) {
executeScriptRecursive(child, script, permissions);
}
return executeScript(node, script, permissions);
}
static void performScriptOperationRecursive(final NodeModel node) {
ModeController modeController = Controller.getCurrentModeController();
for (final NodeModel child : modeController.getMapController().childrenUnfolded(node)) {
performScriptOperationRecursive(child);
}
performScriptOperation(node);
}
static void performScriptOperation(final NodeModel node) {
final NodeAttributeTableModel attributes = NodeAttributeTableModel.getModel(node);
if (attributes == null) {
return;
}
for (int row = 0; row < attributes.getRowCount(); ++row) {
final String attrKey = (String) attributes.getName(row);
final Object value = attributes.getValue(row);
if(value instanceof String){
final String script = (String) value;
if (attrKey.startsWith(ScriptingEngine.SCRIPT_PREFIX)) {
executeScript(node, script);
}
}
}
return;
}
/** allows to set the classpath for scripts. Due to security considerations it's not possible to set
* this more than once. */
static void setClasspath(final List<String> classpath) {
if (ScriptingEngine.classpath != null)
throw new SecurityException("reset of script classpath is forbidden.");
ScriptingEngine.classpath = Collections.unmodifiableList(classpath);
if (!classpath.isEmpty())
LogUtils.info("extending script's classpath by " + classpath);
}
static List<String> getClasspath() {
return classpath;
}
public static File getUserScriptDir() {
final String userDir = ResourceController.getResourceController().getFreeplaneUserDirectory();
return new File(userDir, ScriptingConfiguration.USER_SCRIPTS_DIR);
}
static void showScriptExceptionErrorMessage(ExecuteScriptException ex) {
if (ex.getCause() instanceof SecurityException) {
final String message = WordUtils.wrap(ex.getCause().getMessage(), 80, "\n ", false);
UITools.errorMessage(TextUtils.format("ExecuteScriptSecurityError.text", message));
}
else {
final String message = WordUtils.wrap(ex.getMessage(), 80, "\n ", false);
UITools.errorMessage(TextUtils.format("ExecuteScriptError.text", message));
}
}
}