/*
* Zed Attack Proxy (ZAP) and its related class files.
*
* ZAP is an HTTP/HTTPS proxy for assessing web application security.
*
* Copyright 2011 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.InputStream;
import java.net.URL;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.MissingResourceException;
import java.util.ResourceBundle;
import java.util.TreeMap;
import java.util.Vector;
import javax.help.HelpBroker;
import javax.help.HelpSet;
import javax.help.HelpSetException;
import javax.help.HelpUtilities;
import org.apache.commons.configuration.Configuration;
import org.apache.log4j.Logger;
import org.parosproxy.paros.Constant;
import org.parosproxy.paros.extension.Extension;
import org.parosproxy.paros.extension.ExtensionLoader;
import org.parosproxy.paros.model.Model;
import org.parosproxy.paros.model.OptionsParam;
import org.zaproxy.zap.extension.ext.ExtensionParam;
import org.zaproxy.zap.extension.help.ExtensionHelp;
public class ExtensionFactory {
private static Logger log = Logger.getLogger(ExtensionFactory.class);
private static Vector<Extension> listAllExtension = new Vector<>();
private static TreeMap<String, Extension> mapAllExtension = new TreeMap<>();
private static TreeMap<Integer, Extension> mapOrderToExtension = new TreeMap<>();
private static List<Extension> unorderedExtensions = new ArrayList<>();
private static AddOnLoader addOnLoader = null;
public ExtensionFactory() {
super();
}
private static AddOnLoader getAddOnLoader(List<File> extraDirs) {
if (addOnLoader == null) {
File [] dirs = new File[extraDirs.size()+2];
dirs [0] = new File(Constant.getZapInstall(), Constant.FOLDER_PLUGIN);
dirs [1] = new File(Constant.getZapHome(), Constant.FOLDER_PLUGIN);
for (int i=0; i < extraDirs.size(); i++) {
dirs [2+i] = extraDirs.get(i);
}
addOnLoader = new AddOnLoader(dirs);
log.info("Installed add-ons: " + addOnLoader.getAddOnCollection().getInstalledAddOns());
} else {
log.error("AddOnLoader initialised without additional directories");
}
return addOnLoader;
}
public static AddOnLoader getAddOnLoader() {
if (addOnLoader == null) {
addOnLoader = new AddOnLoader(new File[]{
new File(Constant.getZapInstall(), Constant.FOLDER_PLUGIN),
new File(Constant.getZapHome(), Constant.FOLDER_PLUGIN)});
log.info("Installed add-ons: " + addOnLoader.getAddOnCollection().getInstalledAddOns());
}
return addOnLoader;
}
public static synchronized void loadAllExtension(ExtensionLoader extensionLoader, OptionsParam optionsParam) {
log.info("Loading extensions");
List<Extension> listExts = new ArrayList<>(CoreFunctionality.getBuiltInExtensions());
listExts.addAll(getAddOnLoader(optionsParam.getCheckForUpdatesParam().getAddonDirectories()).getExtensions());
ExtensionParam extParam = optionsParam.getExtensionParam();
synchronized (mapAllExtension) {
mapAllExtension.clear();
for (int i = 0; i < listExts.size(); i++) {
Extension extension = listExts.get(i);
if (mapAllExtension.containsKey(extension.getName())) {
if (mapAllExtension.get(extension.getName()).getClass().equals(extension.getClass())) {
// Same name, same class so ignore
log.error("Duplicate extension: " + extension.getName() + " "
+ extension.getClass().getCanonicalName());
continue;
} else {
// Same name but different class, log but still load it
log.error("Duplicate extension name: " + extension.getName() + " "
+ extension.getClass().getCanonicalName()
+ " " + mapAllExtension.get(extension.getName()).getClass().getCanonicalName());
}
}
if (extension.isDepreciated()) {
log.debug("Depreciated extension " + extension.getName());
continue;
}
extension.setEnabled(extParam.isExtensionEnabled(extension.getName()));
listAllExtension.add(extension);
mapAllExtension.put(extension.getName(), extension);
int order = extension.getOrder();
if (order == 0) {
unorderedExtensions.add(extension);
} else if (mapOrderToExtension.containsKey(order)) {
log.error("Duplicate order " + order + " "
+ mapOrderToExtension.get(order).getName() + "/" + mapOrderToExtension.get(order).getClass().getCanonicalName()
+ " already registered, "
+ extension.getName() + "/" + extension.getClass().getCanonicalName()
+ " will be added as an unordered extension");
unorderedExtensions.add(extension);
} else {
mapOrderToExtension.put(order, extension);
}
}
// Add the ordered extensions
Iterator<Integer> iter = mapOrderToExtension.keySet().iterator();
while (iter.hasNext()) {
Integer order = iter.next();
Extension ext = mapOrderToExtension.get(order);
if (ext.isEnabled()) {
log.debug("Ordered extension " + order + " " + ext.getName());
}
loadMessagesAndAddExtension(extensionLoader, ext);
}
// And then the unordered ones
for (Extension ext : unorderedExtensions) {
if (ext.isEnabled()) {
log.debug("Unordered extension " + ext.getName());
}
loadMessagesAndAddExtension(extensionLoader, ext);
}
}
log.info("Extensions loaded");
}
/**
* Loads the messages of the {@code extension} and, if enabled, adds it to
* the {@code extensionLoader} and loads the extension's help set.
*
* @param extensionLoader the extension loader
* @param extension the extension
* @see #loadMessages(Extension)
* @see ExtensionLoader#addExtension(Extension)
* @see #intitializeHelpSet(Extension)
*/
private static void loadMessagesAndAddExtension(ExtensionLoader extensionLoader, Extension extension) {
loadMessages(extension);
if (extension.isEnabled() && extension.supportsDb(Model.getSingleton().getDb().getType()) &&
(extension.supportsLowMemory() || ! Constant.isLowMemoryOptionSet())) {
extensionLoader.addExtension(extension);
intitializeHelpSet(extension);
} else if (!extension.supportsDb(Model.getSingleton().getDb().getType())) {
log.debug("Not loading extension " + extension.getName() + ": doesnt support " + Model.getSingleton().getDb().getType());
} else if (extension.supportsLowMemory() || ! Constant.isLowMemoryOptionSet()) {
log.debug("Not loading extension " + extension.getName() + ": doesnt support low memory option");
}
}
public static synchronized void addAddOnExtension(
ExtensionLoader extensionLoader,
Configuration config,
Extension extension) {
synchronized (mapAllExtension) {
addExtensionImpl(extension);
if (extension.isEnabled()) {
log.debug("Adding new extension " + extension.getName());
}
loadMessagesAndAddExtension(extensionLoader, extension);
}
}
private static void addExtensionImpl(Extension extension) {
if (mapAllExtension.containsKey(extension.getName())) {
if (mapAllExtension.get(extension.getName()).getClass().equals(extension.getClass())) {
// Same name, same class cant currently replace exts already loaded
log.debug("Duplicate extension: " + extension.getName() + " " + extension.getClass().getCanonicalName());
extension.setEnabled(false);
return;
}
// Same name but different class, log but still load it
log.error("Duplicate extension name: " + extension.getName() + " " + extension.getClass().getCanonicalName() + " "
+ mapAllExtension.get(extension.getName()).getClass().getCanonicalName());
}
if (extension.isDepreciated()) {
log.debug("Depreciated extension " + extension.getName());
return;
}
ExtensionParam extensionParam = Model.getSingleton().getOptionsParam().getExtensionParam();
extension.setEnabled(extensionParam.isExtensionEnabled(extension.getName()));
listAllExtension.add(extension);
mapAllExtension.put(extension.getName(), extension);
// Order actually irrelevant when adding an addon;)
int order = extension.getOrder();
if (order == 0) {
unorderedExtensions.add(extension);
} else if (mapOrderToExtension.containsKey(order)) {
log.error("Duplicate order " + order + " " + mapOrderToExtension.get(order).getName() + "/"
+ mapOrderToExtension.get(order).getClass().getCanonicalName() + " already registered, "
+ extension.getName() + "/" + extension.getClass().getCanonicalName()
+ " will be added as an unordered extension");
unorderedExtensions.add(extension);
} else {
mapOrderToExtension.put(order, extension);
}
}
public static synchronized List<Extension> loadAddOnExtensions(ExtensionLoader extensionLoader, Configuration config, AddOn addOn) {
List<Extension> listExts = getAddOnLoader().getExtensions(addOn);
synchronized (mapAllExtension) {
for (Extension extension : listExts) {
addExtensionImpl(extension);
}
for (Extension ext : listExts) {
if (ext.isEnabled()) {
log.debug("Adding new extension " + ext.getName());
}
loadMessagesAndAddExtension(extensionLoader, ext);
}
}
return listExts;
}
private static void loadMessages(Extension ext) {
ResourceBundle msg = getExtensionResourceBundle(ext);
if (msg != null) {
ext.setMessages(msg);
Constant.messages.addMessageBundle(ext.getI18nPrefix(), ext.getMessages());
}
}
private static ResourceBundle getExtensionResourceBundle(Extension ext) {
String extensionPackage = ext.getClass().getPackage().getName();
ClassLoader classLoader = ext.getClass().getClassLoader();
try {
// Try to load a message bundle in the new/default location
String name = extensionPackage + ".resources." + Constant.MESSAGES_PREFIX;
return getPropertiesResourceBundle(name, classLoader);
} catch (MissingResourceException ignore) {
// Try to load in the old location
String oldLocation = extensionPackage + "." + Constant.MESSAGES_PREFIX;
try {
return getPropertiesResourceBundle(oldLocation, classLoader);
} catch (MissingResourceException ignoreAgain) {
// It will be using the standard message bundle
}
}
return null;
}
private static ResourceBundle getPropertiesResourceBundle(String name, ClassLoader classLoader)
throws MissingResourceException {
return ResourceBundle.getBundle(
name,
Constant.getLocale(),
classLoader,
ResourceBundle.Control.getControl(ResourceBundle.Control.FORMAT_PROPERTIES));
}
/**
* If there are help files within the extension, they are loaded and merged
* with existing help files if the core help was correctly loaded.
* @param ext the extension being initialised
*/
private static void intitializeHelpSet(Extension ext) {
HelpBroker hb = ExtensionHelp.getHelpBroker();
if (hb == null) {
return;
}
URL helpSetUrl = getExtensionHelpSetUrl(ext);
if (helpSetUrl != null) {
try {
log.debug("Load help files for extension '" + ext.getName() + "' and merge with core help.");
HelpSet extHs = new HelpSet(ext.getClass().getClassLoader(), helpSetUrl);
hb.getHelpSet().add(extHs);
} catch (HelpSetException e) {
log.error("An error occured while adding help file of extension '" + ext.getName() + "': " + e.getMessage(), e);
}
}
}
private static URL getExtensionHelpSetUrl(Extension extension) {
String extensionPackage = extension.getClass().getPackage().getName().replace('.', '/') + "/";
URL helpSetUrl = findResource(
extension.getClass().getClassLoader(),
extensionPackage + "resources/help",
"helpset",
".hs",
Constant.getLocale());
if (helpSetUrl == null) {
// Search in old location
helpSetUrl = findResource(
extension.getClass().getClassLoader(),
extensionPackage + "resource/help",
"helpset",
".hs",
Constant.getLocale());
}
return helpSetUrl;
}
public static List<Extension> getAllExtensions() {
return listAllExtension;
}
public static Extension getExtension(String name) {
Extension test = mapAllExtension.get(name);
return test;
}
public static void unloadAddOnExtension(Extension extension) {
synchronized (mapAllExtension) {
unloadMessages(extension);
unloadHelpSet(extension);
mapAllExtension.remove(extension.getName());
listAllExtension.remove(extension);
boolean isUnordered = true;
for (Iterator<Extension> it = mapOrderToExtension.values().iterator(); it.hasNext();) {
if (it.next() == extension) {
it.remove();
isUnordered = false;
break;
}
}
if (isUnordered) {
unorderedExtensions.remove(extension);
}
}
}
private static void unloadMessages(Extension extension) {
ResourceBundle msg = extension.getMessages();
if (msg != null) {
Constant.messages.removeMessageBundle(extension.getI18nPrefix());
}
}
private static void unloadHelpSet(Extension ext) {
HelpBroker hb = ExtensionHelp.getHelpBroker();
if (hb == null) {
return;
}
URL helpSetUrl = getExtensionHelpSetUrl(ext);
if (helpSetUrl != null) {
HelpSet baseHelpSet = hb.getHelpSet();
Enumeration<?> helpSets = baseHelpSet.getHelpSets();
while (helpSets.hasMoreElements()) {
HelpSet extensionHelpSet = (HelpSet) helpSets.nextElement();
if (helpSetUrl.equals(extensionHelpSet.getHelpSetURL())) {
baseHelpSet.remove(extensionHelpSet);
break;
}
}
}
}
/**
* Finds and returns the URL to a resource with the given class loader, or
* system class loader if {@code null}, for the given or default locales.
* <p>
* The resource pathname will be constructed using the parameters package
* name, file name, file extension and candidate locales. The candidate
* locales are created from the given locale using the method
* {@code HelpUtilities#getCandidates(Locale)}.
* </p>
* <p>
* The resource pathname is constructed as:
*
* <pre>
* "package name" + "candidate locale" + '/' + "file name" + "candidate locale" + "file extension"
* </pre>
*
* For example, with the following parameters:
* <ul>
* <li>package name - /org/zaproxy/zap/extension/example/resources/help</li>
* <li>file name - helpset</li>
* <li>file extension - .hs</li>
* <li>locale - es_ES</li>
* </ul>
* and default locale "en_GB", it would produce the following resource
* pathnames:
*
* <pre>
* /org/zaproxy/zap/extension/example/resources/help_es_ES/helpset_es_ES.hs
* /org/zaproxy/zap/extension/example/resources/help_es/helpset_es.hs
* /org/zaproxy/zap/extension/example/resources/help/helpset.hs
* /org/zaproxy/zap/extension/example/resources/help_en_GB/helpset_en_GB.hs
* /org/zaproxy/zap/extension/example/resources/help_en/helpset_en.hs
* </pre>
*
* The URL of the first existent resource is returned.
*
* @param cl the class loader that will be used to get the resource,
* {@code null} the system class loader is used.
* @param packageName the name of the package where the resource is
* @param fileName the file name of the resource
* @param fileExtension the file extension of the resource
* @param locale the target locale of the required resource
* @return An {@code URL} with the path to the resource or {@code null} if
* not found.
* @see HelpUtilities#getCandidates(Locale)
*/
// Implementation based (read copied) from:
// javax.help.HelpUtilities#getLocalizedResource(ClassLoader cl, String front, String back, Locale locale, boolean tryRead)
// Changes:
// - Removed the "tryRead" flag since it's not needed (it's set to try to read always);
// - Replaced the use of StringBuffer with StringBuilder;
// - Renamed parameters "front" to "packageName" and "back" to "name";
// - Renamed variable "tail" to "candidateLocale";
// - Renamed variable "name" to "resource";
// - Added type parameter to "tails" enumeration (now "candidateLocales"), @SuppressWarnings annotation and removed the
// String cast;
// - Changed to use try-with-resource statement to manage the input stream.
// - Changed to also append the "candidateLocale" to the packageName followed by character '/';
private static URL findResource(ClassLoader cl, String packageName, String fileName, String fileExtension, Locale locale) {
URL url;
for (@SuppressWarnings("unchecked") Enumeration<String> candidateLocales = HelpUtilities.getCandidates(locale); candidateLocales.hasMoreElements();) {
String candidateLocale = candidateLocales.nextElement();
String resource = (new StringBuilder(packageName)).append(candidateLocale)
.append('/')
.append(fileName)
.append(candidateLocale)
.append(fileExtension)
.toString();
if (cl == null) {
url = ClassLoader.getSystemResource(resource);
} else {
url = cl.getResource(resource);
}
if (url != null) {
// Try doing an actual read to be sure it exists
try (InputStream is = url.openConnection().getInputStream()) {
if (is != null && is.read() != -1) {
return url;
}
} catch (Throwable t) {
// ignore and continue looking
}
}
}
return null;
}
}