Java tutorial
/* * Zed Attack Proxy (ZAP) and its related class files. * * ZAP is an HTTP/HTTPS proxy for assessing web application security. * * Copyright 2012 ZAP development team * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.zaproxy.zap.control; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.net.MalformedURLException; import java.net.URL; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; import org.apache.commons.configuration.SubnodeConfiguration; import org.apache.commons.lang.ArrayUtils; import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.SystemUtils; import org.apache.log4j.Logger; import org.jgrapht.DirectedGraph; import org.jgrapht.alg.CycleDetector; import org.jgrapht.graph.DefaultDirectedGraph; import org.jgrapht.graph.DefaultEdge; import org.jgrapht.traverse.TopologicalOrderIterator; import org.parosproxy.paros.Constant; import org.parosproxy.paros.core.scanner.AbstractPlugin; import org.parosproxy.paros.extension.Extension; import org.zaproxy.zap.Version; import org.zaproxy.zap.control.BaseZapAddOnXmlData.AddOnDep; import org.zaproxy.zap.control.BaseZapAddOnXmlData.Dependencies; import org.zaproxy.zap.control.BaseZapAddOnXmlData.ExtensionWithDeps; import org.zaproxy.zap.extension.pscan.PluginPassiveScanner; public class AddOn { public enum Status { unknown, example, alpha, beta, weekly, release } private static ZapRelease v2_4 = new ZapRelease("2.4.0"); /** * The installation status of the add-on. * * @since 2.4.0 */ public enum InstallationStatus { /** * The add-on is available for installation, for example, an add-on in the marketplace (even if it requires previous * actions, in this case, download the file). */ AVAILABLE, /** * The add-on was not (yet) installed. For example, the add-on is available in the 'plugin' directory but it's missing a * dependency or requires a greater Java version. It's also in this status while a dependency is being updated. */ NOT_INSTALLED, /** * The add-on is installed. */ INSTALLED, /** * The add-on is being downloaded. */ DOWNLOADING, /** * The uninstallation of the add-on failed. For example, when the add-on is not dynamically installable or when an * {@code Exception} is thrown during the uninstallation. */ UNINSTALLATION_FAILED, /** * The soft uninstallation of the add-on failed. It's in this status when the uninstallation failed for an update of a * dependency. */ SOFT_UNINSTALLATION_FAILED } private String id; private String name; private String description = ""; private String author = ""; private int fileVersion; private Version version; private Status status; private String changes = ""; private File file = null; private URL url = null; private URL info = null; private long size = 0; private boolean hasZapAddOnEntry = false; /** * Flag that indicates if the manifest was read (or attempted to). Allows to prevent reading the manifest a second time when * the add-on file is corrupt. */ private boolean manifestRead; private String notBeforeVersion = null; private String notFromVersion = null; private String hash = null; /** * The installation status of the add-on. * <p> * Default is {@code NOT_INSTALLED}. * * @see InstallationStatus#NOT_INSTALLED */ private InstallationStatus installationStatus = InstallationStatus.NOT_INSTALLED; private List<String> extensions = Collections.emptyList(); private List<ExtensionWithDeps> extensionsWithDeps = Collections.emptyList(); /** * The extensions of the add-on that were loaded. * <p> * This instance variable is lazy initialised. * * @see #addLoadedExtension(Extension) * @see #removeLoadedExtension(Extension) */ private List<Extension> loadedExtensions; private List<String> ascanrules = Collections.emptyList(); private List<AbstractPlugin> loadedAscanrules = Collections.emptyList(); private boolean loadedAscanRulesSet; private List<String> pscanrules = Collections.emptyList(); private List<PluginPassiveScanner> loadedPscanrules = Collections.emptyList(); private boolean loadedPscanRulesSet; private List<String> files = Collections.emptyList(); private AddOnClassnames addOnClassnames = AddOnClassnames.ALL_ALLOWED; private Dependencies dependencies; private static final Logger logger = Logger.getLogger(AddOn.class); public static boolean isAddOn(String fileName) { if (!fileName.toLowerCase().endsWith(".zap")) { return false; } if (fileName.substring(0, fileName.indexOf(".")).split("-").length < 3) { return false; } String[] strArray = fileName.substring(0, fileName.indexOf(".")).split("-"); try { Status.valueOf(strArray[1]); Integer.parseInt(strArray[2]); } catch (Exception e) { return false; } return true; } public static boolean isAddOn(File f) { if (!f.exists()) { return false; } return isAddOn(f.getName()); } public AddOn(String fileName) throws Exception { // Format is <name>-<status>-<version>.zap if (!isAddOn(fileName)) { throw new Exception("Invalid ZAP add-on file " + fileName); } String[] strArray = fileName.substring(0, fileName.indexOf(".")).split("-"); this.id = strArray[0]; this.name = this.id; // Will be overriden if theres a ZapAddOn.xml file this.status = Status.valueOf(strArray[1]); this.fileVersion = Integer.parseInt(strArray[2]); } /** * Constructs an {@code AddOn} from the given {@code file}. * <p> * The {@code ZapAddOn.xml} ZIP file entry is read after validating that the add-on has a valid add-on file name. * <p> * The installation status of the add-on is 'not installed'. * * @param file the file of the add-on * @throws Exception if the given {@code file} does not exist, does not have a valid add-on file name or an error occurred * while reading the {@code ZapAddOn.xml} ZIP file entry */ public AddOn(File file) throws Exception { this(file.getName()); if (!isAddOn(file)) { throw new Exception("Invalid ZAP add-on file " + file.getAbsolutePath()); } this.file = file; loadManifestFile(); } private void loadManifestFile() throws IOException { manifestRead = true; if (file.exists()) { // Might not exist in the tests try (ZipFile zip = new ZipFile(file)) { ZipEntry zapAddOnEntry = zip.getEntry("ZapAddOn.xml"); if (zapAddOnEntry != null) { try (InputStream zis = zip.getInputStream(zapAddOnEntry)) { ZapAddOnXmlFile zapAddOnXml = new ZapAddOnXmlFile(zis); this.name = zapAddOnXml.getName(); this.version = zapAddOnXml.getVersion(); this.description = zapAddOnXml.getDescription(); this.changes = zapAddOnXml.getChanges(); this.author = zapAddOnXml.getAuthor(); this.notBeforeVersion = zapAddOnXml.getNotBeforeVersion(); this.notFromVersion = zapAddOnXml.getNotFromVersion(); this.dependencies = zapAddOnXml.getDependencies(); this.ascanrules = zapAddOnXml.getAscanrules(); this.extensions = zapAddOnXml.getExtensions(); this.extensionsWithDeps = zapAddOnXml.getExtensionsWithDeps(); this.files = zapAddOnXml.getFiles(); this.pscanrules = zapAddOnXml.getPscanrules(); this.addOnClassnames = zapAddOnXml.getAddOnClassnames(); hasZapAddOnEntry = true; } } } } } /** * Constructs an {@code AddOn} from an add-on entry of {@code ZapVersions.xml} file. The installation status of the add-on * is 'not installed'. * <p> * The given {@code SubnodeConfiguration} must have a {@code XPathExpressionEngine} installed. * <p> * The {@code ZapAddOn.xml} ZIP file entry is read, if the add-on file exists locally. * * @param id the id of the add-on * @param baseDir the base directory where the add-on is located * @param xmlData the source of add-on entry of {@code ZapVersions.xml} file * @throws MalformedURLException if the {@code URL} of the add-on is malformed * @throws IOException if an error occurs while reading the XML data * @see org.apache.commons.configuration.tree.xpath.XPathExpressionEngine */ public AddOn(String id, File baseDir, SubnodeConfiguration xmlData) throws MalformedURLException, IOException { this.id = id; ZapVersionsAddOnEntry addOnData = new ZapVersionsAddOnEntry(xmlData); this.name = addOnData.getName(); this.description = addOnData.getDescription(); this.author = addOnData.getAuthor(); this.fileVersion = addOnData.getPackageVersion(); this.dependencies = addOnData.getDependencies(); this.extensionsWithDeps = addOnData.getExtensionsWithDeps(); this.version = addOnData.getVersion(); this.status = AddOn.Status.valueOf(addOnData.getStatus()); this.changes = addOnData.getChanges(); this.url = new URL(addOnData.getUrl()); this.file = new File(baseDir, addOnData.getFile()); this.size = addOnData.getSize(); this.notBeforeVersion = addOnData.getNotBeforeVersion(); this.notFromVersion = addOnData.getNotFromVersion(); if (addOnData.getInfo() != null && !addOnData.getInfo().isEmpty()) { try { this.info = new URL(addOnData.getInfo()); } catch (Exception ignore) { if (logger.isDebugEnabled()) { logger.debug("Wrong info URL for add-on \"" + name + "\":", ignore); } } } this.hash = addOnData.getHash(); loadManifestFile(); } public String getId() { return id; } public void setId(String id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getDescription() { return description; } public void setDescription(String description) { this.description = description; } public int getFileVersion() { return fileVersion; } /** * Gets the semantic version of this add-on. * * @return the semantic version of the add-on, or {@code null} if none * @since 2.4.0 */ public Version getVersion() { return version; } public Status getStatus() { return status; } public void setStatus(Status status) { this.status = status; } public String getChanges() { return changes; } public void setChanges(String changes) { this.changes = changes; } public File getFile() { return file; } public void setFile(File file) { this.file = file; } public URL getUrl() { return url; } public void setUrl(URL url) { this.url = url; } public long getSize() { return size; } public void setSize(long size) { this.size = size; } public String getAuthor() { return author; } public void setAuthor(String author) { this.author = author; } /** * Sets the installation status of the add-on. * * @param installationStatus the new installation status * @throws IllegalArgumentException if the given {@code installationStatus} is {@code null}. * @since 2.4.0 */ public void setInstallationStatus(InstallationStatus installationStatus) { if (installationStatus == null) { throw new IllegalArgumentException("Parameter installationStatus must not be null."); } this.installationStatus = installationStatus; } /** * Gets installations status of the add-on. * * @return the installation status, never {@code null} * @since 2.4.0 */ public InstallationStatus getInstallationStatus() { return installationStatus; } public boolean hasZapAddOnEntry() { if (!hasZapAddOnEntry) { if (!manifestRead) { // Worth trying, as it depends which constructor has been used try { this.loadManifestFile(); } catch (IOException e) { if (logger.isDebugEnabled()) { logger.debug("Failed to read the ZapAddOn.xml file of " + id + ":", e); } } } } return hasZapAddOnEntry; } /** * Gets the classnames that can be loaded for the add-on. * * @return the classnames that can be loaded * @since 2.4.3 */ public AddOnClassnames getAddOnClassnames() { return addOnClassnames; } public List<String> getExtensions() { return extensions; } /** * Returns the classnames of {@code Extension}sthat have dependencies on add-ons. * * @return the classnames of the extensions with dependencies on add-ons. * @see #hasExtensionsWithDeps() */ public List<String> getExtensionsWithDeps() { if (extensionsWithDeps.isEmpty()) { return Collections.emptyList(); } List<String> extensionClassnames = new ArrayList<>(extensionsWithDeps.size()); for (ExtensionWithDeps extension : extensionsWithDeps) { extensionClassnames.add(extension.getClassname()); } return extensionClassnames; } /** * Returns the classnames that can be loaded for the given {@code Extension} (with dependencies). * * @param classname the classname of the extension * @return the classnames that can be loaded * @since 2.4.3 * @see #hasExtensionsWithDeps() */ public AddOnClassnames getExtensionAddOnClassnames(String classname) { if (extensionsWithDeps.isEmpty() || classname == null || classname.isEmpty()) { return AddOnClassnames.ALL_ALLOWED; } for (ExtensionWithDeps extension : extensionsWithDeps) { if (classname.equals(extension.getClassname())) { return extension.getAddOnClassnames(); } } return AddOnClassnames.ALL_ALLOWED; } /** * Tells whether or not this add-on has at least one extension with dependencies. * * @return {@code true} if the add-on has at leas one extension with dependencies, {@code false} otherwise * @see #getExtensionsWithDeps() */ public boolean hasExtensionsWithDeps() { return !extensionsWithDeps.isEmpty(); } /** * Gets the extensions of this add-on that have dependencies and were loaded. * * @return an unmodifiable {@code List} with the extensions of this add-on that have dependencies and were loaded * @since 2.4.0 */ public List<Extension> getLoadedExtensionsWithDeps() { List<String> classnames = getExtensionsWithDeps(); ArrayList<Extension> loadedExtensions = new ArrayList<>(extensionsWithDeps.size()); for (Extension extension : getLoadedExtensions()) { if (classnames.contains(extension.getClass().getCanonicalName())) { loadedExtensions.add(extension); } } loadedExtensions.trimToSize(); return loadedExtensions; } /** * Gets the extensions of this add-on that were loaded. * * @return an unmodifiable {@code List} with the extensions of this add-on that were loaded * @since 2.4.0 */ public List<Extension> getLoadedExtensions() { if (loadedExtensions == null) { return Collections.emptyList(); } return Collections.unmodifiableList(loadedExtensions); } /** * Adds the given {@code extension} to the list of loaded extensions of this add-on. * <p> * This add-on is set to the given {@code extension}. * * @param extension the extension of this add-on that was loaded * @throws IllegalArgumentException if extension is {@code null} * @since 2.4.0 * @see #removeLoadedExtension(Extension) * @see Extension#setAddOn(AddOn) */ public void addLoadedExtension(Extension extension) { if (extension == null) { throw new IllegalArgumentException("Parameter extension must not be null."); } if (loadedExtensions == null) { loadedExtensions = new ArrayList<>(1); } if (!loadedExtensions.contains(extension)) { loadedExtensions.add(extension); extension.setAddOn(this); } } /** * Removes the given {@code extension} from the list of loaded extensions of this add-on. * <p> * The add-on of the given {@code extension} is set to {@code null}. * <p> * The call to this method has no effect if the given {@code extension} does not belong to this add-on. * * @param extension the loaded extension of this add-on that should be removed * @throws IllegalArgumentException if extension is {@code null} * @since 2.4.0 * @see #addLoadedExtension(Extension) * @see Extension#setAddOn(AddOn) */ public void removeLoadedExtension(Extension extension) { if (extension == null) { throw new IllegalArgumentException("Parameter extension must not be null."); } if (loadedExtensions != null && loadedExtensions.contains(extension)) { loadedExtensions.remove(extension); extension.setAddOn(null); } } public List<String> getAscanrules() { return ascanrules; } /** * Gets the active scan rules of this add-on that were loaded. * * @return an unmodifiable {@code List} with the active scan rules of this add-on that were loaded, never {@code null} * @since 2.4.3 * @see #setLoadedAscanrules(List) */ public List<AbstractPlugin> getLoadedAscanrules() { return loadedAscanrules; } /** * Sets the loaded active scan rules of the add-on, allowing to set the status of the active scan rules appropriately and to * keep track of the active scan rules loaded so that they can be removed during uninstallation. * <p> * <strong>Note:</strong> Helper method to be used (only) by/during (un)installation process and loading of the add-on. * Should be called when installing/loading the add-on, by setting the loaded active scan rules, and when uninstalling by * setting an empty list. The method {@code setLoadedAscanrulesSet(boolean)} should also be called. * * @param ascanrules the active scan rules loaded, might be empty if none were actually loaded * @throws IllegalArgumentException if {@code ascanrules} is {@code null}. * @since 2.4.3 * @see #setLoadedAscanrulesSet(boolean) * @see AbstractPlugin#setStatus(Status) */ void setLoadedAscanrules(List<AbstractPlugin> ascanrules) { if (ascanrules == null) { throw new IllegalArgumentException("Parameter ascanrules must not be null."); } if (ascanrules.isEmpty()) { loadedAscanrules = Collections.emptyList(); return; } for (AbstractPlugin ascanrule : ascanrules) { ascanrule.setStatus(getStatus()); } loadedAscanrules = Collections.unmodifiableList(new ArrayList<>(ascanrules)); } /** * Tells whether or not the loaded active scan rules of the add-on, if any, were already set to the add-on. * <p> * <strong>Note:</strong> Helper method to be used (only) by/during (un)installation process and loading of the add-on. * * @return {@code true} if the loaded active scan rules were already set, {@code false} otherwise * @since 2.4.3 * @see #setLoadedAscanrules(List) * @see #setLoadedAscanrulesSet(boolean) */ boolean isLoadedAscanrulesSet() { return loadedAscanRulesSet; } /** * Sets whether or not the loaded active scan rules, if any, where already set to the add-on. * <p> * <strong>Note:</strong> Helper method to be used (only) by/during (un)installation process and loading of the add-on. The * method should be called, with {@code true} during installation/loading and {@code false} during uninstallation, after * calling the method {@code setLoadedAscanrules(List)}. * * @param ascanrulesSet {@code true} if the loaded active scan rules were already set, {@code false} otherwise * @since 2.4.3 * @see #setLoadedAscanrules(List) */ void setLoadedAscanrulesSet(boolean ascanrulesSet) { loadedAscanRulesSet = ascanrulesSet; } public List<String> getPscanrules() { return pscanrules; } /** * Gets the passive scan rules of this add-on that were loaded. * * @return an unmodifiable {@code List} with the passive scan rules of this add-on that were loaded, never {@code null} * @since 2.4.3 * @see #setLoadedPscanrules(List) */ public List<PluginPassiveScanner> getLoadedPscanrules() { return loadedPscanrules; } /** * Sets the loaded passive scan rules of the add-on, allowing to set the status of the passive scan rules appropriately and * keep track of the passive scan rules loaded so that they can be removed during uninstallation. * <p> * <strong>Note:</strong> Helper method to be used (only) by/during (un)installation process and loading of the add-on. * Should be called when installing/loading the add-on, by setting the loaded passive scan rules, and when uninstalling by * setting an empty list. The method {@code setLoadedPscanrulesSet(boolean)} should also be called. * * @param pscanrules the passive scan rules loaded, might be empty if none were actually loaded * @throws IllegalArgumentException if {@code pscanrules} is {@code null}. * @since 2.4.3 * @see #setLoadedPscanrulesSet(boolean) * @see PluginPassiveScanner#setStatus(Status) */ void setLoadedPscanrules(List<PluginPassiveScanner> pscanrules) { if (pscanrules == null) { throw new IllegalArgumentException("Parameter pscanrules must not be null."); } if (pscanrules.isEmpty()) { loadedPscanrules = Collections.emptyList(); return; } for (PluginPassiveScanner pscanrule : pscanrules) { pscanrule.setStatus(getStatus()); } loadedPscanrules = Collections.unmodifiableList(new ArrayList<>(pscanrules)); } /** * Tells whether or not the loaded passive scan rules of the add-on, if any, were already set to the add-on. * <p> * <strong>Note:</strong> Helper method to be used (only) by/during (un)installation process and loading of the add-on. * * @return {@code true} if the loaded passive scan rules were already set, {@code false} otherwise * @since 2.4.3 * @see #setLoadedPscanrules(List) * @see #setLoadedPscanrulesSet(boolean) */ boolean isLoadedPscanrulesSet() { return loadedPscanRulesSet; } /** * Sets whether or not the loaded passive scan rules, if any, where already set to the add-on. * <p> * <strong>Note:</strong> Helper method to be used (only) by/during (un)installation process and loading of the add-on. The * method should be called, with {@code true} during installation/loading and {@code false} during uninstallation, after * calling the method {@code setLoadedPscanrules(List)}. * * @param pscanrulesSet {@code true} if the loaded passive scan rules were already set, {@code false} otherwise * @since 2.4.3 * @see #setLoadedPscanrules(List) */ void setLoadedPscanrulesSet(boolean pscanrulesSet) { loadedPscanRulesSet = pscanrulesSet; } public List<String> getFiles() { return files; } public boolean isSameAddOn(AddOn addOn) { return this.getId().equals(addOn.getId()); } public boolean isUpdateTo(AddOn addOn) throws IllegalArgumentException { if (!this.isSameAddOn(addOn)) { throw new IllegalArgumentException("Different addons: " + this.getId() + " != " + addOn.getId()); } if (this.getFileVersion() > addOn.getFileVersion()) { return true; } return this.getStatus().ordinal() > addOn.getStatus().ordinal(); } /** * @deprecated (2.4.0) Use {@link #calculateRunRequirements(Collection)} instead. Returns {@code false}. */ @Deprecated @SuppressWarnings("javadoc") public boolean canLoad() { return false; } /** * Tells whether or not this add-on can be loaded in the currently running ZAP version, as given by * {@code Constant.PROGRAM_VERSION}. * * @return {@code true} if the add-on can be loaded in the currently running ZAP version, {@code false} otherwise * @see #canLoadInVersion(String) * @see Constant#PROGRAM_VERSION */ public boolean canLoadInCurrentVersion() { return canLoadInVersion(Constant.PROGRAM_VERSION); } /** * Tells whether or not this add-on can be run in the currently running Java version. * <p> * This is a convenience method that calls {@code canRunInJavaVersion(String)} with the running Java version (as given by * {@code SystemUtils.JAVA_VERSION}) as parameter. * * @return {@code true} if the add-on can be run in the currently running Java version, {@code false} otherwise * @since 2.4.0 * @see #canRunInJavaVersion(String) * @see SystemUtils#JAVA_VERSION */ public boolean canRunInCurrentJavaVersion() { return canRunInJavaVersion(SystemUtils.JAVA_VERSION); } /** * Tells whether or not this add-on can be run in the given {@code javaVersion}. * <p> * If the given {@code javaVersion} is {@code null} and this add-on depends on a specific java version the method returns * {@code false}. * * @param javaVersion the java version that will be checked * @return {@code true} if the add-on can be loaded in the given {@code javaVersion}, {@code false} otherwise * @since 2.4.0 */ public boolean canRunInJavaVersion(String javaVersion) { if (dependencies == null) { return true; } String requiredVersion = dependencies.getJavaVersion(); if (requiredVersion == null) { return true; } if (javaVersion == null) { return false; } return getJavaVersion(javaVersion) >= getJavaVersion(requiredVersion); } /** * Calculates the requirements to run this add-on, in the current ZAP and Java versions and with the given * {@code availableAddOns}. * <p> * If the add-on depends on other add-ons, those add-ons are also checked if are also runnable. * <p> * <strong>Note:</strong> All the given {@code availableAddOns} are expected to be loadable in the currently running ZAP * version, that is, the method {@code AddOn.canLoadInCurrentVersion()}, returns {@code true}. * * @param availableAddOns the other add-ons available * @return a requirements to run the add-on, and if not runnable the reason why it's not. * @since 2.4.0 * @see #canLoadInCurrentVersion() * @see #canRunInCurrentJavaVersion() * @see AddOnRunRequirements */ public AddOnRunRequirements calculateRunRequirements(Collection<AddOn> availableAddOns) { AddOnRunRequirements requirements = new AddOnRunRequirements(this); calculateRunRequirementsImpl(availableAddOns, requirements, null, this); if (requirements.isRunnable()) { checkExtensionsWithDeps(availableAddOns, requirements, this); } return requirements; } private static void calculateRunRequirementsImpl(Collection<AddOn> availableAddOns, BaseRunRequirements requirements, AddOn parent, AddOn addOn) { AddOn installedVersion = getAddOn(availableAddOns, addOn.getId()); if (installedVersion != null && !addOn.equals(installedVersion)) { requirements.setIssue(BaseRunRequirements.DependencyIssue.OLDER_VERSION, installedVersion); if (logger.isDebugEnabled()) { logger.debug("Add-on " + addOn + " not runnable, old version still installed: " + installedVersion); } return; } if (!requirements.addDependency(parent, addOn)) { logger.warn("Cyclic dependency detected with: " + requirements.getDependencies()); requirements.setIssue(BaseRunRequirements.DependencyIssue.CYCLIC, requirements.getDependencies()); return; } if (addOn.dependencies == null) { return; } if (!addOn.canRunInCurrentJavaVersion()) { requirements.setMinimumJavaVersionIssue(addOn, addOn.dependencies.getJavaVersion()); } for (AddOnDep dependency : addOn.dependencies.getAddOns()) { String addOnId = dependency.getId(); if (addOnId != null) { AddOn addOnDep = getAddOn(availableAddOns, addOnId); if (addOnDep == null) { requirements.setIssue(BaseRunRequirements.DependencyIssue.MISSING, addOnId); return; } if (dependency.getNotBeforeVersion() > -1 && addOnDep.fileVersion < dependency.getNotBeforeVersion()) { requirements.setIssue(BaseRunRequirements.DependencyIssue.PACKAGE_VERSION_NOT_BEFORE, addOnDep, Integer.valueOf(dependency.getNotBeforeVersion())); return; } if (dependency.getNotFromVersion() > -1 && addOnDep.fileVersion > dependency.getNotFromVersion()) { requirements.setIssue(BaseRunRequirements.DependencyIssue.PACKAGE_VERSION_NOT_FROM, addOnDep, Integer.valueOf(dependency.getNotFromVersion())); return; } if (!dependency.getSemVer().isEmpty()) { if (addOnDep.version == null || !addOnDep.version.matches(dependency.getSemVer())) { requirements.setIssue(BaseRunRequirements.DependencyIssue.VERSION, addOnDep, dependency.getSemVer()); return; } } calculateRunRequirementsImpl(availableAddOns, requirements, addOn, addOnDep); if (requirements.hasDependencyIssue()) { return; } } } } private static void checkExtensionsWithDeps(Collection<AddOn> availableAddOns, AddOnRunRequirements requirements, AddOn addOn) { if (addOn.extensionsWithDeps.isEmpty()) { return; } for (ExtensionWithDeps extension : addOn.extensionsWithDeps) { calculateExtensionRunRequirements(extension, availableAddOns, requirements, addOn); } } private static void calculateExtensionRunRequirements(ExtensionWithDeps extension, Collection<AddOn> availableAddOns, AddOnRunRequirements requirements, AddOn addOn) { ExtensionRunRequirements extensionRequirements = new ExtensionRunRequirements(addOn, extension.getClassname()); requirements.addExtensionRequirements(extensionRequirements); for (AddOnDep dependency : extension.getDependencies()) { String addOnId = dependency.getId(); if (addOnId == null) { continue; } AddOn addOnDep = getAddOn(availableAddOns, addOnId); if (addOnDep == null) { if (addOn.hasOnlyOneExtensionWithDependencies()) { requirements.setIssue(BaseRunRequirements.DependencyIssue.MISSING, addOnId); return; } extensionRequirements.setIssue(BaseRunRequirements.DependencyIssue.MISSING, addOnId); continue; } if (dependency.getNotBeforeVersion() > -1 && addOnDep.fileVersion < dependency.getNotBeforeVersion()) { if (addOn.hasOnlyOneExtensionWithDependencies()) { requirements.setIssue(BaseRunRequirements.DependencyIssue.PACKAGE_VERSION_NOT_BEFORE, addOnDep, Integer.valueOf(dependency.getNotBeforeVersion())); return; } extensionRequirements.setIssue(BaseRunRequirements.DependencyIssue.PACKAGE_VERSION_NOT_BEFORE, addOnDep, Integer.valueOf(dependency.getNotBeforeVersion())); continue; } if (dependency.getNotFromVersion() > -1 && addOnDep.fileVersion > dependency.getNotFromVersion()) { if (addOn.hasOnlyOneExtensionWithDependencies()) { requirements.setIssue(BaseRunRequirements.DependencyIssue.PACKAGE_VERSION_NOT_FROM, addOnDep, Integer.valueOf(dependency.getNotFromVersion())); return; } extensionRequirements.setIssue(BaseRunRequirements.DependencyIssue.PACKAGE_VERSION_NOT_FROM, addOnDep, Integer.valueOf(dependency.getNotFromVersion())); continue; } if (!dependency.getSemVer().isEmpty()) { if (addOnDep.version == null || !addOnDep.version.matches(dependency.getSemVer())) { if (addOn.hasOnlyOneExtensionWithDependencies()) { requirements.setIssue(BaseRunRequirements.DependencyIssue.VERSION, addOnDep, dependency.getSemVer()); return; } extensionRequirements.setIssue(BaseRunRequirements.DependencyIssue.VERSION, addOnDep, dependency.getSemVer()); continue; } } calculateRunRequirementsImpl(availableAddOns, extensionRequirements, addOn, addOnDep); } } private boolean hasOnlyOneExtensionWithDependencies() { if (extensionsWithDeps.size() != 1) { return false; } if (extensions.isEmpty() && files.isEmpty() && pscanrules.isEmpty() && ascanrules.isEmpty()) { return true; } return false; } /** * Calculates the requirements to run the given {@code extension}, in the current ZAP and Java versions and with the given * {@code availableAddOns}. * <p> * If the extension depends on other add-ons, those add-ons are checked if are also runnable. * <p> * <strong>Note:</strong> All the given {@code availableAddOns} are expected to be loadable in the currently running ZAP * version, that is, the method {@code AddOn.canLoadInCurrentVersion()}, returns {@code true}. * * @param extension the extension that will be checked * @param availableAddOns the add-ons available * @return the requirements to run the extension, and if not runnable the reason why it's not. * @since 2.4.0 * @see AddOnRunRequirements#getExtensionRequirements() */ public AddOnRunRequirements calculateExtensionRunRequirements(Extension extension, Collection<AddOn> availableAddOns) { return calculateExtensionRunRequirements(extension.getClass().getCanonicalName(), availableAddOns); } /** * Calculates the requirements to run the extension with the given {@code classname}, in the current ZAP and Java versions * and with the given {@code availableAddOns}. * <p> * If the extension depends on other add-ons, those add-ons are checked if are also runnable. * <p> * <strong>Note:</strong> All the given {@code availableAddOns} are expected to be loadable in the currently running ZAP * version, that is, the method {@code AddOn.canLoadInCurrentVersion()}, returns {@code true}. * * @param classname the classname of extension that will be checked * @param availableAddOns the add-ons available * @return the requirements to run the extension, and if not runnable the reason why it's not. * @since 2.4.0 * @see AddOnRunRequirements#getExtensionRequirements() */ public AddOnRunRequirements calculateExtensionRunRequirements(String classname, Collection<AddOn> availableAddOns) { AddOnRunRequirements requirements = new AddOnRunRequirements(this); for (ExtensionWithDeps extensionWithDeps : extensionsWithDeps) { if (extensionWithDeps.getClassname().equals(classname)) { calculateExtensionRunRequirements(extensionWithDeps, availableAddOns, requirements, this); break; } } return requirements; } /** * Tells whether or not the given {@code extension} has a (direct) dependency on the given {@code addOn} (including * version). * * @param extension the extension that will be checked * @param addOn the add-on that will be checked in the dependencies on the extension * @return {@code true} if the extension depends on the given add-on, {@code false} otherwise. * @since 2.4.0 */ public boolean dependsOn(Extension extension, AddOn addOn) { String classname = extension.getClass().getCanonicalName(); for (ExtensionWithDeps extensionWithDeps : extensionsWithDeps) { if (extensionWithDeps.getClassname().equals(classname)) { return dependsOn(extensionWithDeps.getDependencies(), addOn); } } return false; } private static boolean dependsOn(List<AddOnDep> dependencies, AddOn addOn) { for (AddOnDep dependency : dependencies) { if (dependency.getId().equals(addOn.id)) { if (dependency.getNotBeforeVersion() > -1 && addOn.fileVersion < dependency.getNotBeforeVersion()) { return false; } if (dependency.getNotFromVersion() > -1 && addOn.fileVersion > dependency.getNotFromVersion()) { return false; } if (!dependency.getSemVer().isEmpty()) { if (addOn.version == null) { return false; } else if (!addOn.version.matches(dependency.getSemVer())) { return false; } } return true; } } return false; } /** * Tells whether or not the extension with the given {@code classname} is loaded. * * @param classname the classname of the extension * @return {@code true} if the extension is loaded, {@code false} otherwise * @since 2.4.0 */ public boolean isExtensionLoaded(String classname) { List<Extension> allExtensions = new ArrayList<>( getLoadedExtensions().size() + getLoadedExtensionsWithDeps().size()); allExtensions.addAll(getLoadedExtensions()); allExtensions.addAll(getLoadedExtensionsWithDeps()); for (Extension extension : getLoadedExtensions()) { if (classname.equals(extension.getClass().getCanonicalName())) { return true; } } return false; } /** * Returns the minimum Java version required to run this add-on or an empty {@code String} if there's no minimum version. * * @return the minimum Java version required to run this add-on or an empty {@code String} if no minimum version * @since 2.4.0 */ public String getMinimumJavaVersion() { if (dependencies == null) { return ""; } return dependencies.getJavaVersion(); } /** * Gets the add-on with the given {@code id} from the given collection of {@code addOns}. * * @param addOns the collection of add-ons where the search will be made * @param id the id of the add-on to search for * @return the {@code AddOn} with the given id, or {@code null} if not found */ private static AddOn getAddOn(Collection<AddOn> addOns, String id) { for (AddOn addOn : addOns) { if (addOn.getId().equals(id)) { return addOn; } } return null; } /** * Tells whether or not this add-on can be loaded in the given {@code zapVersion}. * * @param zapVersion the ZAP version that will be checked * @return {@code true} if the add-on can be loaded in the given {@code zapVersion}, {@code false} otherwise */ public boolean canLoadInVersion(String zapVersion) { // Require add-ons to declare the version they implement if (this.notBeforeVersion == null || this.notBeforeVersion.isEmpty()) { return false; } ZapReleaseComparitor zrc = new ZapReleaseComparitor(); ZapRelease zr = new ZapRelease(zapVersion); ZapRelease notBeforeRelease = new ZapRelease(this.notBeforeVersion); if (zrc.compare(zr, notBeforeRelease) < 0) { return false; } if (zrc.compare(notBeforeRelease, v2_4) < 0) { // Dont load any add-ons that imply they are prior to 2.4.0 - they probably wont work return false; } if (this.notFromVersion != null && this.notFromVersion.length() > 0) { ZapRelease notFromRelease = new ZapRelease(this.notFromVersion); return (zrc.compare(zr, notFromRelease) < 0); } return true; } public void setNotBeforeVersion(String notBeforeVersion) { this.notBeforeVersion = notBeforeVersion; } public void setNotFromVersion(String notFromVersion) { this.notFromVersion = notFromVersion; } public String getNotBeforeVersion() { return notBeforeVersion; } public String getNotFromVersion() { return notFromVersion; } public URL getInfo() { return info; } public void setInfo(URL info) { this.info = info; } public String getHash() { return hash; } /** * Returns the IDs of the add-ons dependencies, an empty collection if none. * * @return the IDs of the dependencies. * @since 2.4.0 */ public List<String> getIdsAddOnDependencies() { if (dependencies == null) { return Collections.emptyList(); } List<String> ids = new ArrayList<>(dependencies.getAddOns().size()); for (AddOnDep dep : dependencies.getAddOns()) { ids.add(dep.getId()); } return ids; } /** * Tells whether or not this add-on has a (direct) dependency on the given {@code addOn} (including version). * * @param addOn the add-on that will be checked * @return {@code true} if it depends on the given add-on, {@code false} otherwise. * @since 2.4.0 */ public boolean dependsOn(AddOn addOn) { if (dependencies == null || dependencies.getAddOns().isEmpty()) { return false; } return dependsOn(dependencies.getAddOns(), addOn); } /** * Tells whether or not this add-on has a (direct) dependency on any of the given {@code addOns} (including version). * * @param addOns the add-ons that will be checked * @return {@code true} if it depends on any of the given add-ons, {@code false} otherwise. * @since 2.4.0 */ public boolean dependsOn(Collection<AddOn> addOns) { if (dependencies == null || dependencies.getAddOns().isEmpty()) { return false; } for (AddOn addOn : addOns) { if (dependsOn(addOn)) { return true; } } return false; } @Override public String toString() { StringBuilder strBuilder = new StringBuilder(); strBuilder.append("[id=").append(id); strBuilder.append(", fileVersion=").append(fileVersion); if (version != null) { strBuilder.append(", version=").append(version); } strBuilder.append(']'); return strBuilder.toString(); } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((id == null) ? 0 : id.hashCode()); result = prime * result + fileVersion; result = prime * result + ((version == null) ? 0 : version.hashCode()); return result; } /** * Two add-ons are considered equal if both add-ons have the same ID, file version and semantic version. */ @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (obj == null) { return false; } if (getClass() != obj.getClass()) { return false; } AddOn other = (AddOn) obj; if (id == null) { if (other.id != null) { return false; } } else if (!id.equals(other.id)) { return false; } if (fileVersion != other.fileVersion) { return false; } if (version == null) { if (other.version != null) { return false; } } else if (!version.equals(other.version)) { return false; } return true; } public static abstract class BaseRunRequirements { /** * The reason why an add-on can not be run because of a dependency. * <p> * More details of the issue can be obtained with the method {@code RunRequirements.getDependencyIssueDetails()}. The * exact contents are mentioned in each {@code DependencyIssue} constant. * * @see AddOnRunRequirements#getDependencyIssueDetails() */ public enum DependencyIssue { /** * A cyclic dependency was detected. * <p> * Issue details contain all the add-ons in the cyclic chain. */ CYCLIC, /** * Older version of the add-on is still installed. * <p> * Issue details contain the old version. */ OLDER_VERSION, /** * A dependency was not found. * <p> * Issue details contain the id of the add-on. */ MISSING, /** * The dependency found has a older version than the version required. * <p> * Issue details contain the instance of the {@code AddOn} and the required version. */ PACKAGE_VERSION_NOT_BEFORE, /** * The dependency found has a newer version than the version required. * <p> * Issue details contain the instance of the {@code AddOn} and the required version. */ PACKAGE_VERSION_NOT_FROM, /** * The dependency found has a different semantic version. * <p> * Issue details contain the instance of the {@code AddOn} and the required version. */ VERSION } private final AddOn addOn; private final DirectedGraph<AddOn, DefaultEdge> dependencyTree; private Set<AddOn> dependencies; private DependencyIssue depIssue; private List<Object> issueDetails; private String minimumJavaVersion; private AddOn addOnMinimumJavaVersion; private boolean runnable; private BaseRunRequirements(AddOn addOn) { this.addOn = addOn; dependencyTree = new DefaultDirectedGraph<>(DefaultEdge.class); dependencyTree.addVertex(addOn); runnable = true; issueDetails = Collections.emptyList(); } /** * Gets the add-on that was tested to check if it can be run. * * @return the tested add-on */ public AddOn getAddOn() { return addOn; } /** * Tells whether or not this add-on has a dependency issue. * * @return {@code true} if the add-on has a dependency issue, {@code false} otherwise * @see #getDependencyIssue() * @see #getDependencyIssueDetails() * @see DependencyIssue */ public boolean hasDependencyIssue() { return (depIssue != null); } /** * Gets the dependency issue, if any. * * @return the {@code DependencyIssue} or {@code null}, if none * @see #hasDependencyIssue() * @see #getDependencyIssueDetails() * @see DependencyIssue */ public DependencyIssue getDependencyIssue() { return depIssue; } /** * Gets the details of the dependency issue, if any. * * @return a list containing the details of the issue or an empty list if none * @see #hasDependencyIssue() * @see #getDependencyIssue() * @see DependencyIssue */ public List<Object> getDependencyIssueDetails() { return issueDetails; } /** * Tells whether or not this add-on can be run. * * @return {@code true} if the add-on can be run, {@code false} otherwise */ public boolean isRunnable() { return runnable; } protected void setRunnable(boolean runnable) { this.runnable = runnable; } /** * Gets the (found) dependencies of the add-on, including transitive dependencies. * * @return a set containing the dependencies of the add-on * @see AddOn#getIdsAddOnDependencies() */ public Set<AddOn> getDependencies() { if (dependencies == null) { dependencies = new HashSet<>(); for (TopologicalOrderIterator<AddOn, DefaultEdge> it = new TopologicalOrderIterator<>( dependencyTree); it.hasNext();) { dependencies.add(it.next()); } dependencies.remove(addOn); } return Collections.unmodifiableSet(dependencies); } protected void setIssue(DependencyIssue issue, Object... details) { runnable = false; this.depIssue = issue; if (details != null) { issueDetails = Arrays.asList(details); } else { issueDetails = Collections.emptyList(); } } protected boolean addDependency(AddOn parent, AddOn addOn) { if (parent == null) { return true; } dependencyTree.addVertex(parent); dependencyTree.addVertex(addOn); dependencyTree.addEdge(parent, addOn); CycleDetector<AddOn, DefaultEdge> cycleDetector = new CycleDetector<>(dependencyTree); boolean cycle = cycleDetector.detectCycles(); if (cycle) { dependencies = cycleDetector.findCycles(); return false; } return true; } /** * Tells whether or not this add-on requires a newer Java version to run. * <p> * The requirement might be imposed by a dependency or the add-on itself. To check which one use the methods * {@code getAddOn()} and {@code getAddOnMinimumJavaVersion()}. * * @return {@code true} if the add-on requires a newer Java version, {@code false} otherwise. * @see #getAddOn() * @see #getAddOnMinimumJavaVersion() * @see #getMinimumJavaVersion() */ public boolean isNewerJavaVersionRequired() { return (minimumJavaVersion != null); } /** * Gets the minimum Java version required to run the add-on. * * @return the Java version, or {@code null} if no minimum. * @see #isNewerJavaVersionRequired() * @see #getAddOn() * @see #getAddOnMinimumJavaVersion() */ public String getMinimumJavaVersion() { return minimumJavaVersion; } /** * Gets the add-on that requires the minimum Java version. * * @return the add-on, or {@code null} if no minimum. * @see #isNewerJavaVersionRequired() * @see #getMinimumJavaVersion() * @see #getAddOn() */ public AddOn getAddOnMinimumJavaVersion() { return addOnMinimumJavaVersion; } protected void setMinimumJavaVersionIssue(AddOn srcAddOn, String requiredVersion) { setRunnable(false); if (minimumJavaVersion == null) { setMinimumJavaVersion(srcAddOn, requiredVersion); } else if (getJavaVersion(requiredVersion) > getJavaVersion(minimumJavaVersion)) { setMinimumJavaVersion(srcAddOn, requiredVersion); } } private void setMinimumJavaVersion(AddOn srcAddOn, String requiredVersion) { addOnMinimumJavaVersion = srcAddOn; minimumJavaVersion = requiredVersion; } } /** * The requirements to run an {@code AddOn}. * <p> * It can be used to check if an add-on can or not be run, which requirements it has (for example, minimum Java version or * dependency add-ons) and which issues prevent it from being run, if any. * * @since 2.4.0 */ public static class AddOnRunRequirements extends BaseRunRequirements { private List<ExtensionRunRequirements> addExtensionsRequirements; private AddOnRunRequirements(AddOn addOn) { super(addOn); } /** * Gets the run requirements of the extensions that have dependencies. * * @return a {@code List} containing the requirements of each extension that have dependencies * @see #hasExtensionsWithRunningIssues() */ public List<ExtensionRunRequirements> getExtensionRequirements() { if (addExtensionsRequirements == null) { addExtensionsRequirements = Collections.emptyList(); } return addExtensionsRequirements; } /** * Tells whether or not there's at least one extension with running issues. * * @return {@code true} if at least one extension has running issues, {@code false} otherwise. * @see #getExtensionRequirements() */ public boolean hasExtensionsWithRunningIssues() { for (ExtensionRunRequirements reqs : getExtensionRequirements()) { if (!reqs.isRunnable()) { return true; } } return false; } protected void addExtensionRequirements(ExtensionRunRequirements extension) { if (addExtensionsRequirements == null) { addExtensionsRequirements = new ArrayList<>(5); } addExtensionsRequirements.add(extension); } } /** * The requirements to run an {@code extension} (with add-on dependencies). * <p> * It can be used to check if an extension can or not be run, which requirements it has (for example, dependency add-ons) * and which issues prevent it from being run, if any. * * @since 2.4.0 */ public static class ExtensionRunRequirements extends BaseRunRequirements { private final String classname; private ExtensionRunRequirements(AddOn addOn, String classname) { super(addOn); this.classname = classname; } /** * Gets the classname of the extension. * * @return the classname of the extension */ public String getClassname() { return classname; } } private static int getJavaVersion(String javaVersion) { return toVersionInt(toJavaVersionIntArray(javaVersion, 2)); } // NOTE: Following 2 methods copied from org.apache.commons.lang.SystemUtils version 2.6 because of constrained visibility private static int[] toJavaVersionIntArray(String version, int limit) { if (version == null) { return ArrayUtils.EMPTY_INT_ARRAY; } String[] strings = StringUtils.split(version, "._- "); int[] ints = new int[Math.min(limit, strings.length)]; int j = 0; for (int i = 0; i < strings.length && j < limit; i++) { String s = strings[i]; if (s.length() > 0) { try { ints[j] = Integer.parseInt(s); j++; } catch (Exception e) { } } } if (ints.length > j) { int[] newInts = new int[j]; System.arraycopy(ints, 0, newInts, 0, j); ints = newInts; } return ints; } private static int toVersionInt(int[] javaVersions) { if (javaVersions == null) { return 0; } int intVersion = 0; int len = javaVersions.length; if (len >= 1) { intVersion = javaVersions[0] * 100; } if (len >= 2) { intVersion += javaVersions[1] * 10; } if (len >= 3) { intVersion += javaVersions[2]; } return intVersion; } }