/*
 * Copyright (c) 2007-2013, Oracle and/or its affiliates. All rights reserved.
 * ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
 */
/*
 * Copyright  1999-2004 The Apache Software Foundation.
 *
 *  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 com.sun.org.apache.xml.internal.security.utils;


import java.io.IOException;
import java.io.OutputStream;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import com.sun.org.apache.xml.internal.security.c14n.CanonicalizationException;
import com.sun.org.apache.xml.internal.security.c14n.Canonicalizer;
import com.sun.org.apache.xml.internal.security.c14n.InvalidCanonicalizerException;
import org.w3c.dom.Attr;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.w3c.dom.Text;



/**
 * DOM and XML accessibility and comfort functions.
 *
 * @author Christian Geuer-Pollmann
 */
public class XMLUtils {

   private static boolean ignoreLineBreaks =
      AccessController.doPrivileged(new PrivilegedAction<Boolean>() {
         public Boolean run() {
            return Boolean.getBoolean
               ("com.sun.org.apache.xml.internal.security.ignoreLineBreaks");
         }
      });

    private static volatile String dsPrefix = "ds";
    private static volatile String xencPrefix = "xenc";

   private static final java.util.logging.Logger log =
       java.util.logging.Logger.getLogger(XMLUtils.class.getName());

   /**
    * Constructor XMLUtils
    *
    */
   private XMLUtils() {

      // we don't allow instantiation
   }

    /**
     * Set the prefix for the digital signature namespace
     * @param prefix the new prefix for the digital signature namespace
     */
    public static void setDsPrefix(String prefix) {
        dsPrefix = prefix;
    }

    /**
     * Set the prefix for the encryption namespace
     * @param prefix the new prefix for the encryption namespace
     */
    public static void setXencPrefix(String prefix) {
        xencPrefix = prefix;
    }

   public static Element getNextElement(Node el) {
           while ((el!=null) && (el.getNodeType()!=Node.ELEMENT_NODE)) {
                   el=el.getNextSibling();
           }
           return (Element)el;

   }

   /**
    * @param rootNode
    * @param result
    * @param exclude
    * @param com wheather comments or not
    */
   public static void getSet(Node rootNode,Set<Node> result,Node exclude ,boolean com) {
          if ((exclude!=null) && isDescendantOrSelf(exclude,rootNode)){
                return;
      }
      getSetRec(rootNode,result,exclude,com);
   }

   @SuppressWarnings("fallthrough")
   static final void getSetRec(final Node rootNode,final Set<Node> result,
        final Node exclude ,final boolean com) {
           //Set result = new HashSet();
       if (rootNode==exclude) {
          return;
       }
           switch (rootNode.getNodeType()) {
                case Node.ELEMENT_NODE:
                                result.add(rootNode);
                        Element el=(Element)rootNode;
                if (el.hasAttributes()) {
                                NamedNodeMap nl = ((Element)rootNode).getAttributes();
                                for (int i=0;i<nl.getLength();i++) {
                                        result.add(nl.item(i));
                                }
                }
                //no return keep working - ignore fallthrough warning
                case Node.DOCUMENT_NODE:
                                for (Node r=rootNode.getFirstChild();r!=null;r=r.getNextSibling()){
                                        if (r.getNodeType()==Node.TEXT_NODE) {
                                                result.add(r);
                                                while ((r!=null) && (r.getNodeType()==Node.TEXT_NODE)) {
                                                        r=r.getNextSibling();
                                                }
                                                if (r==null)
                                                        return;
                                        }
                                        getSetRec(r,result,exclude,com);
                                }
                                return;
                        case Node.COMMENT_NODE:
                                if (com) {
                                        result.add(rootNode);
                                }
                            return;
                        case Node.DOCUMENT_TYPE_NODE:
                                return;
                        default:
                                result.add(rootNode);
           }
           return;
   }


