1 /*
2  * Zed Attack Proxy (ZAP) and its related class files.
3  *
4  * ZAP is an HTTP/HTTPS proxy for assessing web application security.
5  *
6  * Copyright 2012 The ZAP Development Team
7  *
8  * Licensed under the Apache License, Version 2.0 (the "License");
9  * you may not use this file except in compliance with the License.
10  * You may obtain a copy of the License at
11  *
12  *     http://www.apache.org/licenses/LICENSE-2.0
13  *
14  * Unless required by applicable law or agreed to in writing, software
15  * distributed under the License is distributed on an "AS IS" BASIS,
16  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17  * See the License for the specific language governing permissions and
18  * limitations under the License.
19  */
20 package org.zaproxy.zap.control;
21 
22 import java.awt.EventQueue;
23 import java.io.File;
24 import java.io.FileFilter;
25 import java.io.FilenameFilter;
26 import java.lang.reflect.Constructor;
27 import java.lang.reflect.Modifier;
28 import java.net.MalformedURLException;
29 import java.net.URI;
30 import java.net.URISyntaxException;
31 import java.net.URL;
32 import java.net.URLClassLoader;
33 import java.util.ArrayList;
34 import java.util.Collection;
35 import java.util.Collections;
36 import java.util.Enumeration;
37 import java.util.HashMap;
38 import java.util.HashSet;
39 import java.util.Iterator;
40 import java.util.List;
41 import java.util.Map;
42 import java.util.Map.Entry;
43 import java.util.ResourceBundle;
44 import java.util.Set;
45 import java.util.concurrent.locks.Lock;
46 import java.util.concurrent.locks.ReentrantLock;
47 import java.util.jar.JarEntry;
48 import java.util.jar.JarFile;
49 import java.util.stream.Collectors;
50 import java.util.zip.ZipEntry;
51 import org.apache.commons.configuration.Configuration;
52 import org.apache.commons.configuration.ConfigurationException;
53 import org.apache.commons.configuration.FileConfiguration;
54 import org.apache.commons.configuration.HierarchicalConfiguration;
55 import org.apache.logging.log4j.LogManager;
56 import org.apache.logging.log4j.Logger;
57 import org.parosproxy.paros.Constant;
58 import org.parosproxy.paros.control.Control;
59 import org.parosproxy.paros.core.scanner.AbstractPlugin;
60 import org.parosproxy.paros.extension.Extension;
61 import org.parosproxy.paros.model.Model;
62 import org.parosproxy.paros.view.View;
63 import org.zaproxy.zap.Version;
64 import org.zaproxy.zap.control.AddOn.AddOnRunRequirements;
65 import org.zaproxy.zap.control.AddOn.ExtensionRunRequirements;
66 import org.zaproxy.zap.extension.pscan.PluginPassiveScanner;
67 import org.zaproxy.zap.utils.ZapXmlConfiguration;
68 
69 /**
70  * This class is heavily based on the original Paros class org.parosproxy.paros.common.DynamicLoader
71  * However its been restructured and enhanced to support multiple directories or versioned ZAP
72  * addons. The constructor takes an array of directories. All of the generic jars in the directories
73  * are loaded. Only the latest ZAP addons are loaded, so if the following addons are found:
74  * zap-ext-test-alpha-1.zap zap-ext-test-beta-2.zap zap-ext-test-alpha-3.zap then only the latest
75  * one (zap-ext-test-alpha-3.zap) will be loaded - this is entirely based on the version number. The
76  * status (alpha/beta/release) is for informational purposes only.
77  */
78 public class AddOnLoader extends URLClassLoader {
79 
80     public static final String ADDONS_BLOCK_LIST = "addons.block";
81 
82     private static final String ADDONS_RUNNABLE_BASE_KEY = "runnableAddOns";
83     private static final String ADDONS_RUNNABLE_KEY = ADDONS_RUNNABLE_BASE_KEY + ".addon";
84     private static final String ADDON_RUNNABLE_ID_KEY = "id";
85     private static final String ADDON_RUNNABLE_VERSION_KEY = "version";
86     private static final String ADDON_RUNNABLE_FULL_VERSION_KEY = "fullversion";
87     private static final String ADDON_RUNNABLE_ALL_EXTENSIONS_KEY = "extensions.extension";
88 
89     /** A "null" object, for use when no callback is given during the uninstallation process. */
90     private static final AddOnUninstallationProgressCallback NULL_CALLBACK =
91             new NullUninstallationProgressCallBack();
92 
93     private static final Logger logger = LogManager.getLogger(AddOnLoader.class);
94 
95     static {
ClassLoader.registerAsParallelCapable()96         ClassLoader.registerAsParallelCapable();
97     }
98 
99     private Lock installationLock = new ReentrantLock();
100     private AddOnCollection aoc = null;
101     private List<File> jars = new ArrayList<>();
102     /**
103      * Addons can be included in the ZAP release, in which case the user might not have permissions
104      * to delete the files. To support the removal of such addons we just maintain a 'block list' in
105      * the configs which is a comma separated list of the addon ids that the user has uninstalled
106      */
107     private List<String> blockList = new ArrayList<>();
108 
109     /**
110      * The runnable add-ons and its extensions.
111      *
112      * <p>The key is the add-on itself and the value its runnable extensions.
113      */
114     private Map<AddOn, List<String>> runnableAddOns;
115 
116     /**
117      * The list of add-ons' IDs that have running issues (either the add-on itself or one of its
118      * extensions) since last run because of changes in the dependencies.
119      */
120     private List<String> idsAddOnsWithRunningIssuesSinceLastRun;
121 
122     /*
123      * Using sub-classloaders means we can unload and reload addons
124      */
125     private Map<String, AddOnClassLoader> addOnLoaders = new HashMap<>();
126 
127     /** File where the data of runnable state and blocked add-ons is saved. */
128     private ZapXmlConfiguration addOnsStateConfig;
129 
AddOnLoader(File[] dirs)130     public AddOnLoader(File[] dirs) {
131         super(new URL[0], AddOnLoader.class.getClassLoader());
132 
133         addOnsStateConfig = new ZapXmlConfiguration();
134         addOnsStateConfig.setRootElementName("addonsstate");
135         File configFile = new File(Constant.getZapHome(), "add-ons-state.xml");
136         addOnsStateConfig.setFile(configFile);
137         if (!migrateOldAddOnsState(addOnsStateConfig) && configFile.exists()) {
138             try {
139                 addOnsStateConfig.load();
140             } catch (ConfigurationException e) {
141                 logger.warn("Failed to read add-ons' state file:", e);
142             }
143         }
144 
145         this.loadBlockList();
146 
147         this.aoc = new AddOnCollection(dirs);
148         loadAllAddOns();
149 
150         if (dirs != null) {
151             for (File dir : dirs) {
152                 try {
153                     this.addDirectory(dir);
154                 } catch (Exception e) {
155                     logger.error(e.getMessage(), e);
156                 }
157             }
158         }
159 
160         for (File jar : jars) {
161             try {
162                 this.addURL(jar.toURI().toURL());
163             } catch (MalformedURLException e) {
164                 logger.error(e.getMessage(), e);
165             }
166         }
167 
168         // Install any files that are not already present
169         for (Entry<String, AddOnClassLoader> entry : addOnLoaders.entrySet()) {
170             AddOnInstaller.installMissingAddOnFiles(
171                     entry.getValue(), getAddOnCollection().getAddOn(entry.getKey()));
172         }
173     }
174 
175     /**
176      * Returns a list with the IDs of add-ons that have running issues since last run, either Java
177      * version was changed, or add-on dependencies are no longer met for the add-on or one of its
178      * extensions.
179      *
180      * @return a list with the add-ons that are not longer runnable
181      * @since 2.4.0
182      */
getIdsAddOnsWithRunningIssuesSinceLastRun()183     public List<String> getIdsAddOnsWithRunningIssuesSinceLastRun() {
184         return Collections.unmodifiableList(idsAddOnsWithRunningIssuesSinceLastRun);
185     }
186 
loadAllAddOns()187     private void loadAllAddOns() {
188         for (Iterator<AddOn> iterator = aoc.getAddOns().iterator(); iterator.hasNext(); ) {
189             AddOn addOn = iterator.next();
190             if (canLoadAddOn(addOn)) {
191                 AddOnInstaller.installMissingAddOnLibs(addOn);
192             } else {
193                 iterator.remove();
194             }
195         }
196 
197         runnableAddOns = new HashMap<>();
198         idsAddOnsWithRunningIssuesSinceLastRun = new ArrayList<>();
199         Map<AddOn, AddOnRunState> oldRunnableAddOns = loadAddOnsRunState(addOnsStateConfig, aoc);
200         List<AddOn> runAddons = new ArrayList<>();
201         Set<AddOn> updatedAddOns = new HashSet<>();
202         Set<AddOn> nonRunnableAddOns = new HashSet<>();
203         for (Iterator<AddOn> iterator = aoc.getAddOns().iterator(); iterator.hasNext(); ) {
204             AddOn addOn = iterator.next();
205             AddOnRunRequirements reqs = calculateRunRequirements(addOn, aoc.getAddOns());
206             if (reqs.isRunnable()) {
207                 AddOnRunState runState = oldRunnableAddOns.get(addOn);
208                 List<String> runnableExtensions;
209                 if (addOn.hasExtensionsWithDeps()) {
210                     runnableExtensions = getRunnableExtensionsWithDeps(reqs);
211                     List<String> oldRunnableExtensions =
212                             runState != null ? runState.getExtensions() : Collections.emptyList();
213                     if (!oldRunnableExtensions.isEmpty()) {
214                         oldRunnableExtensions.removeAll(runnableExtensions);
215                         if (!oldRunnableExtensions.isEmpty()) {
216                             idsAddOnsWithRunningIssuesSinceLastRun.add(addOn.getId());
217                         }
218                     }
219                 } else {
220                     runnableExtensions = Collections.emptyList();
221                 }
222 
223                 runnableAddOns.put(addOn, runnableExtensions);
224                 runAddons.add(addOn);
225                 if (runState != null && runState.hasNewerVersion()) {
226                     updatedAddOns.add(addOn);
227                 }
228             } else {
229                 nonRunnableAddOns.add(addOn);
230             }
231         }
232 
233         nonRunnableAddOns.stream()
234                 .filter(oldRunnableAddOns::containsKey)
235                 .map(AddOn::getId)
236                 .forEach(idsAddOnsWithRunningIssuesSinceLastRun::add);
237 
238         saveAddOnsRunState(runnableAddOns);
239 
240         for (AddOn addOn : runAddons) {
241             addOn.setInstallationStatus(AddOn.InstallationStatus.INSTALLED);
242             AddOnClassLoader addOnClassLoader = createAndAddAddOnClassLoader(addOn);
243             if (updatedAddOns.contains(addOn)) {
244                 AddOnInstaller.updateAddOnFiles(addOnClassLoader, addOn);
245             }
246             AddOnInstaller.installResourceBundle(addOnClassLoader, addOn);
247         }
248     }
249 
getRunnableExtensionsWithDeps( AddOnRunRequirements runRequirements)250     private static List<String> getRunnableExtensionsWithDeps(
251             AddOnRunRequirements runRequirements) {
252         List<String> runnableExtensions = new ArrayList<>();
253         for (ExtensionRunRequirements extReqs : runRequirements.getExtensionRequirements()) {
254             if (extReqs.isRunnable()) {
255                 runnableExtensions.add(extReqs.getClassname());
256             }
257         }
258         return runnableExtensions;
259     }
260 
canLoadAddOn(AddOn ao)261     private boolean canLoadAddOn(AddOn ao) {
262         if (blockList.contains(ao.getId())) {
263             if (logger.isDebugEnabled()) {
264                 logger.debug(
265                         "Can't load add-on "
266                                 + ao.getName()
267                                 + " it's on the block list (add-on uninstalled but the file couldn't be removed).");
268             }
269             return false;
270         }
271 
272         if (!ao.canLoadInCurrentVersion()) {
273             if (logger.isDebugEnabled()) {
274                 logger.debug(
275                         "Can't load add-on "
276                                 + ao.getName()
277                                 + " because of ZAP version constraints; Not before="
278                                 + ao.getNotBeforeVersion()
279                                 + " Not from="
280                                 + ao.getNotFromVersion()
281                                 + " Current Version="
282                                 + Constant.PROGRAM_VERSION);
283             }
284             return false;
285         }
286         return true;
287     }
288 
calculateRunRequirements( AddOn ao, Collection<AddOn> availableAddOns)289     private static AddOnRunRequirements calculateRunRequirements(
290             AddOn ao, Collection<AddOn> availableAddOns) {
291         AddOnRunRequirements reqs = ao.calculateRunRequirements(availableAddOns);
292         if (!reqs.isRunnable()) {
293             if (logger.isDebugEnabled()) {
294                 logger.debug(
295                         "Can't run add-on "
296                                 + ao.getName()
297                                 + " because of missing requirements: "
298                                 + AddOnRunIssuesUtils.getRunningIssues(reqs));
299             }
300         }
301         return reqs;
302     }
303 
createAndAddAddOnClassLoader(AddOn ao)304     private AddOnClassLoader createAndAddAddOnClassLoader(AddOn ao) {
305         try {
306             AddOnClassLoader addOnClassLoader = addOnLoaders.get(ao.getId());
307             if (addOnClassLoader != null) {
308                 return addOnClassLoader;
309             }
310 
311             List<String> idsAddOnDependencies = ao.getIdsAddOnDependencies();
312             if (idsAddOnDependencies.isEmpty()) {
313                 addOnClassLoader =
314                         new AddOnClassLoader(
315                                 ao.getFile().toURI().toURL(), this, ao.getAddOnClassnames());
316                 putAddOnClassLoader(ao, addOnClassLoader);
317                 return addOnClassLoader;
318             }
319 
320             List<AddOnClassLoader> dependencies = new ArrayList<>(idsAddOnDependencies.size());
321             for (String addOnId : idsAddOnDependencies) {
322                 addOnClassLoader = addOnLoaders.get(addOnId);
323                 if (addOnClassLoader == null) {
324                     addOnClassLoader = createAndAddAddOnClassLoader(aoc.getAddOn(addOnId));
325                 }
326                 dependencies.add(addOnClassLoader);
327             }
328 
329             addOnClassLoader =
330                     new AddOnClassLoader(
331                             ao.getFile().toURI().toURL(),
332                             this,
333                             dependencies,
334                             ao.getAddOnClassnames());
335             putAddOnClassLoader(ao, addOnClassLoader);
336             return addOnClassLoader;
337         } catch (MalformedURLException e) {
338             logger.error(e.getMessage(), e);
339             throw new RuntimeException(
340                     "Failed to convert URL for AddOnClassLoader " + ao.getFile().toURI(), e);
341         }
342     }
343 
344     /**
345      * Puts the given add-on class loader into the {@link #addOnLoaders} map and {@link
346      * AddOn#setClassLoader(ClassLoader) sets it into the add-on}.
347      *
348      * <p>The add-on libraries are added to the add-on class loader before that.
349      *
350      * @param ao the add-on to put in the map.
351      * @param addOnClassLoader the class loader of the add-on.
352      */
putAddOnClassLoader(AddOn ao, AddOnClassLoader addOnClassLoader)353     private void putAddOnClassLoader(AddOn ao, AddOnClassLoader addOnClassLoader) {
354         if (!ao.getLibs().isEmpty()) {
355             addOnClassLoader.addUrls(
356                     ao.getLibs().stream()
357                             .map(AddOn.Lib::getFileSystemUrl)
358                             .collect(Collectors.toList()));
359         }
360         ao.setClassLoader(addOnClassLoader);
361         addOnLoaders.put(ao.getId(), addOnClassLoader);
362     }
363 
364     @Override
loadClass(String name)365     public Class<?> loadClass(String name) throws ClassNotFoundException {
366         synchronized (getClassLoadingLock(name)) {
367             try {
368                 return loadClass(name, false);
369             } catch (ClassNotFoundException e) {
370                 // Continue for now
371             }
372             for (AddOnClassLoader loader : addOnLoaders.values()) {
373                 try {
374                     return loader.loadClass(name);
375                 } catch (ClassNotFoundException e) {
376                     // Continue for now
377                 }
378                 for (AddOnClassLoader childLoader : loader.getChildClassLoaders()) {
379                     try {
380                         return childLoader.loadClass(name);
381                     } catch (ClassNotFoundException e) {
382                         // Continue for now
383                     }
384                 }
385             }
386             throw new ClassNotFoundException(name);
387         }
388     }
389 
390     @Override
getClassLoadingLock(String className)391     protected Object getClassLoadingLock(String className) {
392         // Allow AddOnClassLoader to use the same locks.
393         return super.getClassLoadingLock(className);
394     }
395 
396     @Override
getResource(String name)397     public URL getResource(String name) {
398         URL url = super.getResource(name);
399         if (url != null) {
400             return url;
401         }
402         for (AddOnClassLoader loader : addOnLoaders.values()) {
403             url = loader.findResourceInAddOn(name);
404             if (url != null) {
405                 return url;
406             }
407         }
408         return url;
409     }
410 
getAddOnCollection()411     public AddOnCollection getAddOnCollection() {
412         return this.aoc;
413     }
414 
addDirectory(File dir)415     private void addDirectory(File dir) {
416         if (dir == null) {
417             logger.error("Null directory supplied");
418             return;
419         }
420         if (!dir.exists()) {
421             logger.debug("No such directory: " + dir.getAbsolutePath());
422             return;
423         }
424         if (!dir.isDirectory()) {
425             logger.warn("Not a directory: " + dir.getAbsolutePath());
426             return;
427         }
428 
429         // Load the jar files
430         File[] listJars = dir.listFiles(new JarFilenameFilter());
431         if (listJars != null) {
432             for (File jar : listJars) {
433                 this.jars.add(jar);
434             }
435         }
436     }
437 
addAddon(AddOn ao)438     public void addAddon(AddOn ao) {
439         if (!ao.canLoadInCurrentVersion()) {
440             throw new IllegalArgumentException(
441                     "Cant load add-on "
442                             + ao.getName()
443                             + " Not before="
444                             + ao.getNotBeforeVersion()
445                             + " Not from="
446                             + ao.getNotFromVersion()
447                             + " Version="
448                             + Constant.PROGRAM_VERSION);
449         }
450 
451         installationLock.lock();
452         try {
453             if (!this.aoc.addAddOn(ao)) {
454                 return;
455             }
456 
457             addAddOnImpl(ao);
458         } finally {
459             installationLock.unlock();
460         }
461     }
462 
addAddOnImpl(AddOn ao)463     private void addAddOnImpl(AddOn ao) {
464         if (AddOn.InstallationStatus.INSTALLED == ao.getInstallationStatus()) {
465             return;
466         }
467 
468         if (this.blockList.contains(ao.getId())) {
469             // Explicitly being added back, so remove from the block list
470             this.blockList.remove(ao.getId());
471             this.saveBlockList();
472         }
473 
474         if (!isDynamicallyInstallable(ao)) {
475             return;
476         }
477 
478         if (!AddOnInstaller.installAddOnLibs(ao)) {
479             ao.setInstallationStatus(AddOn.InstallationStatus.NOT_INSTALLED);
480             return;
481         }
482 
483         AddOnRunRequirements reqs = calculateRunRequirements(ao, aoc.getInstalledAddOns());
484         if (!reqs.isRunnable()) {
485             ao.setInstallationStatus(AddOn.InstallationStatus.NOT_INSTALLED);
486             return;
487         }
488 
489         AddOnInstaller.install(createAndAddAddOnClassLoader(ao), ao);
490         ao.setInstallationStatus(AddOn.InstallationStatus.INSTALLED);
491         Control.getSingleton().getExtensionLoader().addOnInstalled(ao);
492 
493         if (runnableAddOns.get(ao) == null) {
494             runnableAddOns.put(ao, getRunnableExtensionsWithDeps(reqs));
495             saveAddOnsRunState(runnableAddOns);
496         }
497 
498         checkAndLoadDependentExtensions();
499         checkAndInstallAddOnsNotInstalled();
500 
501         if (View.isInitialised()) {
502             EventQueue.invokeLater(
503                     new Runnable() {
504 
505                         @Override
506                         public void run() {
507                             View.getSingleton().refreshTabViewMenus();
508                         }
509                     });
510         }
511     }
512 
513     /**
514      * Checks and installs all the add-ons whose installation status is {@code NOT_INSTALLED} that
515      * have (now) all required dependencies fulfilled.
516      *
517      * <p>Should be called after an installation of an add-on.
518      *
519      * @see #addAddOnImpl(AddOn)
520      * @see AddOn.InstallationStatus#NOT_INSTALLED
521      * @since 2.4.0
522      */
checkAndInstallAddOnsNotInstalled()523     private void checkAndInstallAddOnsNotInstalled() {
524         List<AddOn> runnableAddOns = new ArrayList<>();
525         for (AddOn addOn : aoc.getAddOns()) {
526             if (AddOn.InstallationStatus.NOT_INSTALLED == addOn.getInstallationStatus()
527                     && addOnLoaders.get(addOn.getId()) == null) {
528                 AddOnRunRequirements reqs =
529                         addOn.calculateRunRequirements(aoc.getInstalledAddOns());
530                 if (reqs.isRunnable()) {
531                     runnableAddOns.add(addOn);
532                 }
533             }
534         }
535 
536         for (AddOn addOn : runnableAddOns) {
537             addAddOnImpl(addOn);
538         }
539     }
540 
541     /**
542      * Checks and loads all the extensions that have (now) all required dependencies fulfilled.
543      *
544      * <p>Should be called after an installation of an add-on.
545      *
546      * @see #addAddOnImpl(AddOn)
547      * @since 2.4.0
548      */
checkAndLoadDependentExtensions()549     private void checkAndLoadDependentExtensions() {
550         boolean changed = false;
551         for (Entry<String, AddOnClassLoader> entry : new HashMap<>(addOnLoaders).entrySet()) {
552             AddOn runningAddOn = aoc.getAddOn(entry.getKey());
553             if (runningAddOn.getInstallationStatus()
554                     == AddOn.InstallationStatus.UNINSTALLATION_FAILED) {
555                 continue;
556             }
557             for (String extClassName : runningAddOn.getExtensionsWithDeps()) {
558                 if (!runningAddOn.isExtensionLoaded(extClassName)) {
559                     AddOn.AddOnRunRequirements reqs =
560                             runningAddOn.calculateExtensionRunRequirements(
561                                     extClassName, aoc.getInstalledAddOns());
562                     ExtensionRunRequirements extReqs = reqs.getExtensionRequirements().get(0);
563                     if (extReqs.isRunnable()) {
564                         List<AddOnClassLoader> dependencies =
565                                 new ArrayList<>(extReqs.getDependencies().size());
566                         for (AddOn addOnDep : extReqs.getDependencies()) {
567                             dependencies.add(addOnLoaders.get(addOnDep.getId()));
568                         }
569                         AddOnClassLoader extAddOnClassLoader =
570                                 new AddOnClassLoader(
571                                         entry.getValue(),
572                                         dependencies,
573                                         runningAddOn.getExtensionAddOnClassnames(extClassName));
574                         Extension ext =
575                                 loadAddOnExtension(
576                                         runningAddOn, extReqs.getClassname(), extAddOnClassLoader);
577                         AddOnInstaller.installAddOnExtension(runningAddOn, ext);
578                         runnableAddOns.get(runningAddOn).add(extReqs.getClassname());
579                         changed = true;
580                     }
581                 }
582             }
583         }
584 
585         if (changed) {
586             saveAddOnsRunState(runnableAddOns);
587         }
588     }
589 
590     /**
591      * Tells whether or not the given {@code addOn} is dynamically installable.
592      *
593      * <p>It checks if the given {@code addOn} is dynamically installable by calling the method
594      * {@code AddOn#hasZapAddOnEntry()}.
595      *
596      * @param addOn the add-on that will be checked
597      * @return {@code true} if the given add-on is dynamically installable, {@code false} otherwise.
598      * @see AddOn#hasZapAddOnEntry()
599      * @since 2.3.0
600      */
isDynamicallyInstallable(AddOn addOn)601     private static boolean isDynamicallyInstallable(AddOn addOn) {
602         return addOn.hasZapAddOnEntry();
603     }
604 
removeAddOn( AddOn ao, boolean upgrading, AddOnUninstallationProgressCallback progressCallback)605     public boolean removeAddOn(
606             AddOn ao, boolean upgrading, AddOnUninstallationProgressCallback progressCallback) {
607         installationLock.lock();
608         try {
609             AddOnUninstallationProgressCallback callback =
610                     (progressCallback == null) ? NULL_CALLBACK : progressCallback;
611 
612             callback.uninstallingAddOn(ao, upgrading);
613             boolean removed = removeAddOnImpl(ao, upgrading, callback);
614             callback.addOnUninstalled(removed);
615 
616             return removed;
617         } finally {
618             installationLock.unlock();
619         }
620     }
621 
removeAddOnImpl( AddOn ao, boolean upgrading, AddOnUninstallationProgressCallback callback)622     private boolean removeAddOnImpl(
623             AddOn ao, boolean upgrading, AddOnUninstallationProgressCallback callback) {
624         if (!isDynamicallyInstallable(ao)) {
625             return false;
626         }
627 
628         if (AddOn.InstallationStatus.SOFT_UNINSTALLATION_FAILED == ao.getInstallationStatus()) {
629             if (runnableAddOns.remove(ao) != null) {
630                 saveAddOnsRunState(runnableAddOns);
631             }
632             AddOnInstaller.uninstallAddOnFiles(ao, NULL_CALLBACK, runnableAddOns.keySet());
633             removeAddOnClassLoader(ao);
634             deleteAddOn(ao, upgrading);
635             ao.setInstallationStatus(AddOn.InstallationStatus.UNINSTALLATION_FAILED);
636             Control.getSingleton().getExtensionLoader().addOnUninstalled(ao, false);
637             return false;
638         }
639 
640         if (!this.aoc.includesAddOn(ao.getId())) {
641             logger.warn("Trying to uninstall an add-on that is not installed: " + ao.getId());
642             return false;
643         }
644 
645         if (AddOn.InstallationStatus.NOT_INSTALLED == ao.getInstallationStatus()) {
646             if (runnableAddOns.remove(ao) != null) {
647                 saveAddOnsRunState(runnableAddOns);
648             }
649 
650             deleteAddOn(ao, upgrading);
651 
652             return this.aoc.removeAddOn(ao);
653         }
654 
655         unloadDependentExtensions(ao);
656         softUninstallDependentAddOns(ao);
657 
658         boolean uninstalledWithoutErrors =
659                 AddOnInstaller.uninstall(ao, callback, runnableAddOns.keySet());
660 
661         if (uninstalledWithoutErrors && !this.aoc.removeAddOn(ao)) {
662             uninstalledWithoutErrors = false;
663         }
664 
665         if (uninstalledWithoutErrors) {
666             removeAddOnClassLoader(ao);
667         }
668 
669         deleteAddOn(ao, upgrading);
670 
671         if (runnableAddOns.remove(ao) != null) {
672             saveAddOnsRunState(runnableAddOns);
673         }
674 
675         ao.setInstallationStatus(
676                 uninstalledWithoutErrors
677                         ? AddOn.InstallationStatus.AVAILABLE
678                         : AddOn.InstallationStatus.UNINSTALLATION_FAILED);
679 
680         Control.getSingleton().getExtensionLoader().addOnUninstalled(ao, uninstalledWithoutErrors);
681         return uninstalledWithoutErrors;
682     }
683 
684     /**
685      * Deletes the file and libraries of the given add-on.
686      *
687      * <p>The add-on is added to the {@link #blockList block list} when not able to delete it and if
688      * not updating it.
689      *
690      * @param addOn the add-on to be deleted.
691      * @param upgrading {@code true} if the add-on is being updated, {@code false} otherwise.
692      * @see AddOnInstaller#uninstallAddOnLibs(AddOn)
693      */
deleteAddOn(AddOn addOn, boolean upgrading)694     private void deleteAddOn(AddOn addOn, boolean upgrading) {
695         AddOnInstaller.uninstallAddOnLibs(addOn);
696 
697         if (addOn.getFile() != null && addOn.getFile().exists()) {
698             if (!addOn.getFile().delete() && !upgrading) {
699                 logger.debug("Cant delete " + addOn.getFile().getAbsolutePath());
700                 this.blockList.add(addOn.getId());
701                 this.saveBlockList();
702             }
703         }
704     }
705 
removeAddOnClassLoader(AddOn addOn)706     private void removeAddOnClassLoader(AddOn addOn) {
707         if (this.addOnLoaders.containsKey(addOn.getId())) {
708             try (AddOnClassLoader addOnClassLoader = this.addOnLoaders.remove(addOn.getId())) {
709                 if (!addOn.getIdsAddOnDependencies().isEmpty()) {
710                     addOnClassLoader.clearDependencies();
711                 }
712                 ResourceBundle.clearCache(addOnClassLoader);
713             } catch (Exception e) {
714                 logger.error(
715                         "Failure while closing class loader of " + addOn.getId() + " add-on:", e);
716             }
717             addOn.setClassLoader(null);
718         }
719     }
720 
unloadDependentExtensions(AddOn ao)721     private void unloadDependentExtensions(AddOn ao) {
722         boolean changed = false;
723         for (Entry<String, AddOnClassLoader> entry : new HashMap<>(addOnLoaders).entrySet()) {
724             AddOn runningAddOn = aoc.getAddOn(entry.getKey());
725             for (Extension ext : runningAddOn.getLoadedExtensionsWithDeps()) {
726                 if (runningAddOn.dependsOn(ext, ao)) {
727                     String classname = ext.getClass().getCanonicalName();
728                     AddOnInstaller.uninstallAddOnExtension(runningAddOn, ext, NULL_CALLBACK);
729                     try (AddOnClassLoader extensionClassLoader =
730                             (AddOnClassLoader) ext.getClass().getClassLoader()) {
731                         ext = null;
732                         entry.getValue().removeChildClassLoader(extensionClassLoader);
733                         extensionClassLoader.clearDependencies();
734                         ResourceBundle.clearCache(extensionClassLoader);
735                     } catch (Exception e) {
736                         logger.error(
737                                 "Failure while closing class loader of extension '"
738                                         + classname
739                                         + "':",
740                                 e);
741                     }
742                     runnableAddOns.get(runningAddOn).remove(classname);
743                     changed = true;
744                 }
745             }
746         }
747 
748         if (changed) {
749             saveAddOnsRunState(runnableAddOns);
750         }
751     }
752 
softUninstallDependentAddOns(AddOn ao)753     private void softUninstallDependentAddOns(AddOn ao) {
754         for (Entry<String, AddOnClassLoader> entry : new HashMap<>(addOnLoaders).entrySet()) {
755             AddOn runningAddOn = aoc.getAddOn(entry.getKey());
756             if (runningAddOn.dependsOn(ao)) {
757                 softUninstallDependentAddOns(runningAddOn);
758 
759                 softUninstall(runningAddOn);
760             }
761         }
762     }
763 
softUninstall(AddOn addOn)764     private void softUninstall(AddOn addOn) {
765         if (AddOn.InstallationStatus.INSTALLED != addOn.getInstallationStatus()) {
766             return;
767         }
768 
769         AddOn.InstallationStatus status;
770         if (isDynamicallyInstallable(addOn) && AddOnInstaller.softUninstall(addOn, NULL_CALLBACK)) {
771             removeAddOnClassLoader(addOn);
772             status = AddOn.InstallationStatus.NOT_INSTALLED;
773         } else {
774             status = AddOn.InstallationStatus.SOFT_UNINSTALLATION_FAILED;
775         }
776 
777         addOn.setInstallationStatus(status);
778         Control.getSingleton()
779                 .getExtensionLoader()
780                 .addOnSoftUninstalled(addOn, status == AddOn.InstallationStatus.NOT_INSTALLED);
781     }
782 
loadBlockList()783     private void loadBlockList() {
784         blockList = loadList(addOnsStateConfig, ADDONS_BLOCK_LIST);
785     }
786 
saveBlockList()787     private void saveBlockList() {
788         saveList(addOnsStateConfig, ADDONS_BLOCK_LIST, this.blockList);
789     }
790 
getClassNames(String packageName, Class<T> classType)791     private <T> List<ClassNameWrapper> getClassNames(String packageName, Class<T> classType) {
792         List<ClassNameWrapper> listClassName = new ArrayList<>();
793 
794         listClassName.addAll(this.getLocalClassNames(packageName));
795         for (String addOnId : this.addOnLoaders.keySet()) {
796             listClassName.addAll(this.getJarClassNames(aoc.getAddOn(addOnId), packageName));
797         }
798         for (File jar : jars) {
799             listClassName.addAll(
800                     this.getJarClassNames(this.getClass().getClassLoader(), jar, packageName));
801         }
802         return listClassName;
803     }
804 
805     /**
806      * Returns all the {@code Extension}s of all the installed add-ons.
807      *
808      * <p>The discovery of {@code Extension}s is done by resorting to the {@link
809      * AddOn#MANIFEST_FILE_NAME manifest file} bundled in the add-ons.
810      *
811      * <p>Extensions with unfulfilled dependencies are not be returned.
812      *
813      * @return a list containing all {@code Extension}s of all installed add-ons
814      * @since 2.4.0
815      * @see Extension
816      * @see #getExtensions(AddOn)
817      */
getExtensions()818     public List<Extension> getExtensions() {
819         List<Extension> list = new ArrayList<>();
820         for (AddOn addOn : getAddOnCollection().getAddOns()) {
821             list.addAll(getExtensions(addOn));
822         }
823 
824         return list;
825     }
826 
827     /**
828      * Returns all {@code Extension}s of the given {@code addOn}.
829      *
830      * <p>The discovery of {@code Extension}s is done by resorting to {@link
831      * AddOn#MANIFEST_FILE_NAME manifest file} bundled in the add-on.
832      *
833      * <p>Extensions with unfulfilled dependencies are not be returned.
834      *
835      * <p><strong>Note:</strong> If the add-on is not installed the method returns an empty list.
836      *
837      * @param addOn the add-on whose extensions will be returned
838      * @return a list containing the {@code Extension}s of the given {@code addOn}
839      * @since 2.4.0
840      * @see Extension
841      * @see #getExtensions()
842      */
getExtensions(AddOn addOn)843     public List<Extension> getExtensions(AddOn addOn) {
844         AddOnClassLoader addOnClassLoader = this.addOnLoaders.get(addOn.getId());
845         if (addOnClassLoader == null) {
846             return Collections.emptyList();
847         }
848 
849         List<Extension> extensions = new ArrayList<>();
850         extensions.addAll(loadAddOnExtensions(addOn, addOn.getExtensions(), addOnClassLoader));
851 
852         if (addOn.hasExtensionsWithDeps()) {
853             AddOn.AddOnRunRequirements reqs =
854                     addOn.calculateRunRequirements(aoc.getInstalledAddOns());
855             for (ExtensionRunRequirements extReqs : reqs.getExtensionRequirements()) {
856                 if (extReqs.isRunnable()) {
857                     List<AddOnClassLoader> dependencies =
858                             new ArrayList<>(extReqs.getDependencies().size());
859                     for (AddOn addOnDep : extReqs.getDependencies()) {
860                         dependencies.add(addOnLoaders.get(addOnDep.getId()));
861                     }
862                     AddOnClassLoader extAddOnClassLoader =
863                             new AddOnClassLoader(
864                                     addOnClassLoader,
865                                     dependencies,
866                                     addOn.getExtensionAddOnClassnames(extReqs.getClassname()));
867                     Extension ext =
868                             loadAddOnExtension(addOn, extReqs.getClassname(), extAddOnClassLoader);
869                     if (ext != null) {
870                         extensions.add(ext);
871                     }
872                 } else if (logger.isDebugEnabled()) {
873                     logger.debug(
874                             "Can't run extension '"
875                                     + extReqs.getClassname()
876                                     + "' of add-on '"
877                                     + addOn.getName()
878                                     + "' because of missing requirements: "
879                                     + AddOnRunIssuesUtils.getRunningIssues(extReqs));
880                 }
881             }
882         }
883         return extensions;
884     }
885 
loadAddOnExtensions( AddOn addOn, List<String> extensions, AddOnClassLoader addOnClassLoader)886     private List<Extension> loadAddOnExtensions(
887             AddOn addOn, List<String> extensions, AddOnClassLoader addOnClassLoader) {
888         if (extensions == null || extensions.isEmpty()) {
889             return Collections.emptyList();
890         }
891 
892         List<Extension> list = new ArrayList<>(extensions.size());
893         for (String extName : extensions) {
894             Extension ext = loadAddOnExtension(addOn, extName, addOnClassLoader);
895             if (ext != null) {
896                 list.add(ext);
897             }
898         }
899         return list;
900     }
901 
loadAddOnExtension( AddOn addOn, String classname, AddOnClassLoader addOnClassLoader)902     private static Extension loadAddOnExtension(
903             AddOn addOn, String classname, AddOnClassLoader addOnClassLoader) {
904         Extension extension =
905                 AddOnLoaderUtils.loadAndInstantiateClass(
906                         addOnClassLoader, classname, Extension.class, "extension");
907         if (extension != null) {
908             addOn.addLoadedExtension(extension);
909         }
910         return extension;
911     }
912 
913     /**
914      * Gets the active scan rules of all the loaded add-ons.
915      *
916      * <p>The discovery of active scan rules is done by resorting to {@link AddOn#MANIFEST_FILE_NAME
917      * manifest file} bundled in the add-ons.
918      *
919      * @return an unmodifiable {@code List} with all the active scan rules, never {@code null}
920      * @since 2.4.0
921      * @see AbstractPlugin
922      */
getActiveScanRules()923     public List<AbstractPlugin> getActiveScanRules() {
924         ArrayList<AbstractPlugin> list = new ArrayList<>();
925         for (AddOn addOn : getAddOnCollection().getAddOns()) {
926             AddOnClassLoader addOnClassLoader = this.addOnLoaders.get(addOn.getId());
927             if (addOnClassLoader != null) {
928                 list.addAll(AddOnLoaderUtils.getActiveScanRules(addOn, addOnClassLoader));
929             }
930         }
931         list.trimToSize();
932         return Collections.unmodifiableList(list);
933     }
934 
935     /**
936      * Gets the passive scan rules of all the loaded add-ons.
937      *
938      * <p>The discovery of passive scan rules is done by resorting to {@link
939      * AddOn#MANIFEST_FILE_NAME manifest file} bundled in the add-ons.
940      *
941      * @return an unmodifiable {@code List} with all the passive scan rules, never {@code null}
942      * @since 2.4.0
943      * @see PluginPassiveScanner
944      */
getPassiveScanRules()945     public List<PluginPassiveScanner> getPassiveScanRules() {
946         ArrayList<PluginPassiveScanner> list = new ArrayList<>();
947         for (AddOn addOn : getAddOnCollection().getAddOns()) {
948             AddOnClassLoader addOnClassLoader = this.addOnLoaders.get(addOn.getId());
949             if (addOnClassLoader != null) {
950                 list.addAll(AddOnLoaderUtils.getPassiveScanRules(addOn, addOnClassLoader));
951             }
952         }
953         list.trimToSize();
954         return Collections.unmodifiableList(list);
955     }
956 
957     /**
958      * Gets a list of classes that implement the given type in the given package.
959      *
960      * <p>It searches in the dependencies, add-ons, and the ZAP JAR.
961      *
962      * @param packageName the name of the package that the classes must be in.
963      * @param classType the type of the classes.
964      * @return a list with the classes that implement the given type, never {@code null}.
965      * @deprecated (2.8.0) The use of this method is discouraged (specially during ZAP startup, as
966      *     it's delayed), it's preferable to provide means to register/declare the required classes
967      *     instead of searching "everywhere".
968      */
969     @Deprecated
getImplementors(String packageName, Class<T> classType)970     public <T> List<T> getImplementors(String packageName, Class<T> classType) {
971         return this.getImplementors(null, packageName, classType);
972     }
973 
974     /**
975      * Gets a list of classes that implement the given type in the given package.
976      *
977      * <p>It searches in the given add-on, if not {@code null}, otherwise it searches in the
978      * dependencies, add-ons, and the ZAP JAR.
979      *
980      * @param ao the add-on to search in, might be {@code null}.
981      * @param packageName the name of the package that the classes must be in.
982      * @param classType the type of the classes.
983      * @return a list with the classes that implement the given type, never {@code null}.
984      * @deprecated (2.8.0) The use of this method is discouraged (specially during ZAP startup, as
985      *     it's delayed), it's preferable to provide means to register/declare the required classes
986      *     instead of searching "everywhere".
987      */
988     @Deprecated
getImplementors(AddOn ao, String packageName, Class<T> classType)989     public <T> List<T> getImplementors(AddOn ao, String packageName, Class<T> classType) {
990         Class<?> cls = null;
991         List<T> listClass = new ArrayList<>();
992 
993         List<ClassNameWrapper> classNames;
994         if (ao != null) {
995             classNames = this.getJarClassNames(ao, packageName);
996         } else {
997             classNames = this.getClassNames(packageName, classType);
998         }
999         for (ClassNameWrapper classWrapper : classNames) {
1000             try {
1001                 cls = classWrapper.getCl().loadClass(classWrapper.getClassName());
1002                 // abstract class or interface cannot be constructed.
1003                 if (Modifier.isAbstract(cls.getModifiers())
1004                         || Modifier.isInterface(cls.getModifiers())) {
1005                     continue;
1006                 }
1007                 if (classType.isAssignableFrom(cls)) {
1008                     @SuppressWarnings("unchecked")
1009                     Constructor<T> c = (Constructor<T>) cls.getConstructor();
1010                     listClass.add(c.newInstance());
1011                 }
1012             } catch (Throwable e) {
1013                 // Often not an error
1014                 logger.debug(e.getMessage(), e);
1015             }
1016         }
1017         return listClass;
1018     }
1019 
1020     /**
1021      * Check local jar (zap.jar) or related package if any target file is found.
1022      *
1023      * @param packageName the package name that the class must belong too
1024      * @return a {@code List} with all the classes belonging to the given package
1025      */
getLocalClassNames(String packageName)1026     private List<ClassNameWrapper> getLocalClassNames(String packageName) {
1027 
1028         if (packageName == null || packageName.equals("")) {
1029             return Collections.emptyList();
1030         }
1031 
1032         String folder = packageName.replace('.', '/');
1033         URL local = AddOnLoader.class.getClassLoader().getResource(folder);
1034         if (local == null) {
1035             return Collections.emptyList();
1036         }
1037         String jarFile = null;
1038         if (local.getProtocol().equals("jar")) {
1039             jarFile = local.toString().substring("jar:".length());
1040             int pos = jarFile.indexOf("!");
1041             jarFile = jarFile.substring(0, pos);
1042 
1043             try {
1044                 // ZAP: Changed to take into account the package name
1045                 return getJarClassNames(
1046                         this.getClass().getClassLoader(), new File(new URI(jarFile)), packageName);
1047             } catch (URISyntaxException e) {
1048                 logger.error(e.getMessage(), e);
1049             }
1050         } else {
1051             try {
1052                 // ZAP: Changed to pass a FileFilter (ClassRecurseDirFileFilter)
1053                 // and to pass the "packageName" with the dots already replaced.
1054                 return parseClassDir(
1055                         this.getClass().getClassLoader(),
1056                         new File(new URI(local.toString())),
1057                         packageName.replace('.', File.separatorChar),
1058                         new ClassRecurseDirFileFilter(true));
1059             } catch (URISyntaxException e) {
1060                 logger.error(e.getMessage(), e);
1061             }
1062         }
1063         return Collections.emptyList();
1064     }
1065 
1066     // ZAP: Changed to use only one FileFilter and the packageName is already
1067     // passed with the dots replaced.
parseClassDir( ClassLoader cl, File file, String packageName, FileFilter fileFilter)1068     private List<ClassNameWrapper> parseClassDir(
1069             ClassLoader cl, File file, String packageName, FileFilter fileFilter) {
1070         List<ClassNameWrapper> classNames = new ArrayList<>();
1071         File[] listFile = file.listFiles(fileFilter);
1072 
1073         for (File entry : listFile) {
1074             if (entry.isDirectory()) {
1075                 classNames.addAll(parseClassDir(cl, entry, packageName, fileFilter));
1076                 continue;
1077             }
1078             String fileName = entry.toString();
1079             int pos = fileName.indexOf(packageName);
1080             if (pos > 0) {
1081                 String className =
1082                         fileName.substring(pos)
1083                                 .replaceAll("\\.class$", "")
1084                                 .replace(File.separatorChar, '.');
1085                 classNames.add(new ClassNameWrapper(cl, className));
1086             }
1087         }
1088         return classNames;
1089     }
1090 
1091     // ZAP: Added to take into account the package name
getJarClassNames(ClassLoader cl, File file, String packageName)1092     private List<ClassNameWrapper> getJarClassNames(ClassLoader cl, File file, String packageName) {
1093         List<ClassNameWrapper> classNames = new ArrayList<>();
1094         ZipEntry entry = null;
1095         String className = "";
1096         try (JarFile jarFile = new JarFile(file)) {
1097             Enumeration<JarEntry> entries = jarFile.entries();
1098             while (entries.hasMoreElements()) {
1099                 entry = entries.nextElement();
1100                 if (entry.isDirectory() || !entry.getName().endsWith(".class")) {
1101                     continue;
1102                 }
1103                 className = entry.toString().replaceAll("\\.class$", "").replaceAll("/", ".");
1104                 if (className.indexOf(packageName) >= 0) {
1105                     classNames.add(new ClassNameWrapper(cl, className));
1106                 }
1107             }
1108         } catch (Exception e) {
1109             logger.error("Failed to open file: " + file.getAbsolutePath(), e);
1110         }
1111         return classNames;
1112     }
1113 
getJarClassNames(AddOn ao, String packageName)1114     private List<ClassNameWrapper> getJarClassNames(AddOn ao, String packageName) {
1115         List<ClassNameWrapper> classNames = new ArrayList<>();
1116         ZipEntry entry = null;
1117         String className = "";
1118         try (JarFile jarFile = new JarFile(ao.getFile())) {
1119             Enumeration<JarEntry> entries = jarFile.entries();
1120             while (entries.hasMoreElements()) {
1121                 entry = entries.nextElement();
1122                 if (entry.isDirectory() || !entry.getName().endsWith(".class")) {
1123                     continue;
1124                 }
1125                 className = entry.toString().replaceAll("\\.class$", "").replaceAll("/", ".");
1126                 if (className.indexOf(packageName) >= 0) {
1127                     classNames.add(
1128                             new ClassNameWrapper(this.addOnLoaders.get(ao.getId()), className));
1129                 }
1130             }
1131         } catch (Exception e) {
1132             logger.error("Failed to open file: " + ao.getFile().getAbsolutePath(), e);
1133         }
1134         return classNames;
1135     }
1136 
1137     private static final class JarFilenameFilter implements FilenameFilter {
1138         @Override
accept(File dir, String fileName)1139         public boolean accept(File dir, String fileName) {
1140             if (fileName.endsWith(".jar")) {
1141                 return true;
1142             }
1143             return false;
1144         }
1145     }
1146 
1147     // ZAP: Added
1148     private static final class ClassRecurseDirFileFilter implements FileFilter {
1149 
1150         private boolean recurse;
1151 
ClassRecurseDirFileFilter(boolean recurse)1152         public ClassRecurseDirFileFilter(boolean recurse) {
1153             this.recurse = recurse;
1154         }
1155 
1156         @Override
accept(File file)1157         public boolean accept(File file) {
1158             if (recurse && file.isDirectory() && !file.getName().startsWith(".")) {
1159                 return true;
1160             } else if (file.isFile() && file.getName().endsWith(".class")) {
1161                 return true;
1162             }
1163 
1164             return false;
1165         }
1166     }
1167 
1168     private class ClassNameWrapper {
1169         private ClassLoader cl;
1170         private String className;
1171 
ClassNameWrapper(ClassLoader cl, String className)1172         public ClassNameWrapper(ClassLoader cl, String className) {
1173             super();
1174             this.cl = cl;
1175             this.className = className;
1176         }
1177 
getCl()1178         public ClassLoader getCl() {
1179             return cl;
1180         }
1181 
getClassName()1182         public String getClassName() {
1183             return className;
1184         }
1185     }
1186 
loadList(Configuration config, String key)1187     private static List<String> loadList(Configuration config, String key) {
1188         List<String> data = new ArrayList<>();
1189         String blockStr = config.getString(key, null);
1190         if (blockStr != null && blockStr.length() > 0) {
1191             for (String str : blockStr.split(",")) {
1192                 data.add(str);
1193             }
1194         }
1195         return data;
1196     }
1197 
saveList(FileConfiguration config, String key, List<String> list)1198     private static void saveList(FileConfiguration config, String key, List<String> list) {
1199         StringBuilder sb = new StringBuilder();
1200 
1201         for (String id : list) {
1202             if (sb.length() > 0) {
1203                 sb.append(',');
1204             }
1205             sb.append(id);
1206         }
1207 
1208         config.setProperty(key, sb.toString());
1209         try {
1210             config.save();
1211         } catch (ConfigurationException e) {
1212             logger.error("Failed to save list [" + key + "]: " + sb.toString(), e);
1213         }
1214     }
1215 
loadAddOnsRunState( HierarchicalConfiguration config, AddOnCollection addOnCollection)1216     private static Map<AddOn, AddOnRunState> loadAddOnsRunState(
1217             HierarchicalConfiguration config, AddOnCollection addOnCollection) {
1218         List<HierarchicalConfiguration> savedAddOns = config.configurationsAt(ADDONS_RUNNABLE_KEY);
1219 
1220         Map<AddOn, AddOnRunState> runnableAddOns = new HashMap<>();
1221         for (HierarchicalConfiguration savedAddOn : savedAddOns) {
1222             AddOn addOn = addOnCollection.getAddOn(savedAddOn.getString(ADDON_RUNNABLE_ID_KEY, ""));
1223             if (addOn == null) {
1224                 // No longer exists, skip it.
1225                 continue;
1226             }
1227             String version = savedAddOn.getString(ADDON_RUNNABLE_FULL_VERSION_KEY, "");
1228             if (version.isEmpty()) {
1229                 // Try read the old version, which was an integer.
1230                 version = savedAddOn.getString(ADDON_RUNNABLE_VERSION_KEY, "");
1231             }
1232             if (version.isEmpty()) {
1233                 // No version, skip it.
1234                 continue;
1235             }
1236 
1237             int result =
1238                     addOn.getVersion().compareTo(createLegacyVersion(version, addOn.getName()));
1239             if (result != 0) {
1240                 if (result > 1) {
1241                     runnableAddOns.put(addOn, new AddOnRunState());
1242                 }
1243                 // Different version, nothing more to do.
1244                 continue;
1245             }
1246 
1247             List<String> runnableExtensions = new ArrayList<>();
1248             List<String> currentExtensions = addOn.getExtensionsWithDeps();
1249             for (String savedExtension :
1250                     savedAddOn.getStringArray(ADDON_RUNNABLE_ALL_EXTENSIONS_KEY)) {
1251                 if (currentExtensions.contains(savedExtension)) {
1252                     runnableExtensions.add(savedExtension);
1253                 }
1254             }
1255             runnableAddOns.put(addOn, new AddOnRunState(runnableExtensions));
1256         }
1257 
1258         return runnableAddOns;
1259     }
1260 
createLegacyVersion(String version, String addOnName)1261     private static Version createLegacyVersion(String version, String addOnName) {
1262         try {
1263             return new Version(version);
1264         } catch (IllegalArgumentException e) {
1265             if (logger.isDebugEnabled()) {
1266                 logger.debug(
1267                         "Failed to create (legacy?) version with ["
1268                                 + version
1269                                 + "] for runnable add-on ["
1270                                 + addOnName
1271                                 + "]",
1272                         e);
1273             }
1274         }
1275 
1276         try {
1277             return new Version(version + ".0.0");
1278         } catch (IllegalArgumentException e) {
1279             if (logger.isDebugEnabled()) {
1280                 logger.debug(
1281                         "Failed to create legacy version with ["
1282                                 + version
1283                                 + ".0.0] for runnable add-on ["
1284                                 + addOnName
1285                                 + "]",
1286                         e);
1287             }
1288         }
1289 
1290         return null;
1291     }
1292 
saveAddOnsRunState(Map<AddOn, List<String>> runnableAddOns)1293     private void saveAddOnsRunState(Map<AddOn, List<String>> runnableAddOns) {
1294         addOnsStateConfig.clearTree(ADDONS_RUNNABLE_BASE_KEY);
1295 
1296         int i = 0;
1297         for (Map.Entry<AddOn, List<String>> runnableAddOnEntry : runnableAddOns.entrySet()) {
1298             String elementBaseKey = ADDONS_RUNNABLE_KEY + "(" + i + ").";
1299             AddOn addOn = runnableAddOnEntry.getKey();
1300 
1301             addOnsStateConfig.setProperty(elementBaseKey + ADDON_RUNNABLE_ID_KEY, addOn.getId());
1302             addOnsStateConfig.setProperty(
1303                     elementBaseKey + ADDON_RUNNABLE_FULL_VERSION_KEY, addOn.getVersion());
1304             // For older ZAP versions, which can't read the semantic version, just an integer.
1305             addOnsStateConfig.setProperty(
1306                     elementBaseKey + ADDON_RUNNABLE_VERSION_KEY,
1307                     addOn.getVersion().getMajorVersion());
1308 
1309             String extensionBaseKey = elementBaseKey + ADDON_RUNNABLE_ALL_EXTENSIONS_KEY;
1310             for (String extension : runnableAddOnEntry.getValue()) {
1311                 addOnsStateConfig.addProperty(extensionBaseKey, extension);
1312             }
1313 
1314             i++;
1315         }
1316 
1317         try {
1318             addOnsStateConfig.save();
1319         } catch (ConfigurationException e) {
1320             logger.error("Failed to save state of runnable add-ons:", e);
1321         }
1322     }
1323 
migrateOldAddOnsState(ZapXmlConfiguration newConfig)1324     private static boolean migrateOldAddOnsState(ZapXmlConfiguration newConfig) {
1325         boolean dataMigrated = false;
1326         HierarchicalConfiguration oldConfig =
1327                 (HierarchicalConfiguration) Model.getSingleton().getOptionsParam().getConfig();
1328 
1329         if (oldConfig.containsKey(ADDONS_BLOCK_LIST)) {
1330             List<String> blockList = loadList(oldConfig, ADDONS_BLOCK_LIST);
1331             oldConfig.clearProperty(ADDONS_BLOCK_LIST);
1332             saveList(newConfig, ADDONS_BLOCK_LIST, blockList);
1333             dataMigrated = true;
1334         }
1335 
1336         List<HierarchicalConfiguration> oldAddOnsState =
1337                 oldConfig.configurationsAt(ADDONS_RUNNABLE_KEY);
1338         if (!oldAddOnsState.isEmpty()) {
1339             int i = 0;
1340             for (HierarchicalConfiguration savedAddOn : oldAddOnsState) {
1341                 String elementBaseKey = ADDONS_RUNNABLE_KEY + "(" + i + ").";
1342                 newConfig.setProperty(
1343                         elementBaseKey + ADDON_RUNNABLE_ID_KEY,
1344                         savedAddOn.getString(ADDON_RUNNABLE_ID_KEY, ""));
1345                 String version = savedAddOn.getString(ADDON_RUNNABLE_FULL_VERSION_KEY, "");
1346                 if (version.isEmpty()) {
1347                     newConfig.setProperty(
1348                             elementBaseKey + ADDON_RUNNABLE_VERSION_KEY,
1349                             savedAddOn.getString(ADDON_RUNNABLE_VERSION_KEY, ""));
1350                 } else {
1351                     newConfig.setProperty(
1352                             elementBaseKey + ADDON_RUNNABLE_FULL_VERSION_KEY, version);
1353                 }
1354 
1355                 String extensionBaseKey = elementBaseKey + ADDON_RUNNABLE_ALL_EXTENSIONS_KEY;
1356                 for (String extension :
1357                         savedAddOn.getStringArray(ADDON_RUNNABLE_ALL_EXTENSIONS_KEY)) {
1358                     newConfig.addProperty(extensionBaseKey, extension);
1359                 }
1360             }
1361             oldConfig.clearTree(ADDONS_RUNNABLE_KEY);
1362             dataMigrated = true;
1363         }
1364         return dataMigrated;
1365     }
1366 
1367     /**
1368      * An {@code UninstallationProgressCallback} that does nothing. A "{@code null}" object, for use
1369      * when no callback is given during the uninstallation process.
1370      */
1371     private static class NullUninstallationProgressCallBack
1372             implements AddOnUninstallationProgressCallback {
1373 
1374         @Override
uninstallingAddOn(AddOn addOn, boolean updating)1375         public void uninstallingAddOn(AddOn addOn, boolean updating) {}
1376 
1377         @Override
activeScanRulesWillBeRemoved(int numberOfRules)1378         public void activeScanRulesWillBeRemoved(int numberOfRules) {}
1379 
1380         @Override
activeScanRuleRemoved(String name)1381         public void activeScanRuleRemoved(String name) {}
1382 
1383         @Override
passiveScanRulesWillBeRemoved(int numberOfRules)1384         public void passiveScanRulesWillBeRemoved(int numberOfRules) {}
1385 
1386         @Override
passiveScanRuleRemoved(String name)1387         public void passiveScanRuleRemoved(String name) {}
1388 
1389         @Override
filesWillBeRemoved(int numberOfFiles)1390         public void filesWillBeRemoved(int numberOfFiles) {}
1391 
1392         @Override
fileRemoved()1393         public void fileRemoved() {}
1394 
1395         @Override
extensionsWillBeRemoved(int numberOfExtensions)1396         public void extensionsWillBeRemoved(int numberOfExtensions) {}
1397 
1398         @Override
extensionRemoved(String name)1399         public void extensionRemoved(String name) {}
1400 
1401         @Override
addOnUninstalled(boolean uninstalled)1402         public void addOnUninstalled(boolean uninstalled) {}
1403     }
1404 
1405     private static class AddOnRunState {
1406 
1407         private final boolean newerVersion;
1408         private final List<String> extensions;
1409 
AddOnRunState()1410         public AddOnRunState() {
1411             this.newerVersion = true;
1412             this.extensions = Collections.emptyList();
1413         }
1414 
AddOnRunState(List<String> extensions)1415         public AddOnRunState(List<String> extensions) {
1416             this.newerVersion = false;
1417             this.extensions = extensions;
1418         }
1419 
hasNewerVersion()1420         public boolean hasNewerVersion() {
1421             return newerVersion;
1422         }
1423 
getExtensions()1424         public List<String> getExtensions() {
1425             return extensions;
1426         }
1427     }
1428 }
1429