001/* 002 * Copyright 2016 Anyware Services 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package org.ametys.plugins.core.ui.script; 017 018import java.io.PrintWriter; 019import java.io.StringWriter; 020import java.util.ArrayList; 021import java.util.Collection; 022import java.util.Date; 023import java.util.HashMap; 024import java.util.HashSet; 025import java.util.List; 026import java.util.Map; 027import java.util.Map.Entry; 028import java.util.Optional; 029import java.util.stream.Collectors; 030 031import javax.script.ScriptContext; 032import javax.script.ScriptEngine; 033import javax.script.ScriptEngineManager; 034import javax.script.ScriptException; 035import javax.script.SimpleBindings; 036 037import org.apache.avalon.framework.component.Component; 038import org.apache.avalon.framework.context.Context; 039import org.apache.avalon.framework.context.ContextException; 040import org.apache.avalon.framework.context.Contextualizable; 041import org.apache.avalon.framework.service.ServiceException; 042import org.apache.avalon.framework.service.ServiceManager; 043import org.apache.avalon.framework.service.Serviceable; 044import org.apache.cocoon.components.ContextHelper; 045import org.apache.commons.lang.exception.ExceptionUtils; 046import org.apache.commons.lang3.StringUtils; 047 048import org.ametys.core.right.RightManager; 049import org.ametys.core.ui.Callable; 050import org.ametys.core.user.CurrentUserProvider; 051import org.ametys.core.util.DateUtils; 052import org.ametys.runtime.i18n.I18nizableText; 053import org.ametys.runtime.plugin.component.AbstractLogEnabled; 054 055/** 056 * Handler to describe and execute server scripts 057 */ 058public class ScriptHandler extends AbstractLogEnabled implements Component, Serviceable, Contextualizable 059{ 060 private static final String __RIGHT_EXECUTE_SCRIPTS = "CORE_Rights_ExecuteScript"; 061 062 private static final String __SCRIPT_INSERT_CLEANUP_MANAGER = "var __cleanup_manager = { _registered:[], register: function (f) { this._registered.push(f) }, cleanup : function () { this._registered.forEach(function (f) {f()} ) } };"; 063 private static final String __SCRIPT_INSERT_RUN_MAIN = "var __result; try { __result = main(); } finally { __cleanup_manager.cleanup() } __result"; 064 065 /** The script binding extension point */ 066 protected ScriptBindingExtensionPoint _scriptBindingEP; 067 068 /** The right manager */ 069 protected RightManager _rightManager; 070 071 /** The current user provider*/ 072 protected CurrentUserProvider _currentUserProvider; 073 074 /** The avalon context */ 075 protected Context _context; 076 077 @Override 078 public void service(ServiceManager serviceManager) throws ServiceException 079 { 080 _scriptBindingEP = (ScriptBindingExtensionPoint) serviceManager.lookup(ScriptBindingExtensionPoint.ROLE); 081 _rightManager = (RightManager) serviceManager.lookup(RightManager.ROLE); 082 _currentUserProvider = (CurrentUserProvider) serviceManager.lookup(CurrentUserProvider.ROLE); 083 } 084 085 public void contextualize(Context context) throws ContextException 086 { 087 _context = context; 088 } 089 090 /** 091 * Execute a script in the js admin console. 092 * @param arguments The map of arguments. Must contains at least the argument "script" 093 * @return A map of information on the script execution. 094 * @throws ScriptException If an error occurs 095 */ 096 @Callable 097 public Map<String, Object> executeScript(Map<String, Object> arguments) throws ScriptException 098 { 099 return executeScript((String) arguments.get("script")); 100 } 101 102 /** 103 * Execute a script in the js admin console. 104 * @param script The script as a String. 105 * @return A map of information on the script execution. 106 * @throws ScriptException If an error occurs 107 */ 108 @Callable 109 public Map<String, Object> executeScript(String script) throws ScriptException 110 { 111 Map<String, Object> results = new HashMap<>(); 112 113 if (_rightManager.hasRight(_currentUserProvider.getUser(), __RIGHT_EXECUTE_SCRIPTS, "/application") != RightManager.RightResult.RIGHT_ALLOW) 114 { 115 // FIXME Currently unable to assign rights to a user in the _admin workspace 116 // throw new RightsException("Insufficient rights to execute a script"); 117 } 118 119 results.put("start", DateUtils.dateToString(new Date())); 120 121 ScriptEngineManager mgr = new ScriptEngineManager(); 122 ScriptEngine engine = mgr.getEngineByName("javascript"); 123 124 // Redirect output to a String 125 StringWriter output = new StringWriter(); 126 PrintWriter pw = new PrintWriter(output); 127 ScriptContext context = engine.getContext(); 128 context.setWriter(pw); 129 130 StringWriter errorOutput = new StringWriter(); 131 PrintWriter errorPw = new PrintWriter(errorOutput); 132 context.setErrorWriter(errorPw); 133 134 context.setAttribute(ScriptEngine.FILENAME, "generated script", ScriptContext.GLOBAL_SCOPE); 135 136 Map<String, Object> variables = new HashMap<>(); 137 138 List<ScriptBinding> scriptBindings = getScriptBindings(); 139 140 try 141 { 142 List<String> scriptText = new ArrayList<>(); 143 scriptText.add(script); 144 scriptText.add(__SCRIPT_INSERT_CLEANUP_MANAGER); 145 setScriptBindings(variables, scriptText, scriptBindings); 146 scriptText.add(__SCRIPT_INSERT_RUN_MAIN); 147 148 // Create bindings 149 SimpleBindings sb = new SimpleBindings(); 150 for (Entry<String, Object> entry : variables.entrySet()) 151 { 152 sb.put(entry.getKey(), entry.getValue()); 153 } 154 155 // Execute script 156 Object scriptResult = engine.eval(StringUtils.join(scriptText, "\n"), sb); 157 if (scriptResult != null) 158 { 159 results.put("result", processScriptResult(results, scriptBindings, scriptResult)); 160 } 161 } 162 catch (Throwable t) 163 { 164 results.put("message", StringUtils.defaultString(t.getMessage())); 165 results.put("stacktrace", ExceptionUtils.getFullStackTrace(t)); 166 getLogger().error("An exception occurred while running script", t); 167 } 168 finally 169 { 170 results.put("end", DateUtils.dateToString(new Date())); 171 // Script output and error. 172 results.put("output", output.toString()); 173 results.put("error", errorOutput.toString()); 174 175 for (String extensionId : _scriptBindingEP.getExtensionsIds()) 176 { 177 ScriptBinding scriptBinding = _scriptBindingEP.getExtension(extensionId); 178 179 scriptBinding.cleanVariables(variables); 180 } 181 } 182 183 return results; 184 } 185 186 private void setScriptBindings(Map<String, Object> variables, List<String> scriptText, List<ScriptBinding> scriptBindings) 187 { 188 for (ScriptBinding scriptBinding : scriptBindings) 189 { 190 Map<String, Object> scriptBindingVariables = scriptBinding.getVariables(); 191 if (scriptBindingVariables != null) 192 { 193 variables.putAll(scriptBindingVariables); 194 } 195 196 String scriptBindingFunctions = scriptBinding.getFunctions(); 197 if (scriptBindingFunctions != null) 198 { 199 scriptText.add(scriptBindingFunctions); 200 } 201 } 202 } 203 204 /** 205 * Process the result of the script 206 * @param results The results map, available to fill 207 * @param scriptBindings The script bindings 208 * @param scriptResult The result of the script 209 * @return The result processed 210 * @throws ScriptException If an exception occurred 211 */ 212 protected Object processScriptResult(Map<String, Object> results, List<ScriptBinding> scriptBindings, Object scriptResult) throws ScriptException 213 { 214 for (ScriptBinding scriptBinding : scriptBindings) 215 { 216 Object processedResult = scriptBinding.processScriptResult(scriptResult); 217 if (processedResult != null) 218 { 219 return processedResult; 220 } 221 } 222 223 if (scriptResult instanceof Collection) 224 { 225 // Collection 226 List<Object> elements = new ArrayList<>(); 227 for (Object obj : (Collection<?>) scriptResult) 228 { 229 elements.add(processScriptResult(results, scriptBindings, obj)); 230 } 231 return elements; 232 } 233 else if (scriptResult instanceof Map) 234 { 235 // Map 236 Map<Object, Object> elements = new HashMap<>(); 237 for (Object key : ((Map) scriptResult).keySet()) 238 { 239 Object value = ((Map) scriptResult).get(key); 240 elements.put(processScriptResult(results, scriptBindings, key), processScriptResult(results, scriptBindings, value)); 241 } 242 return elements; 243 } 244 else 245 { 246 return scriptResult.toString(); 247 } 248 } 249 250 /** 251 * Get the list of variables and functions descriptions currently registered for the Scripts. 252 * @return The list of variables and functions, as describes by the script bindings available. 253 */ 254 @Callable 255 public Map<String, Object> getScriptBindingDescription() 256 { 257 Map<String, I18nizableText> variablesDesc = new HashMap<>(); 258 Map<String, I18nizableText> functionsDesc = new HashMap<>(); 259 List<ScriptBinding> scriptBindings = getScriptBindings(); 260 for (ScriptBinding scriptBinding : scriptBindings) 261 { 262 Map<String, I18nizableText> scriptBindingVariablesDesc = scriptBinding.getVariablesDescriptions(); 263 if (scriptBindingVariablesDesc != null) 264 { 265 // Warn if any variable was already declared by another ScriptBinding. 266 HashSet<String> intersection = new HashSet<>(variablesDesc.keySet()); 267 intersection.retainAll(scriptBindingVariablesDesc.keySet()); 268 if (intersection.size() > 0) 269 { 270 for (String variable : intersection) 271 { 272 getLogger().warn("Multiple ScriptBinding use the same variable name : '" + variable + "'. Only one of these variables will be available."); 273 } 274 } 275 276 variablesDesc.putAll(scriptBindingVariablesDesc); 277 } 278 279 Map<String, I18nizableText> scriptBindingFunctionsDesc = scriptBinding.getFunctionsDescriptions(); 280 if (scriptBindingFunctionsDesc != null) 281 { 282 // Warn if any function was already declared by another ScriptBinding. 283 HashSet<String> intersection = new HashSet<>(functionsDesc.keySet()); 284 intersection.retainAll(scriptBindingFunctionsDesc.keySet()); 285 if (intersection.size() > 0) 286 { 287 for (String function : intersection) 288 { 289 getLogger().warn("Multiple ScriptBinding use the same function name : '" + function + "'. Your scripts may not be able to run properly."); 290 } 291 } 292 293 functionsDesc.putAll(scriptBindingFunctionsDesc); 294 } 295 } 296 297 HashMap<String, Object> result = new HashMap<>(); 298 if (variablesDesc.size() > 0) 299 { 300 result.put("variables", variablesDesc); 301 } 302 if (functionsDesc.size() > 0) 303 { 304 result.put("functions", functionsDesc); 305 } 306 return result; 307 } 308 309 /** 310 * Get the list of script bindings 311 * @return The list of script binding 312 */ 313 protected List<ScriptBinding> getScriptBindings() 314 { 315 // By default, if there is no workspace, such as a script executed from a scheduled task, 316 // we want the script to be iso-functional with a script executed from the admin 317 String workspaceName = Optional.ofNullable((String) ContextHelper.getRequest(_context).getAttribute("workspaceName")).orElse("admin"); 318 319 return _scriptBindingEP.getExtensionsIds() 320 .stream() 321 .map(id -> _scriptBindingEP.getExtension(id)) 322 .filter(binding -> binding.getWorkspacePattern().matcher(workspaceName).matches()) 323 .collect(Collectors.toList()); 324 } 325 326 327}