   /**
    * Outputs a DOM tree to an {@link OutputStream}.
    *
    * @param contextNode root node of the DOM tree
    * @param os the {@link OutputStream}
    */
   public static void outputDOM(Node contextNode, OutputStream os) {
      XMLUtils.outputDOM(contextNode, os, false);
   }

   /**
    * Outputs a DOM tree to an {@link OutputStream}. <I>If an Exception is
    * thrown during execution, it's StackTrace is output to System.out, but the
    * Exception is not re-thrown.</I>
    *
    * @param contextNode root node of the DOM tree
    * @param os the {@link OutputStream}
    * @param addPreamble
    */
   public static void outputDOM(Node contextNode, OutputStream os,
                                boolean addPreamble) {

      try {
         if (addPreamble) {
            os.write("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n".getBytes());
         }

         os.write(
            Canonicalizer.getInstance(
               Canonicalizer.ALGO_ID_C14N_WITH_COMMENTS).canonicalizeSubtree(
               contextNode));
      } catch (IOException ex) {}
      catch (InvalidCanonicalizerException ex) {
         ex.printStackTrace();
      } catch (CanonicalizationException ex) {
         ex.printStackTrace();
      }
   }

   /**
    * Serializes the <CODE>contextNode</CODE> into the OutputStream, <I>but
    * supresses all Exceptions</I>.
    * <BR />
    * NOTE: <I>This should only be used for debugging purposes,
    * NOT in a production environment; this method ignores all exceptions,
    * so you won't notice if something goes wrong. If you're asking what is to
    * be used in a production environment, simply use the code inside the
    * <code>try{}</code> statement, but handle the Exceptions appropriately.</I>
    *
    * @param contextNode
    * @param os
    */
   public static void outputDOMc14nWithComments(Node contextNode,
           OutputStream os) {

      try {
         os.write(
            Canonicalizer.getInstance(
               Canonicalizer.ALGO_ID_C14N_WITH_COMMENTS).canonicalizeSubtree(
               contextNode));
      } catch (IOException ex) {

         // throw new RuntimeException(ex.getMessage());
      } catch (InvalidCanonicalizerException ex) {

         // throw new RuntimeException(ex.getMessage());
      } catch (CanonicalizationException ex) {

         // throw new RuntimeException(ex.getMessage());
      }
   }


   /**
    * Method getFullTextChildrenFromElement
    *
    * @param element
    * @return the string of chi;ds
    */
   public static String getFullTextChildrenFromElement(Element element) {

      StringBuffer sb = new StringBuffer();
      NodeList children = element.getChildNodes();
      int iMax = children.getLength();

      for (int i = 0; i < iMax; i++) {
         Node curr = children.item(i);

         if (curr.getNodeType() == Node.TEXT_NODE) {
            sb.append(((Text) curr).getData());
         }
      }

      return sb.toString();
   }

   static Map<String, String> namePrefixes=new HashMap<String, String>();

   /**
    * Creates an Element in the XML Signature specification namespace.
    *
    * @param doc the factory Document
    * @param elementName the local name of the Element
    * @return the Element
    */
   public static Element createElementInSignatureSpace(Document doc,
           String elementName) {

      if (doc == null) {
         throw new RuntimeException("Document is null");
      }

      if ((dsPrefix == null) || (dsPrefix.length() == 0)) {
         return doc.createElementNS(Constants.SignatureSpecNS, elementName);
      }
      String namePrefix= namePrefixes.get(elementName);
      if (namePrefix==null) {
          StringBuffer tag=new StringBuffer(dsPrefix);
          tag.append(':');
          tag.append(elementName);
          namePrefix=tag.toString();
          namePrefixes.put(elementName,namePrefix);
      }
      return doc.createElementNS(Constants.SignatureSpecNS, namePrefix);
   }

