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.io.File;
23 import java.io.IOException;
24 import java.io.InputStream;
25 import java.net.MalformedURLException;
26 import java.net.URL;
27 import java.nio.file.Files;
28 import java.nio.file.Path;
29 import java.util.ArrayList;
30 import java.util.Arrays;
31 import java.util.Collection;
32 import java.util.Collections;
33 import java.util.HashSet;
34 import java.util.List;
35 import java.util.Locale;
36 import java.util.Objects;
37 import java.util.Optional;
38 import java.util.ResourceBundle;
39 import java.util.Set;
40 import java.util.stream.Collectors;
41 import java.util.zip.ZipEntry;
42 import java.util.zip.ZipException;
43 import java.util.zip.ZipFile;
44 import org.apache.commons.configuration.SubnodeConfiguration;
45 import org.apache.commons.lang.ArrayUtils;
46 import org.apache.commons.lang.StringUtils;
47 import org.apache.commons.lang.SystemUtils;
48 import org.apache.commons.lang3.exception.ExceptionUtils;
49 import org.apache.logging.log4j.LogManager;
50 import org.apache.logging.log4j.Logger;
51 import org.jgrapht.DirectedGraph;
52 import org.jgrapht.alg.CycleDetector;
53 import org.jgrapht.graph.DefaultDirectedGraph;
54 import org.jgrapht.graph.DefaultEdge;
55 import org.jgrapht.traverse.TopologicalOrderIterator;
56 import org.parosproxy.paros.Constant;
57 import org.parosproxy.paros.core.scanner.AbstractPlugin;
58 import org.parosproxy.paros.extension.Extension;
59 import org.zaproxy.zap.Version;
60 import org.zaproxy.zap.control.BaseZapAddOnXmlData.AddOnDep;
61 import org.zaproxy.zap.control.BaseZapAddOnXmlData.Dependencies;
62 import org.zaproxy.zap.control.BaseZapAddOnXmlData.ExtensionWithDeps;
63 import org.zaproxy.zap.extension.pscan.PluginPassiveScanner;
64 
65 public class AddOn {
66 
67     /**
68      * The name of the manifest file, contained in the add-ons.
69      *
70      * <p>The manifest is expected to be in the root of the ZIP file.
71      *
72      * @since 2.6.0
73      */
74     public static final String MANIFEST_FILE_NAME = "ZapAddOn.xml";
75 
76     public enum Status {
77         unknown,
78         example,
79         alpha,
80         beta,
81         weekly,
82         release
83     }
84 
85     private static ZapRelease v2_4 = new ZapRelease("2.4.0");
86 
87     /**
88      * The installation status of the add-on.
89      *
90      * @since 2.4.0
91      */
92     public enum InstallationStatus {
93 
94         /**
95          * The add-on is available for installation, for example, an add-on in the marketplace (even
96          * if it requires previous actions, in this case, download the file).
97          */
98         AVAILABLE,
99 
100         /**
101          * The add-on was not (yet) installed. For example, the add-on is available in the 'plugin'
102          * directory but it's missing a dependency or requires a greater Java version. It's also in
103          * this status while a dependency is being updated.
104          */
105         NOT_INSTALLED,
106 
107         /** The add-on is installed. */
108         INSTALLED,
109 
110         /** The add-on is being downloaded. */
111         DOWNLOADING,
112 
113         /**
114          * The uninstallation of the add-on failed. For example, when the add-on is not dynamically
115          * installable or when an {@code Exception} is thrown during the uninstallation.
116          */
117         UNINSTALLATION_FAILED,
118 
119         /**
120          * The soft uninstallation of the add-on failed. It's in this status when the uninstallation
121          * failed for an update of a dependency.
122          */
123         SOFT_UNINSTALLATION_FAILED
124     }
125 
126     /**
127      * The result of checking if a file is a valid add-on.
128      *
129      * @since 2.8.0
130      * @see AddOn#isValidAddOn(Path)
131      */
132     public static class ValidationResult {
133 
134         /** The validity of the add-on. */
135         public enum Validity {
136             /**
137              * The add-on is valid.
138              *
139              * <p>The result contains the {@link ValidationResult#getManifest() manifest} of the
140              * add-on.
141              */
142             VALID,
143             /** The file path is not valid. */
144             INVALID_PATH,
145             /** The file does not have the expected extension {@value #FILE_EXTENSION}. */
146             INVALID_FILE_NAME,
147             /** The file is not a regular file or is not readable. */
148             FILE_NOT_READABLE,
149             /**
150              * There was an error reading the ZIP file.
151              *
152              * <p>The result contains the {@link ValidationResult#getException() exception}.
153              */
154             UNREADABLE_ZIP_FILE,
155             /**
156              * There was an error reading the file.
157              *
158              * <p>The result contains the {@link ValidationResult#getException() exception}.
159              */
160             IO_ERROR_FILE,
161             /** The ZIP file does not have the add-on manifest, {@value #MANIFEST_FILE_NAME}. */
162             MISSING_MANIFEST,
163             /**
164              * The manifest is not valid.
165              *
166              * <p>The result contains the {@link ValidationResult#getException() exception}.
167              */
168             INVALID_MANIFEST,
169             /** The add-on declared a missing/invalid library. */
170             INVALID_LIB,
171         }
172 
173         private final Validity validity;
174         private final Exception exception;
175         private final ZapAddOnXmlFile manifest;
176 
ValidationResult(Validity validity)177         private ValidationResult(Validity validity) {
178             this(validity, null);
179         }
180 
ValidationResult(ZapAddOnXmlFile manifest)181         private ValidationResult(ZapAddOnXmlFile manifest) {
182             this(Validity.VALID, null, manifest);
183         }
184 
ValidationResult(Validity validity, Exception exception)185         private ValidationResult(Validity validity, Exception exception) {
186             this(validity, exception, null);
187         }
188 
ValidationResult(Validity validity, Exception exception, ZapAddOnXmlFile manifest)189         private ValidationResult(Validity validity, Exception exception, ZapAddOnXmlFile manifest) {
190             this.validity = validity;
191             this.exception = exception;
192             this.manifest = manifest;
193         }
194 
195         /**
196          * Gets the validity of the add-on.
197          *
198          * @return the validity of the add-on.
199          */
getValidity()200         public Validity getValidity() {
201             return validity;
202         }
203 
204         /**
205          * Gets the exception that occurred while validating the file.
206          *
207          * @return the exception, or {@code null} if none.
208          */
getException()209         public Exception getException() {
210             return exception;
211         }
212 
213         /**
214          * Gets the manifest of the add-on.
215          *
216          * @return the manifest of the add-on, if valid, {@code null} otherwise.
217          */
getManifest()218         public ZapAddOnXmlFile getManifest() {
219             return manifest;
220         }
221     }
222 
223     /**
224      * The file extension of ZAP add-ons.
225      *
226      * @since 2.6.0
227      */
228     public static final String FILE_EXTENSION = ".zap";
229 
230     private String id;
231     private String name;
232     private String description = "";
233     private String author = "";
234     /**
235      * The version declared in the manifest file.
236      *
237      * <p>Never {@code null}.
238      */
239     private Version version;
240     /**
241      * The (semantic) version declared in the manifest file, to be replaced by {@link #version}.
242      *
243      * <p>Might be {@code null}.
244      */
245     private Version semVer;
246 
247     private Status status;
248     private String changes = "";
249     private File file = null;
250     private URL url = null;
251     private URL info = null;
252     private URL repo;
253     private long size = 0;
254     private boolean hasZapAddOnEntry = false;
255 
256     /**
257      * Flag that indicates if the manifest was read (or attempted to). Allows to prevent reading the
258      * manifest a second time when the add-on file is corrupt.
259      */
260     private boolean manifestRead;
261 
262     private String notBeforeVersion = null;
263     private String notFromVersion = null;
264     private String hash = null;
265     private String releaseDate;
266 
267     /**
268      * The installation status of the add-on.
269      *
270      * <p>Default is {@code NOT_INSTALLED}.
271      *
272      * @see InstallationStatus#NOT_INSTALLED
273      */
274     private InstallationStatus installationStatus = InstallationStatus.NOT_INSTALLED;
275 
276     private List<String> extensions = Collections.emptyList();
277     private List<ExtensionWithDeps> extensionsWithDeps = Collections.emptyList();
278 
279     /**
280      * The extensions of the add-on that were loaded.
281      *
282      * <p>This instance variable is lazy initialised.
283      *
284      * @see #addLoadedExtension(Extension)
285      * @see #removeLoadedExtension(Extension)
286      */
287     private List<Extension> loadedExtensions;
288 
289     private List<String> ascanrules = Collections.emptyList();
290     private List<AbstractPlugin> loadedAscanrules = Collections.emptyList();
291     private boolean loadedAscanRulesSet;
292     private List<String> pscanrules = Collections.emptyList();
293     private List<PluginPassiveScanner> loadedPscanrules = Collections.emptyList();
294     private boolean loadedPscanRulesSet;
295     private List<String> files = Collections.emptyList();
296     private List<Lib> libs = Collections.emptyList();
297 
298     private AddOnClassnames addOnClassnames = AddOnClassnames.ALL_ALLOWED;
299 
300     private ClassLoader classLoader;
301 
302     private Dependencies dependencies;
303 
304     /**
305      * The data of the bundle declared in the manifest.
306      *
307      * <p>Never {@code null}.
308      */
309     private BundleData bundleData = BundleData.EMPTY_BUNDLE;
310 
311     /**
312      * The resource bundle from the declaration in the manifest.
313      *
314      * <p>Might be {@code null}, if not declared.
315      */
316     private ResourceBundle resourceBundle;
317 
318     /**
319      * The data for the HelpSet, declared in the manifest.
320      *
321      * <p>Never {@code null}.
322      */
323     private HelpSetData helpSetData = HelpSetData.EMPTY_HELP_SET;
324 
325     private static final Logger logger = LogManager.getLogger(AddOn.class);
326 
327     /**
328      * Tells whether or not the given file name matches the name of a ZAP add-on.
329      *
330      * <p>The file name must have the format "{@code <id>-<status>-<version>.zap}". The {@code id}
331      * is a string, the {@code status} must be a value from {@link Status} and the {@code version}
332      * must be an integer.
333      *
334      * @param fileName the name of the file to check
335      * @return {@code true} if the given file name is the name of an add-on, {@code false}
336      *     otherwise.
337      * @deprecated (2.6.0) Use {@link #isAddOnFileName(String)} instead, the checks done in this
338      *     method are more strict than it needs to.
339      * @see #isAddOnFileName(String)
340      */
341     @Deprecated
isAddOn(String fileName)342     public static boolean isAddOn(String fileName) {
343         if (!isAddOnFileName(fileName)) {
344             return false;
345         }
346         if (fileName.substring(0, fileName.indexOf(".")).split("-").length < 3) {
347             return false;
348         }
349         String[] strArray = fileName.substring(0, fileName.indexOf(".")).split("-");
350         try {
351             Status.valueOf(strArray[1]);
352             Integer.parseInt(strArray[2]);
353         } catch (Exception e) {
354             return false;
355         }
356 
357         return true;
358     }
359 
360     /**
361      * Tells whether or not the given file name matches the name of a ZAP add-on.
362      *
363      * <p>The file name must have as file extension {@link #FILE_EXTENSION}.
364      *
365      * @param fileName the name of the file to check
366      * @return {@code true} if the given file name is the name of an add-on, {@code false}
367      *     otherwise.
368      * @since 2.6.0
369      */
isAddOnFileName(String fileName)370     public static boolean isAddOnFileName(String fileName) {
371         if (fileName == null) {
372             return false;
373         }
374         return fileName.toLowerCase(Locale.ROOT).endsWith(FILE_EXTENSION);
375     }
376 
377     /**
378      * Tells whether or not the given file is an add-on.
379      *
380      * @param f the file to be checked
381      * @return {@code true} if the given file is an add-on, {@code false} otherwise.
382      * @deprecated (2.6.0) Use {@link #isAddOn(Path)} instead.
383      */
384     @Deprecated
isAddOn(File f)385     public static boolean isAddOn(File f) {
386         return isAddOn(f.toPath());
387     }
388 
389     /**
390      * Tells whether or not the given file is a ZAP add-on.
391      *
392      * <p>An add-on is a ZIP file that contains, at least, a {@value #MANIFEST_FILE_NAME} file.
393      *
394      * @param file the file to be checked
395      * @return {@code true} if the given file is an add-on, {@code false} otherwise.
396      * @since 2.6.0
397      * @see #isAddOnFileName(String)
398      * @see #isValidAddOn(Path)
399      */
isAddOn(Path file)400     public static boolean isAddOn(Path file) {
401         return isValidAddOn(file).getValidity() == ValidationResult.Validity.VALID;
402     }
403 
404     /**
405      * Tells whether or not the given file is a ZAP add-on.
406      *
407      * @param file the file to be checked.
408      * @return the result of the validation.
409      * @since 2.8.0
410      * @see #isAddOn(Path)
411      */
isValidAddOn(Path file)412     public static ValidationResult isValidAddOn(Path file) {
413         if (file == null || file.getNameCount() == 0) {
414             return new ValidationResult(ValidationResult.Validity.INVALID_PATH);
415         }
416 
417         if (!isAddOnFileName(file.getFileName().toString())) {
418             return new ValidationResult(ValidationResult.Validity.INVALID_FILE_NAME);
419         }
420 
421         if (!Files.isRegularFile(file) || !Files.isReadable(file)) {
422             return new ValidationResult(ValidationResult.Validity.FILE_NOT_READABLE);
423         }
424 
425         try (ZipFile zip = new ZipFile(file.toFile())) {
426             ZipEntry manifestEntry = zip.getEntry(MANIFEST_FILE_NAME);
427             if (manifestEntry == null) {
428                 return new ValidationResult(ValidationResult.Validity.MISSING_MANIFEST);
429             }
430 
431             try (InputStream zis = zip.getInputStream(manifestEntry)) {
432                 ZapAddOnXmlFile manifest;
433                 try {
434                     manifest = new ZapAddOnXmlFile(zis);
435                 } catch (IOException e) {
436                     return new ValidationResult(ValidationResult.Validity.INVALID_MANIFEST, e);
437                 }
438 
439                 if (hasInvalidLibs(file, manifest, zip)) {
440                     return new ValidationResult(ValidationResult.Validity.INVALID_LIB);
441                 }
442                 return new ValidationResult(manifest);
443             }
444         } catch (ZipException e) {
445             return new ValidationResult(ValidationResult.Validity.UNREADABLE_ZIP_FILE, e);
446         } catch (Exception e) {
447             return new ValidationResult(ValidationResult.Validity.IO_ERROR_FILE, e);
448         }
449     }
450 
hasInvalidLibs(Path file, ZapAddOnXmlFile manifest, ZipFile zip)451     private static boolean hasInvalidLibs(Path file, ZapAddOnXmlFile manifest, ZipFile zip) {
452         return manifest.getLibs().stream()
453                 .anyMatch(
454                         e -> {
455                             ZipEntry libEntry = zip.getEntry(e);
456                             if (libEntry == null) {
457                                 logger.warn("The add-on " + file + " does not have the lib: " + e);
458                                 return true;
459                             }
460                             if (libEntry.isDirectory()) {
461                                 logger.warn(
462                                         "The add-on " + file + " does not have a file lib: " + e);
463                                 return true;
464                             }
465                             return false;
466                         });
467     }
468 
469     /**
470      * Convenience method that attempts to create an {@code AddOn} from the given file.
471      *
472      * <p>A warning is logged if the add-on is invalid and an error if an error occurred while
473      * creating it.
474      *
475      * @param file the file of the add-on.
476      * @return a container with the {@code AddOn}, or empty if not valid or an error occurred while
477      *     creating it.
478      * @since 2.8.0
479      */
createAddOn(Path file)480     public static Optional<AddOn> createAddOn(Path file) {
481         try {
482             return Optional.of(new AddOn(file));
483         } catch (AddOn.InvalidAddOnException e) {
484             String logMessage = "Invalid add-on: " + file.toString() + ".";
485             if (logger.isDebugEnabled() || Constant.isDevMode()) {
486                 logger.warn(logMessage, e);
487             } else {
488                 logger.warn(logMessage + " " + e.getMessage());
489             }
490         } catch (Exception e) {
491             logger.error("Failed to create an add-on from: " + file.toString(), e);
492         }
493         return Optional.empty();
494     }
495 
496     /**
497      * Constructs an {@code AddOn} with the given file name.
498      *
499      * @param fileName the file name of the add-on
500      * @throws Exception if the file name is not valid.
501      * @deprecated (2.6.0) Use {@link #AddOn(Path)} instead.
502      */
503     @Deprecated
AddOn(String fileName)504     public AddOn(String fileName) throws Exception {
505         // Format is <name>-<status>-<version>.zap
506         if (!isAddOn(fileName)) {
507             throw new Exception("Invalid ZAP add-on file " + fileName);
508         }
509         String[] strArray = fileName.substring(0, fileName.indexOf(".")).split("-");
510         this.id = strArray[0];
511         this.name = this.id; // Will be overridden if theres a ZapAddOn.xml file
512         this.status = Status.valueOf(strArray[1]);
513         this.version = new Version(Integer.parseInt(strArray[2]) + ".0.0");
514     }
515 
516     /**
517      * Constructs an {@code AddOn} from the given {@code file}.
518      *
519      * <p>The {@value #MANIFEST_FILE_NAME} ZIP file entry is read after validating that the add-on
520      * has a valid add-on file name.
521      *
522      * <p>The installation status of the add-on is 'not installed'.
523      *
524      * @param file the file of the add-on
525      * @throws Exception if the given {@code file} does not exist, does not have a valid add-on file
526      *     name or an error occurred while reading the {@code value #ZAP_ADD_ON_XML} ZIP file entry
527      * @deprecated (2.6.0) Use {@link #AddOn(Path)} instead.
528      */
529     @Deprecated
AddOn(File file)530     public AddOn(File file) throws Exception {
531         this(file.toPath());
532     }
533 
534     /**
535      * Constructs an {@code AddOn} from the given {@code file}.
536      *
537      * <p>The {@value #MANIFEST_FILE_NAME} ZIP file entry is read after validating that the add-on
538      * is a valid add-on.
539      *
540      * <p>The installation status of the add-on is 'not installed'.
541      *
542      * @param file the file of the add-on
543      * @throws InvalidAddOnException (since 2.8.0) if the given {@code file} does not exist, does
544      *     not have a valid add-on file name, or an error occurred while reading the add-on manifest
545      *     ({@value #MANIFEST_FILE_NAME}).
546      * @throws IOException if an error occurred while reading/validating the file.
547      * @see #isAddOn(Path)
548      */
AddOn(Path file)549     public AddOn(Path file) throws IOException {
550         ValidationResult result = isValidAddOn(file);
551         if (result.getValidity() != ValidationResult.Validity.VALID) {
552             throw new InvalidAddOnException(result);
553         }
554         this.id = extractAddOnId(file.getFileName().toString());
555         this.file = file.toFile();
556         readZapAddOnXmlFile(result.getManifest());
557     }
558 
extractAddOnId(String fileName)559     private static String extractAddOnId(String fileName) {
560         return fileName.substring(0, fileName.lastIndexOf('.')).split("-")[0];
561     }
562 
loadManifestFile()563     private void loadManifestFile() throws IOException {
564         manifestRead = true;
565         if (file.exists()) {
566             // Might not exist in the tests
567             try (ZipFile zip = new ZipFile(file)) {
568                 ZipEntry zapAddOnEntry = zip.getEntry(MANIFEST_FILE_NAME);
569                 if (zapAddOnEntry == null) {
570                     throw new IOException(
571                             "Add-on does not have the " + MANIFEST_FILE_NAME + " file.");
572                 }
573 
574                 try (InputStream zis = zip.getInputStream(zapAddOnEntry)) {
575                     ZapAddOnXmlFile zapAddOnXml = new ZapAddOnXmlFile(zis);
576                     readZapAddOnXmlFile(zapAddOnXml);
577                 }
578             }
579         }
580     }
581 
readZapAddOnXmlFile(ZapAddOnXmlFile zapAddOnXml)582     private void readZapAddOnXmlFile(ZapAddOnXmlFile zapAddOnXml) {
583         this.name = zapAddOnXml.getName();
584         this.version = zapAddOnXml.getVersion();
585         this.semVer = zapAddOnXml.getSemVer();
586         this.status = AddOn.Status.valueOf(zapAddOnXml.getStatus());
587         this.description = zapAddOnXml.getDescription();
588         this.changes = zapAddOnXml.getChanges();
589         this.author = zapAddOnXml.getAuthor();
590         this.notBeforeVersion = zapAddOnXml.getNotBeforeVersion();
591         this.notFromVersion = zapAddOnXml.getNotFromVersion();
592         this.dependencies = zapAddOnXml.getDependencies();
593         this.info = createUrl(zapAddOnXml.getUrl());
594         this.repo = createUrl(zapAddOnXml.getRepo());
595 
596         this.ascanrules = zapAddOnXml.getAscanrules();
597         this.extensions = zapAddOnXml.getExtensions();
598         this.extensionsWithDeps = zapAddOnXml.getExtensionsWithDeps();
599         this.files = zapAddOnXml.getFiles();
600         this.libs = createLibs(zapAddOnXml.getLibs());
601         this.pscanrules = zapAddOnXml.getPscanrules();
602 
603         this.addOnClassnames = zapAddOnXml.getAddOnClassnames();
604 
605         String bundleBaseName = zapAddOnXml.getBundleBaseName();
606         if (!bundleBaseName.isEmpty()) {
607             bundleData = new BundleData(bundleBaseName, zapAddOnXml.getBundlePrefix());
608         }
609 
610         String helpSetBaseName = zapAddOnXml.getHelpSetBaseName();
611         if (!helpSetBaseName.isEmpty()) {
612             this.helpSetData =
613                     new HelpSetData(helpSetBaseName, zapAddOnXml.getHelpSetLocaleToken());
614         }
615 
616         hasZapAddOnEntry = true;
617     }
618 
619     /**
620      * Constructs an {@code AddOn} from an add-on entry of {@code ZapVersions.xml} file. The
621      * installation status of the add-on is 'not installed'.
622      *
623      * <p>The given {@code SubnodeConfiguration} must have a {@code XPathExpressionEngine}
624      * installed.
625      *
626      * <p>The {@value #MANIFEST_FILE_NAME} ZIP file entry is read, if the add-on file exists
627      * locally.
628      *
629      * @param id the id of the add-on
630      * @param baseDir the base directory where the add-on is located
631      * @param xmlData the source of add-on entry of {@code ZapVersions.xml} file
632      * @throws MalformedURLException if the {@code URL} of the add-on is malformed
633      * @throws IOException if an error occurs while reading the XML data
634      * @see org.apache.commons.configuration.tree.xpath.XPathExpressionEngine
635      */
AddOn(String id, File baseDir, SubnodeConfiguration xmlData)636     public AddOn(String id, File baseDir, SubnodeConfiguration xmlData)
637             throws MalformedURLException, IOException {
638         this.id = id;
639         ZapVersionsAddOnEntry addOnData = new ZapVersionsAddOnEntry(xmlData);
640         this.name = addOnData.getName();
641         this.description = addOnData.getDescription();
642         this.author = addOnData.getAuthor();
643         this.dependencies = addOnData.getDependencies();
644         this.extensionsWithDeps = addOnData.getExtensionsWithDeps();
645         this.version = addOnData.getVersion();
646         this.semVer = addOnData.getSemVer();
647         this.status = AddOn.Status.valueOf(addOnData.getStatus());
648         this.changes = addOnData.getChanges();
649         this.url = new URL(addOnData.getUrl());
650         this.file = new File(baseDir, addOnData.getFile());
651         this.size = addOnData.getSize();
652         this.notBeforeVersion = addOnData.getNotBeforeVersion();
653         this.notFromVersion = addOnData.getNotFromVersion();
654         this.info = createUrl(addOnData.getInfo());
655         this.repo = createUrl(addOnData.getRepo());
656         this.releaseDate = addOnData.getDate();
657         this.hash = addOnData.getHash();
658 
659         loadManifestFile();
660     }
661 
createUrl(String url)662     private URL createUrl(String url) {
663         if (url != null && !url.isEmpty()) {
664             try {
665                 return new URL(url);
666             } catch (Exception e) {
667                 logger.warn("Invalid URL for add-on \"" + id + "\": " + url, e);
668             }
669         }
670         return null;
671     }
672 
createLibs(List<String> libPaths)673     private static List<Lib> createLibs(List<String> libPaths) {
674         if (libPaths.isEmpty()) {
675             return Collections.emptyList();
676         }
677         return libPaths.stream().map(Lib::new).collect(Collectors.toList());
678     }
679 
getId()680     public String getId() {
681         return id;
682     }
683 
setId(String id)684     public void setId(String id) {
685         this.id = id;
686     }
687 
getName()688     public String getName() {
689         return name;
690     }
691 
setName(String name)692     public void setName(String name) {
693         this.name = name;
694     }
695 
getDescription()696     public String getDescription() {
697         return description;
698     }
699 
setDescription(String description)700     public void setDescription(String description) {
701         this.description = description;
702     }
703 
704     /**
705      * Gets the file version of the add-on.
706      *
707      * @return the file version.
708      * @deprecated (2.7.0) Use {@link #getVersion()} instead.
709      */
710     @Deprecated
getFileVersion()711     public int getFileVersion() {
712         return getVersion().getMajorVersion();
713     }
714 
715     /**
716      * Gets the semantic version of this add-on.
717      *
718      * <p>Since 2.7.0, for add-ons that use just an integer as the version it's appended ".0.0", for
719      * example, for version {@literal 14} it returns the version {@literal 14.0.0}.
720      *
721      * @return the semantic version of the add-on, since 2.7.0, never {@code null}.
722      * @since 2.4.0
723      */
getVersion()724     public Version getVersion() {
725         return version;
726     }
727 
728     /**
729      * Gets the semantic version declared in the manifest file.
730      *
731      * <p>To be replaced by {@link #getVersion()}.
732      *
733      * @return the semantic version declared in the manifest file, might be {@code null}.
734      */
getSemVer()735     Version getSemVer() {
736         return semVer;
737     }
738 
getStatus()739     public Status getStatus() {
740         return status;
741     }
742 
setStatus(Status status)743     public void setStatus(Status status) {
744         this.status = status;
745     }
746 
getChanges()747     public String getChanges() {
748         return changes;
749     }
750 
setChanges(String changes)751     public void setChanges(String changes) {
752         this.changes = changes;
753     }
754 
755     /**
756      * Gets the normalised file name of the add-on.
757      *
758      * <p>Should be used when copying the file from an "unknown" source (e.g. manually installed).
759      *
760      * @return the normalised file name.
761      * @since 2.6.0
762      * @see #getFile()
763      */
getNormalisedFileName()764     public String getNormalisedFileName() {
765         return getId() + "-" + getVersion() + FILE_EXTENSION;
766     }
767 
768     /**
769      * Gets the file of the add-on.
770      *
771      * @return the file of the add-on.
772      * @see #getNormalisedFileName()
773      */
getFile()774     public File getFile() {
775         return file;
776     }
777 
setFile(File file)778     public void setFile(File file) {
779         this.file = file;
780     }
781 
getUrl()782     public URL getUrl() {
783         return url;
784     }
785 
setUrl(URL url)786     public void setUrl(URL url) {
787         this.url = url;
788     }
789 
getSize()790     public long getSize() {
791         return size;
792     }
793 
setSize(long size)794     public void setSize(long size) {
795         this.size = size;
796     }
797 
getAuthor()798     public String getAuthor() {
799         return author;
800     }
801 
setAuthor(String author)802     public void setAuthor(String author) {
803         this.author = author;
804     }
805 
806     /**
807      * Gets the class loader of the add-on.
808      *
809      * @return the class loader of the add-on, {@code null} if the add-on is not installed.
810      * @since 2.8.0
811      */
getClassLoader()812     public ClassLoader getClassLoader() {
813         return classLoader;
814     }
815 
816     /**
817      * Sets the class loader of the add-on.
818      *
819      * <p><strong>Note:</strong> This method should be called only by bootstrap classes.
820      *
821      * @param classLoader the class loader of the add-on, might be {@code null}.
822      * @since 2.8.0
823      */
setClassLoader(ClassLoader classLoader)824     public void setClassLoader(ClassLoader classLoader) {
825         this.classLoader = classLoader;
826     }
827 
828     /**
829      * Sets the installation status of the add-on.
830      *
831      * @param installationStatus the new installation status
832      * @throws IllegalArgumentException if the given {@code installationStatus} is {@code null}.
833      * @since 2.4.0
834      */
setInstallationStatus(InstallationStatus installationStatus)835     public void setInstallationStatus(InstallationStatus installationStatus) {
836         if (installationStatus == null) {
837             throw new IllegalArgumentException("Parameter installationStatus must not be null.");
838         }
839 
840         this.installationStatus = installationStatus;
841     }
842 
843     /**
844      * Gets installations status of the add-on.
845      *
846      * @return the installation status, never {@code null}
847      * @since 2.4.0
848      */
getInstallationStatus()849     public InstallationStatus getInstallationStatus() {
850         return installationStatus;
851     }
852 
hasZapAddOnEntry()853     public boolean hasZapAddOnEntry() {
854         if (!hasZapAddOnEntry) {
855             if (!manifestRead) {
856                 // Worth trying, as it depends which constructor has been used
857                 try {
858                     this.loadManifestFile();
859                 } catch (IOException e) {
860                     if (logger.isDebugEnabled()) {
861                         logger.debug(
862                                 "Failed to read the "
863                                         + AddOn.MANIFEST_FILE_NAME
864                                         + " file of "
865                                         + id
866                                         + ":",
867                                 e);
868                     }
869                 }
870             }
871         }
872         return hasZapAddOnEntry;
873     }
874 
875     /**
876      * Gets the classnames that can be loaded for the add-on.
877      *
878      * @return the classnames that can be loaded
879      * @since 2.4.3
880      */
getAddOnClassnames()881     public AddOnClassnames getAddOnClassnames() {
882         return addOnClassnames;
883     }
884 
getExtensions()885     public List<String> getExtensions() {
886         return extensions;
887     }
888 
889     /**
890      * Returns the classnames of {@code Extension}sthat have dependencies on add-ons.
891      *
892      * @return the classnames of the extensions with dependencies on add-ons.
893      * @see #hasExtensionsWithDeps()
894      */
getExtensionsWithDeps()895     public List<String> getExtensionsWithDeps() {
896         if (extensionsWithDeps.isEmpty()) {
897             return Collections.emptyList();
898         }
899 
900         List<String> extensionClassnames = new ArrayList<>(extensionsWithDeps.size());
901         for (ExtensionWithDeps extension : extensionsWithDeps) {
902             extensionClassnames.add(extension.getClassname());
903         }
904         return extensionClassnames;
905     }
906 
907     /**
908      * Returns the classnames that can be loaded for the given {@code Extension} (with
909      * dependencies).
910      *
911      * @param classname the classname of the extension
912      * @return the classnames that can be loaded
913      * @since 2.4.3
914      * @see #hasExtensionsWithDeps()
915      */
getExtensionAddOnClassnames(String classname)916     public AddOnClassnames getExtensionAddOnClassnames(String classname) {
917         if (extensionsWithDeps.isEmpty() || classname == null || classname.isEmpty()) {
918             return AddOnClassnames.ALL_ALLOWED;
919         }
920 
921         for (ExtensionWithDeps extension : extensionsWithDeps) {
922             if (classname.equals(extension.getClassname())) {
923                 return extension.getAddOnClassnames();
924             }
925         }
926         return AddOnClassnames.ALL_ALLOWED;
927     }
928 
929     /**
930      * Tells whether or not this add-on has at least one extension with dependencies.
931      *
932      * @return {@code true} if the add-on has at leas one extension with dependencies, {@code false}
933      *     otherwise
934      * @see #getExtensionsWithDeps()
935      */
hasExtensionsWithDeps()936     public boolean hasExtensionsWithDeps() {
937         return !extensionsWithDeps.isEmpty();
938     }
939 
940     /**
941      * Gets the extensions of this add-on that have dependencies and were loaded.
942      *
943      * @return an unmodifiable {@code List} with the extensions of this add-on that have
944      *     dependencies and were loaded
945      * @since 2.4.0
946      */
getLoadedExtensionsWithDeps()947     public List<Extension> getLoadedExtensionsWithDeps() {
948         List<String> classnames = getExtensionsWithDeps();
949         ArrayList<Extension> loadedExtensions = new ArrayList<>(extensionsWithDeps.size());
950         for (Extension extension : getLoadedExtensions()) {
951             if (classnames.contains(extension.getClass().getCanonicalName())) {
952                 loadedExtensions.add(extension);
953             }
954         }
955         loadedExtensions.trimToSize();
956         return loadedExtensions;
957     }
958 
959     /**
960      * Gets the extensions of this add-on that were loaded.
961      *
962      * @return an unmodifiable {@code List} with the extensions of this add-on that were loaded
963      * @since 2.4.0
964      */
getLoadedExtensions()965     public List<Extension> getLoadedExtensions() {
966         if (loadedExtensions == null) {
967             return Collections.emptyList();
968         }
969         return Collections.unmodifiableList(loadedExtensions);
970     }
971 
972     /**
973      * Adds the given {@code extension} to the list of loaded extensions of this add-on.
974      *
975      * <p>This add-on is set to the given {@code extension}.
976      *
977      * @param extension the extension of this add-on that was loaded
978      * @throws IllegalArgumentException if extension is {@code null}
979      * @since 2.4.0
980      * @see #removeLoadedExtension(Extension)
981      * @see Extension#setAddOn(AddOn)
982      */
addLoadedExtension(Extension extension)983     public void addLoadedExtension(Extension extension) {
984         if (extension == null) {
985             throw new IllegalArgumentException("Parameter extension must not be null.");
986         }
987 
988         if (loadedExtensions == null) {
989             loadedExtensions = new ArrayList<>(1);
990         }
991 
992         if (!loadedExtensions.contains(extension)) {
993             loadedExtensions.add(extension);
994             extension.setAddOn(this);
995         }
996     }
997 
998     /**
999      * Removes the given {@code extension} from the list of loaded extensions of this add-on.
1000      *
1001      * <p>The add-on of the given {@code extension} is set to {@code null}.
1002      *
1003      * <p>The call to this method has no effect if the given {@code extension} does not belong to
1004      * this add-on.
1005      *
1006      * @param extension the loaded extension of this add-on that should be removed
1007      * @throws IllegalArgumentException if extension is {@code null}
1008      * @since 2.4.0
1009      * @see #addLoadedExtension(Extension)
1010      * @see Extension#setAddOn(AddOn)
1011      */
removeLoadedExtension(Extension extension)1012     public void removeLoadedExtension(Extension extension) {
1013         if (extension == null) {
1014             throw new IllegalArgumentException("Parameter extension must not be null.");
1015         }
1016 
1017         if (loadedExtensions != null && loadedExtensions.contains(extension)) {
1018             loadedExtensions.remove(extension);
1019             extension.setAddOn(null);
1020         }
1021     }
1022 
getAscanrules()1023     public List<String> getAscanrules() {
1024         return ascanrules;
1025     }
1026 
1027     /**
1028      * Gets the active scan rules of this add-on that were loaded.
1029      *
1030      * @return an unmodifiable {@code List} with the active scan rules of this add-on that were
1031      *     loaded, never {@code null}
1032      * @since 2.4.3
1033      * @see #setLoadedAscanrules(List)
1034      */
getLoadedAscanrules()1035     public List<AbstractPlugin> getLoadedAscanrules() {
1036         return loadedAscanrules;
1037     }
1038 
1039     /**
1040      * Sets the loaded active scan rules of the add-on, allowing to set the status of the active
1041      * scan rules appropriately and to keep track of the active scan rules loaded so that they can
1042      * be removed during uninstallation.
1043      *
1044      * <p><strong>Note:</strong> Helper method to be used (only) by/during (un)installation process
1045      * and loading of the add-on. Should be called when installing/loading the add-on, by setting
1046      * the loaded active scan rules, and when uninstalling by setting an empty list. The method
1047      * {@code setLoadedAscanrulesSet(boolean)} should also be called.
1048      *
1049      * @param ascanrules the active scan rules loaded, might be empty if none were actually loaded
1050      * @throws IllegalArgumentException if {@code ascanrules} is {@code null}.
1051      * @since 2.4.3
1052      * @see #setLoadedAscanrulesSet(boolean)
1053      * @see AbstractPlugin#setStatus(Status)
1054      */
setLoadedAscanrules(List<AbstractPlugin> ascanrules)1055     void setLoadedAscanrules(List<AbstractPlugin> ascanrules) {
1056         if (ascanrules == null) {
1057             throw new IllegalArgumentException("Parameter ascanrules must not be null.");
1058         }
1059 
1060         if (ascanrules.isEmpty()) {
1061             loadedAscanrules = Collections.emptyList();
1062             return;
1063         }
1064 
1065         for (AbstractPlugin ascanrule : ascanrules) {
1066             ascanrule.setStatus(getStatus());
1067         }
1068         loadedAscanrules = Collections.unmodifiableList(new ArrayList<>(ascanrules));
1069     }
1070 
1071     /**
1072      * Tells whether or not the loaded active scan rules of the add-on, if any, were already set to
1073      * the add-on.
1074      *
1075      * <p><strong>Note:</strong> Helper method to be used (only) by/during (un)installation process
1076      * and loading of the add-on.
1077      *
1078      * @return {@code true} if the loaded active scan rules were already set, {@code false}
1079      *     otherwise
1080      * @since 2.4.3
1081      * @see #setLoadedAscanrules(List)
1082      * @see #setLoadedAscanrulesSet(boolean)
1083      */
isLoadedAscanrulesSet()1084     boolean isLoadedAscanrulesSet() {
1085         return loadedAscanRulesSet;
1086     }
1087 
1088     /**
1089      * Sets whether or not the loaded active scan rules, if any, where already set to the add-on.
1090      *
1091      * <p><strong>Note:</strong> Helper method to be used (only) by/during (un)installation process
1092      * and loading of the add-on. The method should be called, with {@code true} during
1093      * installation/loading and {@code false} during uninstallation, after calling the method {@code
1094      * setLoadedAscanrules(List)}.
1095      *
1096      * @param ascanrulesSet {@code true} if the loaded active scan rules were already set, {@code
1097      *     false} otherwise
1098      * @since 2.4.3
1099      * @see #setLoadedAscanrules(List)
1100      */
setLoadedAscanrulesSet(boolean ascanrulesSet)1101     void setLoadedAscanrulesSet(boolean ascanrulesSet) {
1102         loadedAscanRulesSet = ascanrulesSet;
1103     }
1104 
getPscanrules()1105     public List<String> getPscanrules() {
1106         return pscanrules;
1107     }
1108 
1109     /**
1110      * Gets the passive scan rules of this add-on that were loaded.
1111      *
1112      * @return an unmodifiable {@code List} with the passive scan rules of this add-on that were
1113      *     loaded, never {@code null}
1114      * @since 2.4.3
1115      * @see #setLoadedPscanrules(List)
1116      */
getLoadedPscanrules()1117     public List<PluginPassiveScanner> getLoadedPscanrules() {
1118         return loadedPscanrules;
1119     }
1120 
1121     /**
1122      * Sets the loaded passive scan rules of the add-on, allowing to set the status of the passive
1123      * scan rules appropriately and keep track of the passive scan rules loaded so that they can be
1124      * removed during uninstallation.
1125      *
1126      * <p><strong>Note:</strong> Helper method to be used (only) by/during (un)installation process
1127      * and loading of the add-on. Should be called when installing/loading the add-on, by setting
1128      * the loaded passive scan rules, and when uninstalling by setting an empty list. The method
1129      * {@code setLoadedPscanrulesSet(boolean)} should also be called.
1130      *
1131      * @param pscanrules the passive scan rules loaded, might be empty if none were actually loaded
1132      * @throws IllegalArgumentException if {@code pscanrules} is {@code null}.
1133      * @since 2.4.3
1134      * @see #setLoadedPscanrulesSet(boolean)
1135      * @see PluginPassiveScanner#setStatus(Status)
1136      */
setLoadedPscanrules(List<PluginPassiveScanner> pscanrules)1137     void setLoadedPscanrules(List<PluginPassiveScanner> pscanrules) {
1138         if (pscanrules == null) {
1139             throw new IllegalArgumentException("Parameter pscanrules must not be null.");
1140         }
1141 
1142         if (pscanrules.isEmpty()) {
1143             loadedPscanrules = Collections.emptyList();
1144             return;
1145         }
1146 
1147         for (PluginPassiveScanner pscanrule : pscanrules) {
1148             pscanrule.setStatus(getStatus());
1149         }
1150         loadedPscanrules = Collections.unmodifiableList(new ArrayList<>(pscanrules));
1151     }
1152 
1153     /**
1154      * Tells whether or not the loaded passive scan rules of the add-on, if any, were already set to
1155      * the add-on.
1156      *
1157      * <p><strong>Note:</strong> Helper method to be used (only) by/during (un)installation process
1158      * and loading of the add-on.
1159      *
1160      * @return {@code true} if the loaded passive scan rules were already set, {@code false}
1161      *     otherwise
1162      * @since 2.4.3
1163      * @see #setLoadedPscanrules(List)
1164      * @see #setLoadedPscanrulesSet(boolean)
1165      */
isLoadedPscanrulesSet()1166     boolean isLoadedPscanrulesSet() {
1167         return loadedPscanRulesSet;
1168     }
1169 
1170     /**
1171      * Sets whether or not the loaded passive scan rules, if any, where already set to the add-on.
1172      *
1173      * <p><strong>Note:</strong> Helper method to be used (only) by/during (un)installation process
1174      * and loading of the add-on. The method should be called, with {@code true} during
1175      * installation/loading and {@code false} during uninstallation, after calling the method {@code
1176      * setLoadedPscanrules(List)}.
1177      *
1178      * @param pscanrulesSet {@code true} if the loaded passive scan rules were already set, {@code
1179      *     false} otherwise
1180      * @since 2.4.3
1181      * @see #setLoadedPscanrules(List)
1182      */
setLoadedPscanrulesSet(boolean pscanrulesSet)1183     void setLoadedPscanrulesSet(boolean pscanrulesSet) {
1184         loadedPscanRulesSet = pscanrulesSet;
1185     }
1186 
getFiles()1187     public List<String> getFiles() {
1188         return files;
1189     }
1190 
1191     /**
1192      * Gets the bundled libraries of the add-on.
1193      *
1194      * @return the bundled libraries, never {@code null}.
1195      */
getLibs()1196     List<Lib> getLibs() {
1197         return libs;
1198     }
1199 
hasMissingLibs()1200     private boolean hasMissingLibs() {
1201         return libs.stream().anyMatch(lib -> lib.getFileSystemUrl() == null);
1202     }
1203 
isSameAddOn(AddOn addOn)1204     public boolean isSameAddOn(AddOn addOn) {
1205         return this.getId().equals(addOn.getId());
1206     }
1207 
isUpdateTo(AddOn addOn)1208     public boolean isUpdateTo(AddOn addOn) throws IllegalArgumentException {
1209         if (!this.isSameAddOn(addOn)) {
1210             throw new IllegalArgumentException(
1211                     "Different addons: " + this.getId() + " != " + addOn.getId());
1212         }
1213         int result = this.getVersion().compareTo(addOn.getVersion());
1214         if (result != 0) {
1215             return result > 0;
1216         }
1217 
1218         result = this.getStatus().compareTo(addOn.getStatus());
1219         if (result != 0) {
1220             return result > 0;
1221         }
1222 
1223         if (getFile() == null) {
1224             return false;
1225         }
1226         if (addOn.getFile() == null) {
1227             return true;
1228         }
1229         return getFile().lastModified() > addOn.getFile().lastModified();
1230     }
1231 
1232     /**
1233      * @deprecated (2.4.0) Use {@link #calculateRunRequirements(Collection)} instead. Returns {@code
1234      *     false}.
1235      * @return {@code false} always.
1236      */
1237     @Deprecated
canLoad()1238     public boolean canLoad() {
1239         return false;
1240     }
1241 
1242     /**
1243      * Tells whether or not this add-on can be loaded in the currently running ZAP version, as given
1244      * by {@code Constant.PROGRAM_VERSION}.
1245      *
1246      * @return {@code true} if the add-on can be loaded in the currently running ZAP version, {@code
1247      *     false} otherwise
1248      * @see #canLoadInVersion(String)
1249      * @see Constant#PROGRAM_VERSION
1250      */
canLoadInCurrentVersion()1251     public boolean canLoadInCurrentVersion() {
1252         return canLoadInVersion(Constant.PROGRAM_VERSION);
1253     }
1254 
1255     /**
1256      * Tells whether or not this add-on can be run in the currently running Java version.
1257      *
1258      * <p>This is a convenience method that calls {@code canRunInJavaVersion(String)} with the
1259      * running Java version (as given by {@code SystemUtils.JAVA_VERSION}) as parameter.
1260      *
1261      * @return {@code true} if the add-on can be run in the currently running Java version, {@code
1262      *     false} otherwise
1263      * @since 2.4.0
1264      * @see #canRunInJavaVersion(String)
1265      * @see SystemUtils#JAVA_VERSION
1266      */
canRunInCurrentJavaVersion()1267     public boolean canRunInCurrentJavaVersion() {
1268         return canRunInJavaVersion(SystemUtils.JAVA_VERSION);
1269     }
1270 
1271     /**
1272      * Tells whether or not this add-on can be run in the given {@code javaVersion}.
1273      *
1274      * <p>If the given {@code javaVersion} is {@code null} and this add-on depends on a specific
1275      * java version the method returns {@code false}.
1276      *
1277      * @param javaVersion the java version that will be checked
1278      * @return {@code true} if the add-on can be loaded in the given {@code javaVersion}, {@code
1279      *     false} otherwise
1280      * @since 2.4.0
1281      */
canRunInJavaVersion(String javaVersion)1282     public boolean canRunInJavaVersion(String javaVersion) {
1283         if (dependencies == null) {
1284             return true;
1285         }
1286 
1287         String requiredVersion = dependencies.getJavaVersion();
1288         if (requiredVersion == null) {
1289             return true;
1290         }
1291 
1292         if (javaVersion == null) {
1293             return false;
1294         }
1295 
1296         return getJavaVersion(javaVersion) >= getJavaVersion(requiredVersion);
1297     }
1298 
1299     /**
1300      * Calculates the requirements to run this add-on, in the current ZAP and Java versions and with
1301      * the given {@code availableAddOns}.
1302      *
1303      * <p>If the add-on depends on other add-ons, those add-ons are also checked if are also
1304      * runnable.
1305      *
1306      * <p><strong>Note:</strong> All the given {@code availableAddOns} are expected to be loadable
1307      * in the currently running ZAP version, that is, the method {@code
1308      * AddOn.canLoadInCurrentVersion()}, returns {@code true}.
1309      *
1310      * @param availableAddOns the other add-ons available
1311      * @return a requirements to run the add-on, and if not runnable the reason why it's not.
1312      * @since 2.4.0
1313      * @see #canLoadInCurrentVersion()
1314      * @see #canRunInCurrentJavaVersion()
1315      * @see AddOnRunRequirements
1316      */
calculateRunRequirements(Collection<AddOn> availableAddOns)1317     public AddOnRunRequirements calculateRunRequirements(Collection<AddOn> availableAddOns) {
1318         return calculateRunRequirements(availableAddOns, true);
1319     }
1320 
1321     /**
1322      * Calculates the requirements to install/run this add-on, same as {@link
1323      * #calculateRunRequirements(Collection)} but does not require the libraries of the add-ons
1324      * (this or dependencies) to exist in the file system.
1325      *
1326      * @param availableAddOns the other add-ons available.
1327      * @return the requirements to install/run the add-on, and if not runnable the reason why it's
1328      *     not.
1329      * @since 2.9.0
1330      * @see #calculateRunRequirements(Collection)
1331      * @see AddOnRunRequirements
1332      */
calculateInstallRequirements(Collection<AddOn> availableAddOns)1333     public AddOnRunRequirements calculateInstallRequirements(Collection<AddOn> availableAddOns) {
1334         return calculateRunRequirements(availableAddOns, false);
1335     }
1336 
calculateRunRequirements( Collection<AddOn> availableAddOns, boolean checkLibs)1337     private AddOnRunRequirements calculateRunRequirements(
1338             Collection<AddOn> availableAddOns, boolean checkLibs) {
1339         AddOnRunRequirements requirements = new AddOnRunRequirements(this);
1340         calculateRunRequirementsImpl(availableAddOns, requirements, null, this, checkLibs);
1341         if (requirements.isRunnable()) {
1342             checkExtensionsWithDeps(availableAddOns, requirements, this, checkLibs);
1343         }
1344         return requirements;
1345     }
1346 
calculateRunRequirementsImpl( Collection<AddOn> availableAddOns, BaseRunRequirements requirements, AddOn parent, AddOn addOn, boolean checkLibs)1347     private static void calculateRunRequirementsImpl(
1348             Collection<AddOn> availableAddOns,
1349             BaseRunRequirements requirements,
1350             AddOn parent,
1351             AddOn addOn,
1352             boolean checkLibs) {
1353         if (checkLibs && addOn.hasMissingLibs()) {
1354             requirements.setMissingLibsIssue(addOn);
1355             return;
1356         }
1357 
1358         AddOn installedVersion = getAddOn(availableAddOns, addOn.getId());
1359         if (installedVersion != null && !addOn.equals(installedVersion)) {
1360             requirements.setIssue(
1361                     BaseRunRequirements.DependencyIssue.OLDER_VERSION, installedVersion);
1362             if (logger.isDebugEnabled()) {
1363                 logger.debug(
1364                         "Add-on "
1365                                 + addOn
1366                                 + " not runnable, old version still installed: "
1367                                 + installedVersion);
1368             }
1369             return;
1370         }
1371 
1372         if (!requirements.addDependency(parent, addOn)) {
1373             logger.warn("Cyclic dependency detected with: " + requirements.getDependencies());
1374             requirements.setIssue(
1375                     BaseRunRequirements.DependencyIssue.CYCLIC, requirements.getDependencies());
1376             return;
1377         }
1378 
1379         if (addOn.dependencies == null) {
1380             return;
1381         }
1382 
1383         if (!addOn.canRunInCurrentJavaVersion()) {
1384             requirements.setMinimumJavaVersionIssue(addOn, addOn.dependencies.getJavaVersion());
1385         }
1386 
1387         for (AddOnDep dependency : addOn.dependencies.getAddOns()) {
1388             String addOnId = dependency.getId();
1389             if (addOnId != null) {
1390                 AddOn addOnDep = getAddOn(availableAddOns, addOnId);
1391                 if (addOnDep == null) {
1392                     requirements.setIssue(BaseRunRequirements.DependencyIssue.MISSING, addOnId);
1393                     return;
1394                 }
1395 
1396                 if (!dependency.getVersion().isEmpty()) {
1397                     if (!versionMatches(addOnDep, dependency)) {
1398                         requirements.setIssue(
1399                                 BaseRunRequirements.DependencyIssue.VERSION,
1400                                 addOnDep,
1401                                 dependency.getVersion());
1402                         return;
1403                     }
1404                 }
1405 
1406                 calculateRunRequirementsImpl(
1407                         availableAddOns, requirements, addOn, addOnDep, checkLibs);
1408                 if (requirements.hasDependencyIssue()) {
1409                     return;
1410                 }
1411             }
1412         }
1413     }
1414 
checkExtensionsWithDeps( Collection<AddOn> availableAddOns, AddOnRunRequirements requirements, AddOn addOn, boolean checkLibs)1415     private static void checkExtensionsWithDeps(
1416             Collection<AddOn> availableAddOns,
1417             AddOnRunRequirements requirements,
1418             AddOn addOn,
1419             boolean checkLibs) {
1420         if (addOn.extensionsWithDeps.isEmpty()) {
1421             return;
1422         }
1423 
1424         for (ExtensionWithDeps extension : addOn.extensionsWithDeps) {
1425             calculateExtensionRunRequirements(
1426                     extension, availableAddOns, requirements, addOn, checkLibs);
1427         }
1428     }
1429 
calculateExtensionRunRequirements( ExtensionWithDeps extension, Collection<AddOn> availableAddOns, AddOnRunRequirements requirements, AddOn addOn, boolean checkLibs)1430     private static void calculateExtensionRunRequirements(
1431             ExtensionWithDeps extension,
1432             Collection<AddOn> availableAddOns,
1433             AddOnRunRequirements requirements,
1434             AddOn addOn,
1435             boolean checkLibs) {
1436         ExtensionRunRequirements extensionRequirements =
1437                 new ExtensionRunRequirements(addOn, extension.getClassname());
1438         requirements.addExtensionRequirements(extensionRequirements);
1439         for (AddOnDep dependency : extension.getDependencies()) {
1440             String addOnId = dependency.getId();
1441             if (addOnId == null) {
1442                 continue;
1443             }
1444 
1445             AddOn addOnDep = getAddOn(availableAddOns, addOnId);
1446             if (addOnDep == null) {
1447                 if (addOn.hasOnlyOneExtensionWithDependencies()) {
1448                     requirements.setIssue(BaseRunRequirements.DependencyIssue.MISSING, addOnId);
1449                     return;
1450                 }
1451                 extensionRequirements.setIssue(
1452                         BaseRunRequirements.DependencyIssue.MISSING, addOnId);
1453                 continue;
1454             }
1455 
1456             if (!dependency.getVersion().isEmpty()) {
1457                 if (!versionMatches(addOnDep, dependency)) {
1458                     if (addOn.hasOnlyOneExtensionWithDependencies()) {
1459                         requirements.setIssue(
1460                                 BaseRunRequirements.DependencyIssue.VERSION,
1461                                 addOnDep,
1462                                 dependency.getVersion());
1463                         return;
1464                     }
1465                     extensionRequirements.setIssue(
1466                             BaseRunRequirements.DependencyIssue.VERSION,
1467                             addOnDep,
1468                             dependency.getVersion());
1469                     continue;
1470                 }
1471             }
1472 
1473             calculateRunRequirementsImpl(
1474                     availableAddOns, extensionRequirements, addOn, addOnDep, checkLibs);
1475         }
1476     }
1477 
hasOnlyOneExtensionWithDependencies()1478     private boolean hasOnlyOneExtensionWithDependencies() {
1479         if (extensionsWithDeps.size() != 1) {
1480             return false;
1481         }
1482         if (extensions.isEmpty()
1483                 && files.isEmpty()
1484                 && pscanrules.isEmpty()
1485                 && ascanrules.isEmpty()) {
1486             return true;
1487         }
1488         return false;
1489     }
1490 
1491     /**
1492      * Calculates the requirements to run the given {@code extension}, in the current ZAP and Java
1493      * versions and with the given {@code availableAddOns}.
1494      *
1495      * <p>If the extension depends on other add-ons, those add-ons are checked if are also runnable.
1496      *
1497      * <p><strong>Note:</strong> All the given {@code availableAddOns} are expected to be loadable
1498      * in the currently running ZAP version, that is, the method {@code
1499      * AddOn.canLoadInCurrentVersion()}, returns {@code true}.
1500      *
1501      * @param extension the extension that will be checked
1502      * @param availableAddOns the add-ons available
1503      * @return the requirements to run the extension, and if not runnable the reason why it's not.
1504      * @since 2.4.0
1505      * @see AddOnRunRequirements#getExtensionRequirements()
1506      */
calculateExtensionRunRequirements( Extension extension, Collection<AddOn> availableAddOns)1507     public AddOnRunRequirements calculateExtensionRunRequirements(
1508             Extension extension, Collection<AddOn> availableAddOns) {
1509         return calculateExtensionRunRequirementsImpl(
1510                 extension.getClass().getCanonicalName(), availableAddOns, true);
1511     }
1512 
1513     /**
1514      * Calculates the requirements to install/run the given {@code extension}, same as {@link
1515      * #calculateExtensionRunRequirements(Extension, Collection)} but does not require the libraries
1516      * of the add-ons (this or dependencies) to exist in the file system.
1517      *
1518      * @param extension the extension that will be checked.
1519      * @param availableAddOns the add-ons available.
1520      * @return the requirements to install/run the extension, and if not runnable the reason why
1521      *     it's not.
1522      * @since 2.9.0
1523      * @see AddOnRunRequirements#getExtensionRequirements()
1524      */
calculateExtensionInstallRequirements( Extension extension, Collection<AddOn> availableAddOns)1525     public AddOnRunRequirements calculateExtensionInstallRequirements(
1526             Extension extension, Collection<AddOn> availableAddOns) {
1527         return calculateExtensionRunRequirementsImpl(
1528                 extension.getClass().getCanonicalName(), availableAddOns, false);
1529     }
1530 
1531     /**
1532      * Calculates the requirements to run the extension with the given {@code classname}, in the
1533      * current ZAP and Java versions and with the given {@code availableAddOns}.
1534      *
1535      * <p>If the extension depends on other add-ons, those add-ons are checked if are also runnable.
1536      *
1537      * <p><strong>Note:</strong> All the given {@code availableAddOns} are expected to be loadable
1538      * in the currently running ZAP version, that is, the method {@code
1539      * AddOn.canLoadInCurrentVersion()}, returns {@code true}.
1540      *
1541      * @param classname the classname of extension that will be checked
1542      * @param availableAddOns the add-ons available
1543      * @return the requirements to run the extension, and if not runnable the reason why it's not.
1544      * @since 2.4.0
1545      * @see AddOnRunRequirements#getExtensionRequirements()
1546      */
calculateExtensionRunRequirements( String classname, Collection<AddOn> availableAddOns)1547     public AddOnRunRequirements calculateExtensionRunRequirements(
1548             String classname, Collection<AddOn> availableAddOns) {
1549         return calculateExtensionRunRequirementsImpl(classname, availableAddOns, true);
1550     }
1551 
1552     /**
1553      * Calculates the requirements to install/run the given {@code extension}, same as {@link
1554      * #calculateExtensionRunRequirements(String, Collection)} but does not require the libraries of
1555      * the add-ons (this or dependencies) to exist in the file system.
1556      *
1557      * @param classname the classname of extension that will be checked.
1558      * @param availableAddOns the add-ons available.
1559      * @return the requirements to install/run the extension, and if not runnable the reason why
1560      *     it's not.
1561      * @since 2.9.0
1562      * @see AddOnRunRequirements#getExtensionRequirements()
1563      */
calculateExtensionInstallRequirements( String classname, Collection<AddOn> availableAddOns)1564     public AddOnRunRequirements calculateExtensionInstallRequirements(
1565             String classname, Collection<AddOn> availableAddOns) {
1566         return calculateExtensionRunRequirementsImpl(classname, availableAddOns, false);
1567     }
1568 
calculateExtensionRunRequirementsImpl( String classname, Collection<AddOn> availableAddOns, boolean checkLibs)1569     private AddOnRunRequirements calculateExtensionRunRequirementsImpl(
1570             String classname, Collection<AddOn> availableAddOns, boolean checkLibs) {
1571         AddOnRunRequirements requirements = new AddOnRunRequirements(this);
1572         for (ExtensionWithDeps extensionWithDeps : extensionsWithDeps) {
1573             if (extensionWithDeps.getClassname().equals(classname)) {
1574                 calculateExtensionRunRequirements(
1575                         extensionWithDeps, availableAddOns, requirements, this, checkLibs);
1576                 break;
1577             }
1578         }
1579         return requirements;
1580     }
1581 
1582     /**
1583      * Tells whether or not the given {@code extension} has a (direct) dependency on the given
1584      * {@code addOn} (including version).
1585      *
1586      * @param extension the extension that will be checked
1587      * @param addOn the add-on that will be checked in the dependencies on the extension
1588      * @return {@code true} if the extension depends on the given add-on, {@code false} otherwise.
1589      * @since 2.4.0
1590      */
dependsOn(Extension extension, AddOn addOn)1591     public boolean dependsOn(Extension extension, AddOn addOn) {
1592         String classname = extension.getClass().getCanonicalName();
1593 
1594         for (ExtensionWithDeps extensionWithDeps : extensionsWithDeps) {
1595             if (extensionWithDeps.getClassname().equals(classname)) {
1596                 return dependsOn(extensionWithDeps.getDependencies(), addOn);
1597             }
1598         }
1599         return false;
1600     }
1601 
dependsOn(List<AddOnDep> dependencies, AddOn addOn)1602     private static boolean dependsOn(List<AddOnDep> dependencies, AddOn addOn) {
1603         for (AddOnDep dependency : dependencies) {
1604             if (dependency.getId().equals(addOn.id)) {
1605                 if (!dependency.getVersion().isEmpty()) {
1606                     return versionMatches(addOn, dependency);
1607                 }
1608                 return true;
1609             }
1610         }
1611         return false;
1612     }
1613 
1614     /**
1615      * Tells whether or not the given add-on version matches the one required by the dependency.
1616      *
1617      * <p>This methods is required to also check the {@code semVer} of the add-on, once removed it
1618      * can match the version directly.
1619      *
1620      * @param addOn the add-on to check
1621      * @param dependency the dependency
1622      * @return {@code true} if the version matches, {@code false} otherwise.
1623      */
versionMatches(AddOn addOn, AddOnDep dependency)1624     private static boolean versionMatches(AddOn addOn, AddOnDep dependency) {
1625         if (addOn.version.matches(dependency.getVersion())) {
1626             return true;
1627         }
1628 
1629         if (addOn.semVer != null && addOn.semVer.matches(dependency.getVersion())) {
1630             return true;
1631         }
1632 
1633         return false;
1634     }
1635 
1636     /**
1637      * Tells whether or not the extension with the given {@code classname} is loaded.
1638      *
1639      * @param classname the classname of the extension
1640      * @return {@code true} if the extension is loaded, {@code false} otherwise
1641      * @since 2.4.0
1642      */
isExtensionLoaded(String classname)1643     public boolean isExtensionLoaded(String classname) {
1644         for (Extension extension : getLoadedExtensions()) {
1645             if (classname.equals(extension.getClass().getCanonicalName())) {
1646                 return true;
1647             }
1648         }
1649         return false;
1650     }
1651 
1652     /**
1653      * Returns the minimum Java version required to run this add-on or an empty {@code String} if
1654      * there's no minimum version.
1655      *
1656      * @return the minimum Java version required to run this add-on or an empty {@code String} if no
1657      *     minimum version
1658      * @since 2.4.0
1659      */
getMinimumJavaVersion()1660     public String getMinimumJavaVersion() {
1661         if (dependencies == null) {
1662             return "";
1663         }
1664         return dependencies.getJavaVersion();
1665     }
1666 
1667     /**
1668      * Gets the add-on with the given {@code id} from the given collection of {@code addOns}.
1669      *
1670      * @param addOns the collection of add-ons where the search will be made
1671      * @param id the id of the add-on to search for
1672      * @return the {@code AddOn} with the given id, or {@code null} if not found
1673      */
getAddOn(Collection<AddOn> addOns, String id)1674     private static AddOn getAddOn(Collection<AddOn> addOns, String id) {
1675         for (AddOn addOn : addOns) {
1676             if (addOn.getId().equals(id)) {
1677                 return addOn;
1678             }
1679         }
1680         return null;
1681     }
1682 
1683     /**
1684      * Tells whether or not this add-on can be loaded in the given {@code zapVersion}.
1685      *
1686      * @param zapVersion the ZAP version that will be checked
1687      * @return {@code true} if the add-on can be loaded in the given {@code zapVersion}, {@code
1688      *     false} otherwise
1689      */
canLoadInVersion(String zapVersion)1690     public boolean canLoadInVersion(String zapVersion) {
1691         // Require add-ons to declare the version they implement
1692         if (this.notBeforeVersion == null || this.notBeforeVersion.isEmpty()) {
1693             return false;
1694         }
1695 
1696         ZapReleaseComparitor zrc = new ZapReleaseComparitor();
1697         ZapRelease zr = new ZapRelease(zapVersion);
1698         ZapRelease notBeforeRelease = new ZapRelease(this.notBeforeVersion);
1699         if (zrc.compare(zr, notBeforeRelease) < 0) {
1700             return false;
1701         }
1702 
1703         if (zrc.compare(notBeforeRelease, v2_4) < 0) {
1704             // Don't load any add-ons that imply they are prior to 2.4.0 - they probably wont work
1705             return false;
1706         }
1707         if (this.notFromVersion != null && this.notFromVersion.length() > 0) {
1708             ZapRelease notFromRelease = new ZapRelease(this.notFromVersion);
1709             return (zrc.compare(zr, notFromRelease) < 0);
1710         }
1711         return true;
1712     }
1713 
setNotBeforeVersion(String notBeforeVersion)1714     public void setNotBeforeVersion(String notBeforeVersion) {
1715         this.notBeforeVersion = notBeforeVersion;
1716     }
1717 
setNotFromVersion(String notFromVersion)1718     public void setNotFromVersion(String notFromVersion) {
1719         this.notFromVersion = notFromVersion;
1720     }
1721 
getNotBeforeVersion()1722     public String getNotBeforeVersion() {
1723         return notBeforeVersion;
1724     }
1725 
getNotFromVersion()1726     public String getNotFromVersion() {
1727         return notFromVersion;
1728     }
1729 
getInfo()1730     public URL getInfo() {
1731         return info;
1732     }
1733 
setInfo(URL info)1734     public void setInfo(URL info) {
1735         this.info = info;
1736     }
1737 
1738     /**
1739      * Gets the URL to the browsable repo.
1740      *
1741      * @return the URL to the repo, might be {@code null}.
1742      * @since 2.9.0
1743      */
getRepo()1744     public URL getRepo() {
1745         return repo;
1746     }
1747 
getHash()1748     public String getHash() {
1749         return hash;
1750     }
1751 
1752     /**
1753      * Gets the date when the add-on was released.
1754      *
1755      * <p>The date has the format {@code YYYY-MM-DD}.
1756      *
1757      * <p><strong>Note:</strong> The date is only available for add-ons created from the
1758      * marketplace.
1759      *
1760      * @return the release date, or {@code null} if not available.
1761      * @since 2.10.0
1762      */
getReleaseDate()1763     public String getReleaseDate() {
1764         return releaseDate;
1765     }
1766 
1767     /**
1768      * Gets the data of the bundle declared in the manifest.
1769      *
1770      * @return the bundle data, never {@code null}.
1771      * @since 2.8.0
1772      * @see #getResourceBundle()
1773      */
getBundleData()1774     public BundleData getBundleData() {
1775         return bundleData;
1776     }
1777 
1778     /**
1779      * Gets the resource bundle of the add-on.
1780      *
1781      * @return the resource bundle, or {@code null} if none.
1782      * @since 2.8.0
1783      */
getResourceBundle()1784     public ResourceBundle getResourceBundle() {
1785         return resourceBundle;
1786     }
1787 
1788     /**
1789      * Sets the resource bundle of the add-on.
1790      *
1791      * <p><strong>Note:</strong> This method should be called only by bootstrap classes.
1792      *
1793      * @param resourceBundle the resource bundle of the add-on, might be {@code null}.
1794      * @since 2.8.0
1795      * @see #getBundleData()
1796      */
setResourceBundle(ResourceBundle resourceBundle)1797     public void setResourceBundle(ResourceBundle resourceBundle) {
1798         this.resourceBundle = resourceBundle;
1799     }
1800 
1801     /**
1802      * Gets the data for the HelpSet, declared in the manifest.
1803      *
1804      * @return the HelpSet data, never {@code null}.
1805      * @since 2.8.0
1806      */
getHelpSetData()1807     public HelpSetData getHelpSetData() {
1808         return helpSetData;
1809     }
1810 
1811     /**
1812      * Returns the IDs of the add-ons dependencies, an empty collection if none.
1813      *
1814      * @return the IDs of the dependencies.
1815      * @since 2.4.0
1816      */
getIdsAddOnDependencies()1817     public List<String> getIdsAddOnDependencies() {
1818         if (dependencies == null) {
1819             return Collections.emptyList();
1820         }
1821 
1822         List<String> ids = new ArrayList<>(dependencies.getAddOns().size());
1823         for (AddOnDep dep : dependencies.getAddOns()) {
1824             ids.add(dep.getId());
1825         }
1826         return ids;
1827     }
1828 
1829     /**
1830      * Tells whether or not this add-on has a (direct) dependency on the given {@code addOn}
1831      * (including version).
1832      *
1833      * @param addOn the add-on that will be checked
1834      * @return {@code true} if it depends on the given add-on, {@code false} otherwise.
1835      * @since 2.4.0
1836      */
dependsOn(AddOn addOn)1837     public boolean dependsOn(AddOn addOn) {
1838         if (dependencies == null || dependencies.getAddOns().isEmpty()) {
1839             return false;
1840         }
1841 
1842         return dependsOn(dependencies.getAddOns(), addOn);
1843     }
1844 
1845     /**
1846      * Tells whether or not this add-on has a (direct) dependency on any of the given {@code addOns}
1847      * (including version).
1848      *
1849      * @param addOns the add-ons that will be checked
1850      * @return {@code true} if it depends on any of the given add-ons, {@code false} otherwise.
1851      * @since 2.4.0
1852      */
dependsOn(Collection<AddOn> addOns)1853     public boolean dependsOn(Collection<AddOn> addOns) {
1854         if (dependencies == null || dependencies.getAddOns().isEmpty()) {
1855             return false;
1856         }
1857 
1858         for (AddOn addOn : addOns) {
1859             if (dependsOn(addOn)) {
1860                 return true;
1861             }
1862         }
1863         return false;
1864     }
1865 
1866     @Override
toString()1867     public String toString() {
1868         StringBuilder strBuilder = new StringBuilder();
1869         strBuilder.append("[id=").append(id);
1870         strBuilder.append(", version=").append(version);
1871         strBuilder.append(']');
1872 
1873         return strBuilder.toString();
1874     }
1875 
1876     @Override
hashCode()1877     public int hashCode() {
1878         final int prime = 31;
1879         int result = 1;
1880         result = prime * result + ((id == null) ? 0 : id.hashCode());
1881         result = prime * result + version.hashCode();
1882         return result;
1883     }
1884 
1885     /** Two add-ons are considered equal if both add-ons have the same ID and version. */
1886     @Override
equals(Object obj)1887     public boolean equals(Object obj) {
1888         if (this == obj) {
1889             return true;
1890         }
1891         if (obj == null) {
1892             return false;
1893         }
1894         if (getClass() != obj.getClass()) {
1895             return false;
1896         }
1897         AddOn other = (AddOn) obj;
1898         if (id == null) {
1899             if (other.id != null) {
1900                 return false;
1901             }
1902         } else if (!id.equals(other.id)) {
1903             return false;
1904         }
1905         return version.equals(other.version);
1906     }
1907 
1908     /**
1909      * A library (JAR) bundled in the add-on.
1910      *
1911      * <p>Bundled libraries are copied and loaded from the file system, which allows to properly
1912      * maintain/access all JAR's data (e.g. module info, manifest, services).
1913      */
1914     static class Lib {
1915         private final String jarPath;
1916         private final String name;
1917         private URL fileSystemUrl;
1918 
Lib(String path)1919         Lib(String path) {
1920             jarPath = path;
1921             name = extractName(path);
1922         }
1923 
getJarPath()1924         String getJarPath() {
1925             return jarPath;
1926         }
1927 
getName()1928         String getName() {
1929             return name;
1930         }
1931 
getFileSystemUrl()1932         URL getFileSystemUrl() {
1933             return fileSystemUrl;
1934         }
1935 
setFileSystemUrl(URL fileSystemUrl)1936         void setFileSystemUrl(URL fileSystemUrl) {
1937             this.fileSystemUrl = fileSystemUrl;
1938         }
1939 
extractName(String path)1940         private static String extractName(String path) {
1941             int idx = path.lastIndexOf('/');
1942             if (idx == -1) {
1943                 return path;
1944             }
1945             return path.substring(idx + 1);
1946         }
1947 
1948         @Override
hashCode()1949         public int hashCode() {
1950             return Objects.hash(jarPath);
1951         }
1952 
1953         @Override
equals(Object obj)1954         public boolean equals(Object obj) {
1955             if (this == obj) {
1956                 return true;
1957             }
1958             if (obj == null) {
1959                 return false;
1960             }
1961             if (getClass() != obj.getClass()) {
1962                 return false;
1963             }
1964             return Objects.equals(jarPath, ((Lib) obj).jarPath);
1965         }
1966     }
1967 
1968     public abstract static class BaseRunRequirements {
1969 
1970         /**
1971          * The reason why an add-on can not be run because of a dependency.
1972          *
1973          * <p>More details of the issue can be obtained with the method {@code
1974          * RunRequirements.getDependencyIssueDetails()}. The exact contents are mentioned in each
1975          * {@code DependencyIssue} constant.
1976          *
1977          * @see AddOnRunRequirements#getDependencyIssueDetails()
1978          */
1979         public enum DependencyIssue {
1980 
1981             /**
1982              * A cyclic dependency was detected.
1983              *
1984              * <p>Issue details contain all the add-ons in the cyclic chain.
1985              */
1986             CYCLIC,
1987 
1988             /**
1989              * Older version of the add-on is still installed.
1990              *
1991              * <p>Issue details contain the old version.
1992              */
1993             OLDER_VERSION,
1994 
1995             /**
1996              * A dependency was not found.
1997              *
1998              * <p>Issue details contain the id of the add-on.
1999              */
2000             MISSING,
2001 
2002             /**
2003              * The dependency found has a older version than the version required.
2004              *
2005              * <p>Issue details contain the instance of the {@code AddOn} and the required version.
2006              *
2007              * @deprecated (2.7.0) No longer in use. It should be used just {@link #VERSION}.
2008              */
2009             @Deprecated
2010             PACKAGE_VERSION_NOT_BEFORE,
2011 
2012             /**
2013              * The dependency found has a newer version than the version required.
2014              *
2015              * <p>Issue details contain the instance of the {@code AddOn} and the required version.
2016              *
2017              * @deprecated (2.7.0) No longer in use. It should be used just {@link #VERSION}.
2018              */
2019             @Deprecated
2020             PACKAGE_VERSION_NOT_FROM,
2021 
2022             /**
2023              * The dependency found has a different version.
2024              *
2025              * <p>Issue details contain the instance of the {@code AddOn} and the required version.
2026              */
2027             VERSION
2028         }
2029 
2030         private final AddOn addOn;
2031         private final DirectedGraph<AddOn, DefaultEdge> dependencyTree;
2032         private Set<AddOn> dependencies;
2033 
2034         private DependencyIssue depIssue;
2035         private List<Object> issueDetails;
2036 
2037         private String minimumJavaVersion;
2038         private AddOn addOnMinimumJavaVersion;
2039         private AddOn addOnMissingLibs;
2040 
2041         private boolean runnable;
2042 
BaseRunRequirements(AddOn addOn)2043         private BaseRunRequirements(AddOn addOn) {
2044             this.addOn = addOn;
2045             dependencyTree = new DefaultDirectedGraph<>(DefaultEdge.class);
2046             dependencyTree.addVertex(addOn);
2047             runnable = true;
2048             issueDetails = Collections.emptyList();
2049         }
2050 
2051         /**
2052          * Gets the add-on that was tested to check if it can be run.
2053          *
2054          * @return the tested add-on
2055          */
getAddOn()2056         public AddOn getAddOn() {
2057             return addOn;
2058         }
2059 
2060         /**
2061          * Tells whether or not this add-on has a dependency issue.
2062          *
2063          * @return {@code true} if the add-on has a dependency issue, {@code false} otherwise
2064          * @see #getDependencyIssue()
2065          * @see #getDependencyIssueDetails()
2066          * @see DependencyIssue
2067          */
hasDependencyIssue()2068         public boolean hasDependencyIssue() {
2069             return (depIssue != null);
2070         }
2071 
2072         /**
2073          * Gets the dependency issue, if any.
2074          *
2075          * @return the {@code DependencyIssue} or {@code null}, if none
2076          * @see #hasDependencyIssue()
2077          * @see #getDependencyIssueDetails()
2078          * @see DependencyIssue
2079          */
getDependencyIssue()2080         public DependencyIssue getDependencyIssue() {
2081             return depIssue;
2082         }
2083 
2084         /**
2085          * Gets the details of the dependency issue, if any.
2086          *
2087          * @return a list containing the details of the issue or an empty list if none
2088          * @see #hasDependencyIssue()
2089          * @see #getDependencyIssue()
2090          * @see DependencyIssue
2091          */
getDependencyIssueDetails()2092         public List<Object> getDependencyIssueDetails() {
2093             return issueDetails;
2094         }
2095 
2096         /**
2097          * Tells whether or not this add-on can be run.
2098          *
2099          * @return {@code true} if the add-on can be run, {@code false} otherwise
2100          */
isRunnable()2101         public boolean isRunnable() {
2102             return runnable;
2103         }
2104 
setRunnable(boolean runnable)2105         protected void setRunnable(boolean runnable) {
2106             this.runnable = runnable;
2107         }
2108 
2109         /**
2110          * Gets the (found) dependencies of the add-on, including transitive dependencies.
2111          *
2112          * @return a set containing the dependencies of the add-on
2113          * @see AddOn#getIdsAddOnDependencies()
2114          */
getDependencies()2115         public Set<AddOn> getDependencies() {
2116             if (dependencies == null) {
2117                 dependencies = new HashSet<>();
2118                 for (TopologicalOrderIterator<AddOn, DefaultEdge> it =
2119                                 new TopologicalOrderIterator<>(dependencyTree);
2120                         it.hasNext(); ) {
2121                     dependencies.add(it.next());
2122                 }
2123                 dependencies.remove(addOn);
2124             }
2125             return Collections.unmodifiableSet(dependencies);
2126         }
2127 
setIssue(DependencyIssue issue, Object... details)2128         protected void setIssue(DependencyIssue issue, Object... details) {
2129             runnable = false;
2130             this.depIssue = issue;
2131             if (details != null) {
2132                 issueDetails = Arrays.asList(details);
2133             } else {
2134                 issueDetails = Collections.emptyList();
2135             }
2136         }
2137 
addDependency(AddOn parent, AddOn addOn)2138         protected boolean addDependency(AddOn parent, AddOn addOn) {
2139             if (parent == null) {
2140                 return true;
2141             }
2142 
2143             dependencyTree.addVertex(parent);
2144             dependencyTree.addVertex(addOn);
2145 
2146             dependencyTree.addEdge(parent, addOn);
2147 
2148             CycleDetector<AddOn, DefaultEdge> cycleDetector = new CycleDetector<>(dependencyTree);
2149             boolean cycle = cycleDetector.detectCycles();
2150             if (cycle) {
2151                 dependencies = cycleDetector.findCycles();
2152 
2153                 return false;
2154             }
2155             return true;
2156         }
2157 
2158         /**
2159          * Tells whether or not this add-on requires a newer Java version to run.
2160          *
2161          * <p>The requirement might be imposed by a dependency or the add-on itself. To check which
2162          * one use the methods {@code getAddOn()} and {@code getAddOnMinimumJavaVersion()}.
2163          *
2164          * @return {@code true} if the add-on requires a newer Java version, {@code false}
2165          *     otherwise.
2166          * @see #getAddOn()
2167          * @see #getAddOnMinimumJavaVersion()
2168          * @see #getMinimumJavaVersion()
2169          */
isNewerJavaVersionRequired()2170         public boolean isNewerJavaVersionRequired() {
2171             return (minimumJavaVersion != null);
2172         }
2173 
2174         /**
2175          * Gets the minimum Java version required to run the add-on.
2176          *
2177          * @return the Java version, or {@code null} if no minimum.
2178          * @see #isNewerJavaVersionRequired()
2179          * @see #getAddOn()
2180          * @see #getAddOnMinimumJavaVersion()
2181          */
getMinimumJavaVersion()2182         public String getMinimumJavaVersion() {
2183             return minimumJavaVersion;
2184         }
2185 
2186         /**
2187          * Gets the add-on that requires the minimum Java version.
2188          *
2189          * @return the add-on, or {@code null} if no minimum.
2190          * @see #isNewerJavaVersionRequired()
2191          * @see #getMinimumJavaVersion()
2192          * @see #getAddOn()
2193          */
getAddOnMinimumJavaVersion()2194         public AddOn getAddOnMinimumJavaVersion() {
2195             return addOnMinimumJavaVersion;
2196         }
2197 
setMinimumJavaVersionIssue(AddOn srcAddOn, String requiredVersion)2198         protected void setMinimumJavaVersionIssue(AddOn srcAddOn, String requiredVersion) {
2199             setRunnable(false);
2200 
2201             if (minimumJavaVersion == null) {
2202                 setMinimumJavaVersion(srcAddOn, requiredVersion);
2203             } else if (getJavaVersion(requiredVersion) > getJavaVersion(minimumJavaVersion)) {
2204                 setMinimumJavaVersion(srcAddOn, requiredVersion);
2205             }
2206         }
2207 
setMinimumJavaVersion(AddOn srcAddOn, String requiredVersion)2208         private void setMinimumJavaVersion(AddOn srcAddOn, String requiredVersion) {
2209             addOnMinimumJavaVersion = srcAddOn;
2210             minimumJavaVersion = requiredVersion;
2211         }
2212 
2213         /**
2214          * Tells whether or not this add-on has missing libraries (that is, not present in the file
2215          * system).
2216          *
2217          * <p>The issue might be caused by a dependency or the add-on itself. To check which one use
2218          * the methods {@link #getAddOn()} and {@link #getAddOnMissingLibs()}.
2219          *
2220          * @return {@code true} if the add-on has missing libraries, {@code false} otherwise.
2221          * @since 2.9.0
2222          */
hasMissingLibs()2223         public boolean hasMissingLibs() {
2224             return addOnMissingLibs != null;
2225         }
2226 
2227         /**
2228          * Gets the add-on that has missing libraries.
2229          *
2230          * @return the add-on, or {@code null} if none.
2231          * @see #hasMissingLibs()
2232          * @see #getAddOn()
2233          * @since 2.9.0
2234          */
getAddOnMissingLibs()2235         public AddOn getAddOnMissingLibs() {
2236             return addOnMissingLibs;
2237         }
2238 
setMissingLibsIssue(AddOn srcAddOn)2239         protected void setMissingLibsIssue(AddOn srcAddOn) {
2240             setRunnable(false);
2241             addOnMissingLibs = srcAddOn;
2242         }
2243     }
2244 
2245     /**
2246      * The requirements to run an {@code AddOn}.
2247      *
2248      * <p>It can be used to check if an add-on can or not be run, which requirements it has (for
2249      * example, minimum Java version or dependency add-ons) and which issues prevent it from being
2250      * run, if any.
2251      *
2252      * @since 2.4.0
2253      */
2254     public static class AddOnRunRequirements extends BaseRunRequirements {
2255 
2256         private List<ExtensionRunRequirements> addExtensionsRequirements;
2257 
AddOnRunRequirements(AddOn addOn)2258         private AddOnRunRequirements(AddOn addOn) {
2259             super(addOn);
2260         }
2261 
2262         /**
2263          * Gets the run requirements of the extensions that have dependencies.
2264          *
2265          * @return a {@code List} containing the requirements of each extension that have
2266          *     dependencies
2267          * @see #hasExtensionsWithRunningIssues()
2268          */
getExtensionRequirements()2269         public List<ExtensionRunRequirements> getExtensionRequirements() {
2270             if (addExtensionsRequirements == null) {
2271                 addExtensionsRequirements = Collections.emptyList();
2272             }
2273             return addExtensionsRequirements;
2274         }
2275 
2276         /**
2277          * Tells whether or not there's at least one extension with running issues.
2278          *
2279          * @return {@code true} if at least one extension has running issues, {@code false}
2280          *     otherwise.
2281          * @see #getExtensionRequirements()
2282          */
hasExtensionsWithRunningIssues()2283         public boolean hasExtensionsWithRunningIssues() {
2284             for (ExtensionRunRequirements reqs : getExtensionRequirements()) {
2285                 if (!reqs.isRunnable()) {
2286                     return true;
2287                 }
2288             }
2289             return false;
2290         }
2291 
addExtensionRequirements(ExtensionRunRequirements extension)2292         protected void addExtensionRequirements(ExtensionRunRequirements extension) {
2293             if (addExtensionsRequirements == null) {
2294                 addExtensionsRequirements = new ArrayList<>(5);
2295             }
2296             addExtensionsRequirements.add(extension);
2297         }
2298     }
2299 
2300     /**
2301      * The requirements to run an {@code extension} (with add-on dependencies).
2302      *
2303      * <p>It can be used to check if an extension can or not be run, which requirements it has (for
2304      * example, dependency add-ons) and which issues prevent it from being run, if any.
2305      *
2306      * @since 2.4.0
2307      */
2308     public static class ExtensionRunRequirements extends BaseRunRequirements {
2309 
2310         private final String classname;
2311 
ExtensionRunRequirements(AddOn addOn, String classname)2312         private ExtensionRunRequirements(AddOn addOn, String classname) {
2313             super(addOn);
2314             this.classname = classname;
2315         }
2316 
2317         /**
2318          * Gets the classname of the extension.
2319          *
2320          * @return the classname of the extension
2321          */
getClassname()2322         public String getClassname() {
2323             return classname;
2324         }
2325     }
2326 
2327     /**
2328      * The data of the bundle declared in the manifest of an add-on.
2329      *
2330      * <p>Used to load a {@link ResourceBundle}.
2331      *
2332      * @since 2.8.0
2333      */
2334     public static final class BundleData {
2335 
2336         private static final BundleData EMPTY_BUNDLE = new BundleData();
2337 
2338         private final String baseName;
2339         private final String prefix;
2340 
BundleData()2341         private BundleData() {
2342             this("", "");
2343         }
2344 
BundleData(String baseName, String prefix)2345         private BundleData(String baseName, String prefix) {
2346             this.baseName = baseName;
2347             this.prefix = prefix;
2348         }
2349 
2350         /**
2351          * Tells whether or not the the bundle data is empty.
2352          *
2353          * <p>An empty {@code BundleData} does not contain any information to load a {@link
2354          * ResourceBundle}.
2355          *
2356          * @return {@code true} if empty, {@code false} otherwise.
2357          */
isEmpty()2358         public boolean isEmpty() {
2359             return this == EMPTY_BUNDLE;
2360         }
2361 
2362         /**
2363          * Gets the base name of the bundle.
2364          *
2365          * @return the base name, or empty if not defined.
2366          */
getBaseName()2367         public String getBaseName() {
2368             return baseName;
2369         }
2370 
2371         /**
2372          * Gets the prefix of the bundle, to add into a {@link
2373          * org.zaproxy.zap.utils.I18N#addMessageBundle(String, ResourceBundle) I18N}.
2374          *
2375          * @return the prefix, or empty if not defined.
2376          */
getPrefix()2377         public String getPrefix() {
2378             return prefix;
2379         }
2380     }
2381 
2382     /**
2383      * The data to load a {@link javax.help.HelpSet HelpSet}, declared in the manifest of an add-on.
2384      *
2385      * <p>Used to dynamically add/remove the help of the add-on.
2386      *
2387      * @since 2.8.0
2388      */
2389     public static final class HelpSetData {
2390 
2391         private static final HelpSetData EMPTY_HELP_SET = new HelpSetData();
2392 
2393         private final String baseName;
2394         private final String localeToken;
2395 
HelpSetData()2396         private HelpSetData() {
2397             this("", "");
2398         }
2399 
HelpSetData(String baseName, String localeToken)2400         private HelpSetData(String baseName, String localeToken) {
2401             this.baseName = baseName;
2402             this.localeToken = localeToken;
2403         }
2404 
2405         /**
2406          * Tells whether or not the the HelpSet data is empty.
2407          *
2408          * <p>An empty {@code HelpSetData} does not contain any information to load the help.
2409          *
2410          * @return {@code true} if empty, {@code false} otherwise.
2411          */
isEmpty()2412         public boolean isEmpty() {
2413             return this == EMPTY_HELP_SET;
2414         }
2415 
2416         /**
2417          * Gets the base name of the HelpSet file.
2418          *
2419          * @return the base name, or empty if not defined.
2420          */
getBaseName()2421         public String getBaseName() {
2422             return baseName;
2423         }
2424 
2425         /**
2426          * Gets the locale token.
2427          *
2428          * @return the locale token, or empty if not defined.
2429          */
getLocaleToken()2430         public String getLocaleToken() {
2431             return localeToken;
2432         }
2433     }
2434 
2435     /**
2436      * An {@link IOException} that indicates that a file is not a valid add-on.
2437      *
2438      * @since 2.8.0
2439      * @see #getValidationResult()
2440      */
2441     public static class InvalidAddOnException extends IOException {
2442 
2443         private static final long serialVersionUID = 1L;
2444 
2445         private final ValidationResult validationResult;
2446 
InvalidAddOnException(ValidationResult validationResult)2447         private InvalidAddOnException(ValidationResult validationResult) {
2448             super(getRootCauseMessage(validationResult), validationResult.getException());
2449             this.validationResult = validationResult;
2450         }
2451 
2452         /**
2453          * Gets the result of validating the add-on.
2454          *
2455          * @return the result, never {@code null}.
2456          */
getValidationResult()2457         public ValidationResult getValidationResult() {
2458             return validationResult;
2459         }
2460     }
2461 
getRootCauseMessage(ValidationResult validationResult)2462     private static String getRootCauseMessage(ValidationResult validationResult) {
2463         Exception exception = validationResult.getException();
2464         if (exception == null) {
2465             return validationResult.getValidity().toString();
2466         }
2467         Throwable root = ExceptionUtils.getRootCause(exception);
2468         return root == null ? exception.getLocalizedMessage() : root.getLocalizedMessage();
2469     }
2470 
getJavaVersion(String javaVersion)2471     private static int getJavaVersion(String javaVersion) {
2472         return toVersionInt(toJavaVersionIntArray(javaVersion, 2));
2473     }
2474 
2475     // NOTE: Following 2 methods copied from org.apache.commons.lang.SystemUtils version 2.6 because
2476     // of constrained visibility
toJavaVersionIntArray(String version, int limit)2477     private static int[] toJavaVersionIntArray(String version, int limit) {
2478         if (version == null) {
2479             return ArrayUtils.EMPTY_INT_ARRAY;
2480         }
2481         String[] strings = StringUtils.split(version, "._- ");
2482         int[] ints = new int[Math.min(limit, strings.length)];
2483         int j = 0;
2484         for (int i = 0; i < strings.length && j < limit; i++) {
2485             String s = strings[i];
2486             if (s.length() > 0) {
2487                 try {
2488                     ints[j] = Integer.parseInt(s);
2489                     j++;
2490                 } catch (Exception e) {
2491                 }
2492             }
2493         }
2494         if (ints.length > j) {
2495             int[] newInts = new int[j];
2496             System.arraycopy(ints, 0, newInts, 0, j);
2497             ints = newInts;
2498         }
2499         return ints;
2500     }
2501 
toVersionInt(int[] javaVersions)2502     private static int toVersionInt(int[] javaVersions) {
2503         if (javaVersions == null) {
2504             return 0;
2505         }
2506         int intVersion = 0;
2507         int len = javaVersions.length;
2508         if (len >= 1) {
2509             intVersion = javaVersions[0] * 100;
2510         }
2511         if (len >= 2) {
2512             intVersion += javaVersions[1] * 10;
2513         }
2514         if (len >= 3) {
2515             intVersion += javaVersions[2];
2516         }
2517         return intVersion;
2518     }
2519 }
2520