/* * Zed Attack Proxy (ZAP) and its related class files. * * ZAP is an HTTP/HTTPS proxy for assessing web application security. * * Copyright 2014 The ZAP Development Team * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.zaproxy.zap.extension.help; import java.util.Collections; import java.util.HashMap; import java.util.Map; import javax.help.MergeHelpUtilities; import javax.help.NavigatorView; import javax.help.SortMerge; import javax.help.TreeItem; import javax.help.UniteAppendMerge; import javax.swing.tree.DefaultMutableTreeNode; import javax.swing.tree.TreeNode; /** * <strong>NOTE:</strong> The name (and package) of the class must not be changed lightly! It will break help's TOC merging at * runtime. The name and package is hard coded in helpset files and is also referenced in others for documentation purposes. * (END NOTE) * <p> * An {@code UniteAppendMerge} that takes into account the "tocid" attribute of the "tocitem" elements to do the merging. The * "tocid" attribute is used to facilitate the merging of the TOC with internationalised helpsets. The node names and targets do * not provide enough information to do a safe merging. The name might not be the same (when it is translated) and the target is * not present in all nodes. In those cases a "tocid" attribute is set to unambiguously identify those nodes. * <p> * The merge depends on the information provided by the "tocitem" elements and if they use or not the "tocid" attribute. * <p> * First the nodes are compared to check if they have the same "tocid" and merged if they have.<br> * Otherwise and for backward compatibility with helpsets that still do not use the attribute "tocid" a forced merge is * performed if some predefined requirements are met. The requirements are as follow: * <ol> * <li>The master node must have an attribute "tocid";</li> * <li>The master node "tocid" must be present in the map of forced merges ({@code TOC_IDS_FORCE_MERGE_MAP});</li> * <li>The slave node must have the same name as the one defined in the value ({@code ForceMergeRequirement}) of the map of * forced merges;</li> * <li>The master node level must be the same as the one defined in the value of the map of forced merges (the level is used to * prevent matching other nodes with the same name in the tree).</li> * </ol> * <p> * If none of the aforementioned merges are performed the actual merging will be done as defined by {@code UniteAppendMerge}. * * @see UniteAppendMerge * @see ZapTocItem * @see ZapTocView * @see #TOC_IDS_FORCE_MERGE_MAP * @see ZapTocMerger.ForceMergeRequirement */ // Note: This class contains copied (verbatim) code from the base class UniteAppendMerge. public class ZapTocMerger extends UniteAppendMerge { private static final String DEFAULT_MERGE_TYPE = ZapTocMerger.class.getCanonicalName(); private static final String ADDONS_TOC_ID = "addons"; /** * A map containing the requirements to do forced merging. * <p> * The map key corresponds to the attribute "tocid" of the "tocitem" elements as defined in the toc.xml file. The value has * the requirements that should be met to actually do the merging. */ public static final Map<String, ForceMergeRequirement> TOC_IDS_FORCE_MERGE_MAP; static { Map<String, ForceMergeRequirement> tempMap = new HashMap<>(); // Note: The attribute "tocid" should match the ones defined in the toc.xml file. // Note 2: the TOC tree node names are hard coded because the "older" add-ons use the same (hard coded) names. tempMap.put("toplevelitem", new ForceMergeRequirement(1, "ZAP User Guide")); tempMap.put(ADDONS_TOC_ID, new ForceMergeRequirement(2, "Add Ons")); TOC_IDS_FORCE_MERGE_MAP = Collections.unmodifiableMap(tempMap); } public ZapTocMerger(NavigatorView master, NavigatorView slave) { super(master, slave); } /** * Processes unite-append merge * * @param node The master node * @return Merged master node */ // Note: the implementation and JavaDoc has been copied (verbatim) from the base method to call the method // ZapTocMerger#mergeNodes(TreeNode, TreeNode) instead of UniteAppendMerge#mergeNodes(TreeNode, TreeNode). @Override public TreeNode processMerge(TreeNode node) { DefaultMutableTreeNode masterNode = (DefaultMutableTreeNode) node; // if master and slave are the same object return the // masterNode if (masterNode.equals(slaveTopNode)) { return masterNode; } // If there are not children in slaveTopNode return the // masterNode if (slaveTopNode.getChildCount() == 0) { return masterNode; } mergeNodes(masterNode, slaveTopNode); return masterNode; } /** * Merge Nodes. Merge two nodes according to the merging rules of the masterNode. Each Subclass should override this * implementation. * * @param master The master node to merge with * @param slave The node to merge into the master */ // Note: the implementation and JavaDoc has been copied (verbatim) from UniteAppendMerge#mergeNodes(TreeNode, TreeNode) // except for the call to doCustomMerge(DefaultMutableTreeNode, DefaultMutableTreeNode) and the calls to // MergeHelpUtilities.mergeNode* which is set, using DEFAULT_MERGE_TYPE, to merge with this class instead. public static void mergeNodes(TreeNode master, TreeNode slave) { DefaultMutableTreeNode masterNode = (DefaultMutableTreeNode) master; DefaultMutableTreeNode slaveNode = (DefaultMutableTreeNode) slave; int masterCnt = masterNode.getChildCount(); // loop thru the slaves while (slaveNode.getChildCount() > 0) { DefaultMutableTreeNode slaveNodeChild = (DefaultMutableTreeNode) slaveNode.getFirstChild(); // loop thru the master children for (int m = 0; m < masterCnt; m++) { DefaultMutableTreeNode masterAtM = (DefaultMutableTreeNode) masterNode.getChildAt(m); if (doCustomMerge(slaveNodeChild, masterAtM)) { slaveNodeChild = null; break; } // see if the names are the same if (MergeHelpUtilities.compareNames(masterAtM, slaveNodeChild) == 0) { if (MergeHelpUtilities.haveEqualID(masterAtM, slaveNodeChild)) { // ID and name the same merge the slave node in MergeHelpUtilities.mergeNodes(DEFAULT_MERGE_TYPE, masterAtM, slaveNodeChild); // Need to remove the slaveNodeChild from the list slaveNodeChild.removeFromParent(); slaveNodeChild = null; break; } // Names are the same but the ID are not // Mark the nodes and add the slaveChild MergeHelpUtilities.markNodes(masterAtM, slaveNodeChild); masterNode.add(slaveNodeChild); MergeHelpUtilities.mergeNodeChildren(DEFAULT_MERGE_TYPE, slaveNodeChild); slaveNodeChild = null; break; } } if (slaveNodeChild != null) { masterNode.add(slaveNodeChild); MergeHelpUtilities.mergeNodeChildren(DEFAULT_MERGE_TYPE, slaveNodeChild); } } // There are no more children. // Remove slaveNode from it's parent slaveNode.removeFromParent(); slaveNode = null; } private static boolean doCustomMerge(DefaultMutableTreeNode slaveNodeChild, DefaultMutableTreeNode masterAtM) { if (isSameTOCID(masterAtM, slaveNodeChild) || isForceMerge(masterAtM, slaveNodeChild)) { MergeHelpUtilities.mergeNodes(DEFAULT_MERGE_TYPE, masterAtM, slaveNodeChild); slaveNodeChild.removeFromParent(); if (ADDONS_TOC_ID.equals(getTOCID(masterAtM))) { SortMerge.sortNode(masterAtM, MergeHelpUtilities.getLocale(masterAtM)); } return true; } return false; } private static boolean isSameTOCID(DefaultMutableTreeNode masterAtM, DefaultMutableTreeNode slaveNodeChild) { String slaveTocId = getTOCID(slaveNodeChild); if (slaveTocId == null) { return false; } return slaveTocId.equals(getTOCID(masterAtM)); } private static String getTOCID(DefaultMutableTreeNode node) { TreeItem treeItem = (TreeItem) node.getUserObject(); if (treeItem != null && (treeItem instanceof ZapTocItem)) { return ((ZapTocItem) treeItem).getTocId(); } return null; } private static boolean isForceMerge(DefaultMutableTreeNode masterAtM, DefaultMutableTreeNode slaveNodeChild) { TreeItem slaveNodeChildTreeItem = (TreeItem) slaveNodeChild.getUserObject(); String slaveName = slaveNodeChildTreeItem.getName(); if (slaveName == null) { return false; } String tocId = getTOCID(masterAtM); if (tocId != null) { ForceMergeRequirement forceMergeRequirement = TOC_IDS_FORCE_MERGE_MAP.get(tocId); if (forceMergeRequirement != null && forceMergeRequirement.isSameMasterLevel(masterAtM.getLevel()) && forceMergeRequirement.isSameSlaveName(slaveName)) { return true; } } return false; } /** * Merge Node Children. Merge the children of a node according to the merging rules of the parent. Each subclass must * implement this method * * @param node The parent node from which the children are merged */ // Note: the implementation and JavaDoc has been copied (verbatim) from UniteAppendMerge#mergeNodeChildren(TreeNode) except // for the call to MergeHelpUtilities.mergeNodeChildren(String, child) which is set, using DEFAULT_MERGE_TYPE, to merge with // this class instead. public static void mergeNodeChildren(TreeNode node) { DefaultMutableTreeNode masterNode = (DefaultMutableTreeNode) node; // The rules are there are no rules. Nothing else needs to be done // except for merging through the children for (int i = 0; i < masterNode.getChildCount(); i++) { DefaultMutableTreeNode child = (DefaultMutableTreeNode) masterNode.getChildAt(i); if (!child.isLeaf()) { MergeHelpUtilities.mergeNodeChildren(DEFAULT_MERGE_TYPE, child); } } } /** * The {@code ForceMergeRequirement} class contains the requirements to do a forced merging. * * @see ForceMergeRequirement#ForceMergeRequirement(int, String) */ public static final class ForceMergeRequirement { private final int masterNodeLevel; private final String slaveNodeName; /** * Creates a {@code ForceMergeRequirement} instance. * * @param masterNodeLevel the level of the master node in the TOC tree * @param slaveNodeName the name of the slave node * @see DefaultMutableTreeNode#getLevel() */ public ForceMergeRequirement(int masterNodeLevel, String slaveNodeName) { if (masterNodeLevel < 0) { throw new IllegalArgumentException("Parameter masterNodeLevel must not be negative."); } if (slaveNodeName == null || slaveNodeName.isEmpty()) { throw new IllegalArgumentException("Parameter slaveNodeName must not be null."); } this.masterNodeLevel = masterNodeLevel; this.slaveNodeName = slaveNodeName; } public boolean isSameMasterLevel(int level) { return (masterNodeLevel == level); } public boolean isSameSlaveName(String name) { return slaveNodeName.equals(name); } } }