    /**
     * Returns true if the element is in XML Signature namespace and the local
     * name equals the supplied one.
     *
     * @param element
     * @param localName
     * @return true if the element is in XML Signature namespace and the local name equals the supplied one
     */
    public static boolean elementIsInSignatureSpace(Element element, String localName) {
        if (element == null) {
            return false;
        }

        return Constants.SignatureSpecNS.equals(element.getNamespaceURI())
            && element.getLocalName().equals(localName);
    }

    /**
     * Returns true if the element is in XML Encryption namespace and the local
     * name equals the supplied one.
     *
     * @param element
     * @param localName
     * @return true if the element is in XML Encryption namespace and the local name equals the supplied one
     */
    public static boolean elementIsInEncryptionSpace(Element element, String localName) {
        if (element == null) {
            return false;
        }
        return EncryptionConstants.EncryptionSpecNS.equals(element.getNamespaceURI())
            && element.getLocalName().equals(localName);
    }

   /**
    * This method returns the owner document of a particular node.
    * This method is necessary because it <I>always</I> returns a
    * {@link Document}. {@link Node#getOwnerDocument} returns <CODE>null</CODE>
    * if the {@link Node} is a {@link Document}.
    *
    * @param node
    * @return the owner document of the node
    */
   public static Document getOwnerDocument(Node node) {

      if (node.getNodeType() == Node.DOCUMENT_NODE) {
         return (Document) node;
      }
         try {
            return node.getOwnerDocument();
         } catch (NullPointerException npe) {
            throw new NullPointerException(I18n.translate("endorsed.jdk1.4.0")
                                           + " Original message was \""
                                           + npe.getMessage() + "\"");
         }

   }

    /**
     * This method returns the first non-null owner document of the Node's in this Set.
     * This method is necessary because it <I>always</I> returns a
     * {@link Document}. {@link Node#getOwnerDocument} returns <CODE>null</CODE>
     * if the {@link Node} is a {@link Document}.
     *
     * @param xpathNodeSet
     * @return the owner document
     */
    public static Document getOwnerDocument(Set<Node> xpathNodeSet) {
       NullPointerException npe = null;
       for (Node node : xpathNodeSet) {
           int nodeType =node.getNodeType();
           if (nodeType == Node.DOCUMENT_NODE) {
              return (Document) node;
           }
              try {
                 if (nodeType==Node.ATTRIBUTE_NODE) {
                    return ((Attr)node).getOwnerElement().getOwnerDocument();
                 }
                 return node.getOwnerDocument();
              } catch (NullPointerException e) {
                  npe = e;
              }

       }
       throw new NullPointerException(I18n.translate("endorsed.jdk1.4.0")
                                       + " Original message was \""
                                       + (npe == null ? "" : npe.getMessage()) + "\"");
    }

    /**
     * Method createDSctx
     *
     * @param doc
     * @param prefix
     * @param namespace
     * @return the element.
     */
    public static Element createDSctx(Document doc, String prefix,
                                      String namespace) {

       if ((prefix == null) || (prefix.trim().length() == 0)) {
          throw new IllegalArgumentException("You must supply a prefix");
       }

       Element ctx = doc.createElementNS(null, "namespaceContext");

       ctx.setAttributeNS(Constants.NamespaceSpecNS, "xmlns:" + prefix.trim(),
                          namespace);

       return ctx;
    }

   /**
    * Method addReturnToElement
    *
    * @param e
    */
   public static void addReturnToElement(Element e) {

      if (!ignoreLineBreaks) {
         Document doc = e.getOwnerDocument();
         e.appendChild(doc.createTextNode("\n"));
      }
   }

   public static void addReturnToElement(Document doc, HelperNodeList nl) {
      if (!ignoreLineBreaks) {
         nl.appendChild(doc.createTextNode("\n"));
      }
   }

   public static void addReturnBeforeChild(Element e, Node child) {
      if (!ignoreLineBreaks) {
         Document doc = e.getOwnerDocument();
         e.insertBefore(doc.createTextNode("\n"), child);
      }
   }

