/* * Copyright (c) 2014 Data Harmonisation Panel * * All rights reserved. This program and the accompanying materials are made * available under the terms of the GNU Lesser General Public License as * published by the Free Software Foundation, either version 3 of the License, * or (at your option) any later version. * * You should have received a copy of the GNU Lesser General Public License * along with this distribution. If not, see <http://www.gnu.org/licenses/>. * * Contributors: * Data Harmonisation Panel <http://www.dhpanel.eu> */ package eu.esdihumboldt.hale.ui.service.groovy.internal; import java.io.UnsupportedEncodingException; import java.net.URI; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.concurrent.atomic.AtomicBoolean; import org.eclipse.jface.dialogs.MessageDialog; import org.eclipse.swt.widgets.Display; import org.eclipse.ui.PlatformUI; import com.google.common.base.Objects; import com.google.common.collect.ListMultimap; import eu.esdihumboldt.cst.functions.groovy.GroovyConstants; import eu.esdihumboldt.hale.common.align.custom.CustomPropertyFunctionType; import eu.esdihumboldt.hale.common.align.custom.DefaultCustomPropertyFunction; import eu.esdihumboldt.hale.common.align.extension.function.custom.CustomPropertyFunction; import eu.esdihumboldt.hale.common.align.model.Cell; import eu.esdihumboldt.hale.common.align.model.ParameterValue; import eu.esdihumboldt.hale.common.core.io.Text; import eu.esdihumboldt.hale.common.core.io.Value; import eu.esdihumboldt.hale.common.scripting.scripts.groovy.GroovyScript; import eu.esdihumboldt.hale.ui.internal.HALEUIPlugin; import eu.esdihumboldt.hale.ui.service.align.AlignmentService; import eu.esdihumboldt.hale.ui.service.align.AlignmentServiceListener; import eu.esdihumboldt.hale.ui.service.project.ProjectService; import eu.esdihumboldt.hale.ui.service.project.ProjectServiceAdapter; import eu.esdihumboldt.util.groovy.sandbox.DefaultGroovyService; import eu.esdihumboldt.util.groovy.sandbox.GroovyRestrictionException; import groovy.lang.Script; /** * Groovy service utilizing preferences to save project restriction exceptions. * * @author Kai Schwierczek */ public class PreferencesGroovyService extends DefaultGroovyService { private static final String PREFERENCE_NAME = "groovy.restriction-exceptions"; private final ProjectService projectService; private final AlignmentService alignmentService; // for caching the value for the current project private volatile boolean restrictionActive = true; private URI restrictionActiveURI = null; /** * scriptHash must not be accessed directly, but through * {@link #getScriptHash()}. */ private String scriptHash = null; private boolean askedForAllowance = false; /** * Constructs the service. * * @param projectService the project service, needed for project URI and * clean information * @param alignmentService the alignment service, needed to get Groovy * scripts from cells */ public PreferencesGroovyService(ProjectService projectService, AlignmentService alignmentService) { this.projectService = projectService; this.alignmentService = alignmentService; projectService.addListener(new ProjectServiceAdapter() { @Override public void onClean() { if (restrictionActive == false) { restrictionActive = true; notifyRestrictionChanged(true); } restrictionActiveURI = null; scriptHash = null; askedForAllowance = false; } @Override public void afterSave(ProjectService projectService) { projectSaved(); } @Override public void afterLoad(ProjectService projectService) { projectLoaded(); } }); alignmentService.addListener(new AlignmentServiceListener() { @Override public void cellsReplaced(Map<? extends Cell, ? extends Cell> cells) { PreferencesGroovyService.this.alignmentChanged(); } @Override public void cellsRemoved(Iterable<Cell> cells) { PreferencesGroovyService.this.alignmentChanged(); } @Override public void cellsPropertyChanged(Iterable<Cell> cells, String propertyName) { PreferencesGroovyService.this.alignmentChanged(); } @Override public void cellsAdded(Iterable<Cell> cells) { PreferencesGroovyService.this.alignmentChanged(); } @Override public void alignmentCleared() { PreferencesGroovyService.this.alignmentChanged(); } @Override public void customFunctionsChanged() { PreferencesGroovyService.this.alignmentChanged(); } @Override public void alignmentChanged() { PreferencesGroovyService.this.alignmentChanged(); } }); } @Override public <T> T evaluate(Script script, ResultProcessor<T> processor) throws Exception { try { return super.evaluate(script, processor); } catch (GroovyRestrictionException e) { if (!askedForAllowance && askForAllowance()) { return super.evaluate(script, processor); } else { throw e; } } } /** * Ask for allowance to run script with full permissions. * * @return true, if allowance was given. */ private synchronized boolean askForAllowance() { // synchronization... if (!askedForAllowance) { final AtomicBoolean disableRestriction = new AtomicBoolean(false); PlatformUI.getWorkbench().getDisplay().syncExec(new Runnable() { @Override public void run() { // check if it was enabled previously String message = "A Groovy script tries using a restricted method, do you want to lift all restrictions?"; if (restrictionActiveURI != null) { String hash = loadPreferences().get(restrictionActiveURI); if (hash != null) { message += "\nA previous version of the current project had these additional rights, but was modified since."; } } message += "\n\nWARNING: The Groovy script can then do \"anything\", so be sure to trust your source!"; boolean result = MessageDialog.openQuestion( Display.getCurrent().getActiveShell(), "Groovy script restriction", message); disableRestriction.set(result); } }); // careful: setRestrictionActive has to stay in this thread (not // Display => deadlock)! if (disableRestriction.get()) { setRestrictionActive(false); } askedForAllowance = true; } return !restrictionActive; } @Override public void setRestrictionActive(final boolean active) { if (restrictionActive != active) { restrictionActive = active; if (restrictionActiveURI != null) { Map<URI, String> preferences = loadPreferences(); if (!restrictionActive) preferences.put(restrictionActiveURI, getScriptHash()); else preferences.remove(restrictionActiveURI); savePreferences(preferences); } // notification must happen asynchronously // (otherwise loading a project may fail) // XXX use another possibility, not the display thread? PlatformUI.getWorkbench().getDisplay().asyncExec(new Runnable() { @Override public void run() { notifyRestrictionChanged(active); } }); } } @Override public boolean isRestrictionActive() { return restrictionActive; } /** * Calculates the current alignments script hash. * * @return the current alignments script hash */ private synchronized String getScriptHash() { if (scriptHash == null) { List<String> scripts = new ArrayList<>(); // get all Groovy scripts for (Cell cell : alignmentService.getAlignment().getCells()) { ListMultimap<String, ParameterValue> parameters = cell .getTransformationParameters(); if (parameters == null) continue; // Groovy transformations if (cell.getTransformationIdentifier().contains("groovy")) { List<ParameterValue> val = parameters.get(GroovyConstants.PARAMETER_SCRIPT); if (!val.isEmpty()) { String script = getScriptString(val.get(0)); if (script != null) { scripts.add(script); } } } // GroovyScript function parameters for (ParameterValue value : parameters.values()) { if (GroovyScript.GROOVY_SCRIPT_ID.equals(value.getType())) { String script = getScriptString(value); if (script != null) { scripts.add(script); } } } } // Groovy scripts of custom property functions for (CustomPropertyFunction customFunction : alignmentService.getAlignment() .getAllCustomPropertyFunctions().values()) { if (customFunction instanceof DefaultCustomPropertyFunction) { DefaultCustomPropertyFunction cf = (DefaultCustomPropertyFunction) customFunction; if (CustomPropertyFunctionType.GROOVY.equals(cf.getFunctionType())) { Value functionDef = cf.getFunctionDefinition(); if (functionDef != null && !functionDef.isEmpty()) { String script = getScriptString(functionDef); if (script != null) { scripts.add(script); } } } } } // order scripts (for consistent hash) Collections.sort(scripts); // compute hash // not simply using hashCode, because it would be far to easy to // modify the script in a undetectable way try { MessageDigest md = MessageDigest.getInstance("MD5"); for (String script : scripts) md.update(script.getBytes("UTF-8")); byte[] hash = md.digest(); StringBuilder sb = new StringBuilder(2 * hash.length); for (byte b : hash) { sb.append(String.format("%02x", b & 0xff)); } scriptHash = sb.toString(); // Both exceptions cannot happen in a valid Java platform. // Anyways, if they happen, execution should stop here! } catch (NoSuchAlgorithmException e) { throw new IllegalStateException("No MD5 MessageDigest!"); } catch (UnsupportedEncodingException e) { throw new IllegalStateException("No UTF-8 Charset!"); } } return scriptHash; } /** * Call when alignment changed. */ private void alignmentChanged() { // Reset script hash, only recompute when necessary! scriptHash = null; } /** * Call when the project was saved. */ private void projectSaved() { // update preferences if restriction disabled if (!restrictionActive) { restrictionActiveURI = projectService.getLoadLocation(); Map<URI, String> preferences = loadPreferences(); preferences.put(restrictionActiveURI, getScriptHash()); savePreferences(preferences); } } /** * Call when a project was loaded */ protected void projectLoaded() { URI location = projectService.getLoadLocation(); if (!Objects.equal(location, restrictionActiveURI)) { restrictionActiveURI = location; if (location == null) { restrictionActive = true; } else { String hash = loadPreferences().get(location); boolean hashChecked = hash != null && getScriptHash().equals(hash); restrictionActive = !hashChecked; // XXX inform user directly if the hash was invalid? if (!restrictionActive) { notifyRestrictionChanged(restrictionActive); } } } } private String getScriptString(Value value) { Text text = value.as(Text.class); if (text != null) { return text.getText(); } return value.as(String.class); } private Map<URI, String> loadPreferences() { String preferenceString = HALEUIPlugin.getDefault().getPreferenceStore() .getString(PREFERENCE_NAME); Map<URI, String> preferences = new HashMap<>(); if (preferenceString.isEmpty()) return preferences; String[] entries = preferenceString.split(" "); for (int i = 0; i < entries.length; i += 2) { preferences.put(URI.create(entries[i]), entries[i + 1]); } return preferences; } private void savePreferences(Map<URI, String> preferences) { StringBuilder sb = new StringBuilder(); for (Entry<URI, String> entry : preferences.entrySet()) { sb.append(entry.getKey().toString()).append(' '); sb.append(entry.getValue()).append(' '); } HALEUIPlugin.getDefault().getPreferenceStore().setValue(PREFERENCE_NAME, sb.toString()); } }