   /**
    * Method convertNodelistToSet
    *
    * @param xpathNodeSet
    * @return the set with the nodelist
    */
   public static Set<Node> convertNodelistToSet(NodeList xpathNodeSet) {

      if (xpathNodeSet == null) {
         return new HashSet<Node>();
      }

      int length = xpathNodeSet.getLength();
      Set<Node> set = new HashSet<Node>(length);

      for (int i = 0; i < length; i++) {
         set.add(xpathNodeSet.item(i));
      }

      return set;
   }


   /**
    * This method spreads all namespace attributes in a DOM document to their
    * children. This is needed because the XML Signature XPath transform
    * must evaluate the XPath against all nodes in the input, even against
    * XPath namespace nodes. Through a bug in XalanJ2, the namespace nodes are
    * not fully visible in the Xalan XPath model, so we have to do this by
    * hand in DOM spaces so that the nodes become visible in XPath space.
    *
    * @param doc
    * @see <A HREF="http://nagoya.apache.org/bugzilla/show_bug.cgi?id=2650">Namespace axis resolution is not XPath compliant </A>
    */
   public static void circumventBug2650(Document doc) {

      Element documentElement = doc.getDocumentElement();

      // if the document element has no xmlns definition, we add xmlns=""
      Attr xmlnsAttr =
         documentElement.getAttributeNodeNS(Constants.NamespaceSpecNS, "xmlns");

      if (xmlnsAttr == null) {
         documentElement.setAttributeNS(Constants.NamespaceSpecNS, "xmlns", "");
      }

      XMLUtils.circumventBug2650internal(doc);
   }

   /**
    * This is the work horse for {@link #circumventBug2650}.
    *
    * @param node
    * @see <A HREF="http://nagoya.apache.org/bugzilla/show_bug.cgi?id=2650">Namespace axis resolution is not XPath compliant </A>
    */
   @SuppressWarnings("fallthrough")
   private static void circumventBug2650internal(Node node) {
           Node parent=null;
           Node sibling=null;
           final String namespaceNs=Constants.NamespaceSpecNS;
           do {
         switch (node.getNodeType()) {
         case Node.ELEMENT_NODE :
                 Element element = (Element) node;
             if (!element.hasChildNodes())
                 break;
             if (element.hasAttributes()) {
             NamedNodeMap attributes = element.getAttributes();
             int attributesLength = attributes.getLength();

             for (Node child = element.getFirstChild(); child!=null;
                child=child.getNextSibling()) {

                if (child.getNodeType() != Node.ELEMENT_NODE) {
                        continue;
                }
                Element childElement = (Element) child;

                for (int i = 0; i < attributesLength; i++) {
                        Attr currentAttr = (Attr) attributes.item(i);
                        if (namespaceNs!=currentAttr.getNamespaceURI())
                                continue;
                        if (childElement.hasAttributeNS(namespaceNs,
                                                        currentAttr.getLocalName())) {
                                        continue;
                        }
                        childElement.setAttributeNS(namespaceNs,
                                                currentAttr.getName(),
                                                currentAttr.getNodeValue());


                }
             }
             }
         case Node.ENTITY_REFERENCE_NODE :
         case Node.DOCUMENT_NODE :
                 parent=node;
                 sibling=node.getFirstChild();
             break;
         }
         while ((sibling==null) && (parent!=null)) {
                         sibling=parent.getNextSibling();
                         parent=parent.getParentNode();
                 };
       if (sibling==null) {
                         return;
                 }

         node=sibling;
         sibling=node.getNextSibling();
           } while (true);
   }

    /**
     * @param sibling
     * @param nodeName
     * @param number
     * @return nodes with the constrain
     */
    public static Element selectDsNode(Node sibling, String nodeName, int number) {
        while (sibling != null) {
            if (Constants.SignatureSpecNS.equals(sibling.getNamespaceURI())
                && sibling.getLocalName().equals(nodeName)) {
                if (number == 0){
                    return (Element)sibling;
                }
                number--;
            }
            sibling = sibling.getNextSibling();
        }
        return null;
    }

    /**
     * @param sibling
     * @param nodeName
     * @param number
     * @return nodes with the constrain
     */
    public static Element selectXencNode(Node sibling, String nodeName, int number) {
        while (sibling != null) {
            if (EncryptionConstants.EncryptionSpecNS.equals(sibling.getNamespaceURI())
                && sibling.getLocalName().equals(nodeName)) {
                if (number == 0){
                    return (Element)sibling;
                }
                number--;
            }
            sibling = sibling.getNextSibling();
        }
        return null;
    }

   /**
    * @param sibling
    * @param nodeName
    * @param number
    * @return nodes with the constrain
    */
   public static Text selectDsNodeText(Node sibling, String nodeName, int number) {
            Node n=selectDsNode(sibling,nodeName,number);
        if (n==null) {
                return null;
        }
        n=n.getFirstChild();
        while (n!=null && n.getNodeType()!=Node.TEXT_NODE) {
                n=n.getNextSibling();
        }
        return (Text)n;
   }

   /**
    * @param sibling
    * @param uri
    * @param nodeName
    * @param number
    * @return nodes with the constrain
    */
   public static Text selectNodeText(Node sibling, String uri, String nodeName, int number) {
        Node n=selectNode(sibling,uri,nodeName,number);
    if (n==null) {
        return null;
    }
    n=n.getFirstChild();
    while (n!=null && n.getNodeType()!=Node.TEXT_NODE) {
        n=n.getNextSibling();
    }
    return (Text)n;
   }

    /**
     * @param sibling
     * @param uri
     * @param nodeName
     * @param number
     * @return nodes with the constrain
     */
    public static Element selectNode(Node sibling, String uri, String nodeName, int number) {
        while (sibling != null) {
            if (sibling.getNamespaceURI() != null && sibling.getNamespaceURI().equals(uri)
                && sibling.getLocalName().equals(nodeName)) {
                if (number == 0){
                    return (Element)sibling;
                }
                number--;
            }
            sibling = sibling.getNextSibling();
        }
        return null;
    }

    /**
     * @param sibling
     * @param nodeName
     * @return nodes with the constrain
     */
    public static Element[] selectDsNodes(Node sibling, String nodeName) {
        return selectNodes(sibling,Constants.SignatureSpecNS, nodeName);
    }

    /**
     * @param sibling
     * @param uri
     * @param nodeName
     * @return nodes with the constrain
     */
     public static Element[] selectNodes(Node sibling, String uri, String nodeName) {
        List<Element> list = new ArrayList<Element>();
        while (sibling != null) {
            if (sibling.getNamespaceURI() != null && sibling.getNamespaceURI().equals(uri)
                && sibling.getLocalName().equals(nodeName)) {
                list.add((Element)sibling);
            }
            sibling = sibling.getNextSibling();
        }
        return list.toArray(new Element[list.size()]);
    }

   /**
    * @param signatureElement
    * @param inputSet
    * @return nodes with the constrain
    */
    public static Set<Node> excludeNodeFromSet(Node signatureElement, Set<Node> inputSet) {
          Set<Node> resultSet = new HashSet<Node>();
          Iterator<Node> iterator = inputSet.iterator();

          while (iterator.hasNext()) {
            Node inputNode = iterator.next();

            if (!XMLUtils
                    .isDescendantOrSelf(signatureElement, inputNode)) {
               resultSet.add(inputNode);
            }
         }
         return resultSet;
     }

   /**
    * Returns true if the descendantOrSelf is on the descendant-or-self axis
    * of the context node.
    *
    * @param ctx
    * @param descendantOrSelf
    * @return true if the node is descendant
    */
   static public boolean isDescendantOrSelf(Node ctx, Node descendantOrSelf) {

      if (ctx == descendantOrSelf) {
         return true;
      }

      Node parent = descendantOrSelf;

      while (true) {
         if (parent == null) {
            return false;
         }

         if (parent == ctx) {
            return true;
         }

         if (parent.getNodeType() == Node.ATTRIBUTE_NODE) {
            parent = ((Attr) parent).getOwnerElement();
         } else {
            parent = parent.getParentNode();
         }
      }
   }

    public static boolean ignoreLineBreaks() {
        return ignoreLineBreaks;
    }

    /**
     * This method is a tree-search to help prevent against wrapping attacks.
     * It checks that no two Elements have ID Attributes that match the "value"
     * argument, if this is the case then "false" is returned. Note that a
     * return value of "true" does not necessarily mean that a matching Element
     * has been found, just that no wrapping attack has been detected.
     */
    public static boolean protectAgainstWrappingAttack(Node startNode,
                                                       String value)
    {
        Node startParent = startNode.getParentNode();
        Node processedNode = null;
        Element foundElement = null;

        String id = value.trim();
        if (id.charAt(0) == '#') {
            id = id.substring(1);
        }

        while (startNode != null) {
            if (startNode.getNodeType() == Node.ELEMENT_NODE) {
                Element se = (Element) startNode;

                NamedNodeMap attributes = se.getAttributes();
                if (attributes != null) {
                    for (int i = 0; i < attributes.getLength(); i++) {
                        Attr attr = (Attr)attributes.item(i);
                        if (attr.isId() && id.equals(attr.getValue())) {
                            if (foundElement == null) {
                                // Continue searching to find duplicates
                                foundElement = attr.getOwnerElement();
                            } else {
                                log.log(java.util.logging.Level.FINE, "Multiple elements with the same 'Id' attribute value!");
                                return false;
                            }
                        }
                    }
                }
            }

            processedNode = startNode;
            startNode = startNode.getFirstChild();

            // no child, this node is done.
            if (startNode == null) {
                // close node processing, get sibling
                startNode = processedNode.getNextSibling();
            }

            // no more siblings, get parent, all children
            // of parent are processed.
            while (startNode == null) {
                processedNode = processedNode.getParentNode();
                if (processedNode == startParent) {
                    return true;
                }
                // close parent node processing (processed node now)
                startNode = processedNode.getNextSibling();
            }
        }
        return true;
    }

    /**
     * This method is a tree-search to help prevent against wrapping attacks.
     * It checks that no other Element than the given "knownElement" argument
     * has an ID attribute that matches the "value" argument, which is the ID
     * value of "knownElement". If this is the case then "false" is returned.
     */
    public static boolean protectAgainstWrappingAttack(Node startNode,
                                                       Element knownElement,
                                                       String value)
    {
        Node startParent = startNode.getParentNode();
        Node processedNode = null;

        String id = value.trim();
        if (id.charAt(0) == '#') {
            id = id.substring(1);
        }

        while (startNode != null) {
            if (startNode.getNodeType() == Node.ELEMENT_NODE) {
                Element se = (Element) startNode;

                NamedNodeMap attributes = se.getAttributes();
                if (attributes != null) {
                    for (int i = 0; i < attributes.getLength(); i++) {
                        Attr attr = (Attr)attributes.item(i);
                        if (attr.isId() && id.equals(attr.getValue())
                            && se != knownElement)
                        {
                            log.log(java.util.logging.Level.FINE, "Multiple elements with the same 'Id' attribute value!");
                            return false;
                        }
                    }
                }
            }

            processedNode = startNode;
            startNode = startNode.getFirstChild();

            // no child, this node is done.
            if (startNode == null) {
                // close node processing, get sibling
                startNode = processedNode.getNextSibling();
            }

            // no more siblings, get parent, all children
            // of parent are processed.
            while (startNode == null) {
                processedNode = processedNode.getParentNode();
                if (processedNode == startParent) {
                    return true;
                }
                // close parent node processing (processed node now)
                startNode = processedNode.getNextSibling();
            }
        }
        return true;
    }

}
