/*

   Licensed to the Apache Software Foundation (ASF) under one or more
   contributor license agreements.  See the NOTICE file distributed with
   this work for additional information regarding copyright ownership.
   The ASF licenses this file to You 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.apache.batik.bridge;

import java.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.StringTokenizer;
import java.util.List;
import java.util.ArrayList;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;

import org.apache.batik.anim.dom.SAXSVGDocumentFactory;
import org.apache.batik.gvt.GraphicsNode;
import org.apache.batik.test.AbstractTest;
import org.apache.batik.test.DefaultTestReport;
import org.apache.batik.test.TestReport;
import org.apache.batik.util.ParsedURL;
import org.apache.batik.util.XMLResourceDescriptor;

/**
 * This test validates that SecurityExceptions are generated when
 * the user is trying the access external resources and the UserAgent
 * disallows that.
 *
 * In the following, 'unsecure' means an external resource coming from
 * a different location than the file referencing it.
 *
 * This test works with an SVG file containing an unsecure stylesheet
 * and a set of unsecure elements of all kinds, such as &lt;image&gt;
 * &lt;use&gt; or &lt;feImage&gt;. All these elements are defined
 * in a defs section. The test tries to load the document and validates
 * that a SecurityException is thrown (because of the unsecure
 * stylesheet). Then, the test iterates over the various unsecure
 * elements, inserting them into the document outside the defs
 * section, which should result in a SecurityException in each case.
 *
 * There is a property (secure) to have the test work the opposite
 * way and check that no SecurityException is thrown if access
 * to external resources is allowed.
 *
 * @author <a href="mailto:vhardy@apache.org">Vincent Hardy</a>
 * @version $Id: ExternalResourcesTest.java 1831636 2018-05-15 13:44:46Z ssteiner $
 */

public class ExternalResourcesTest extends AbstractTest
    implements ErrorConstants {
    /**
     * Error when the input file cannot be loaded into a
     * Document object
     * {0} = IOException message
     */
    public static final String ERROR_CANNOT_LOAD_SVG_DOCUMENT
        = "ExternalResourcesTest.error.cannot.load.svg.document";

    /**
     * Error while processing the document
     * {0} = BridgeException message
     */
    public static final String ERROR_WHILE_PROCESSING_SVG_DOCUMENT
        = "ExternalResourcesTest.error.while.processing.svg.document";

    /**
     * Error: an expected exception was not thrown
     * {0} = List of ids for which the exception was not thrown
     */
    public static final String ERROR_UNTHROWN_SECURITY_EXCEPTIONS
        = "ExternalResourcesTest.error.unthrown.security.exceptions";

    /**
     * Error: an unexpected exception was thrown
     * {0} = List of ids for which an exception was thrown
     */
    public static final String ERROR_THROWN_SECURITY_EXCEPTIONS
        = "ExternalResourcesTest.error.thrown.security.exceptions";

    /**
     * Error when the insertion point cannot be found in the
     * test document
     * {0} = insertion point id
     */
    public static final String ERROR_NO_INSERTION_POINT_IN_DOCUMENT
        = "ExternalResourceTest.error.no.insertion.point.in.document";

    /**
     * Error when the test could not find a list of ids for testing
     */
    public static final String ERROR_NO_ID_LIST
        = "ExternalResourceTest.error.no.id.list";

    /**
     * Error when one of the target id cannot be found
     * {0} = id which was not found
     */
    public static final String ERROR_TARGET_ID_NOT_FOUND
        = "ExternalResourcesTest.error.target.id.not.found";

    /**
     * Entry describing the error
     */
    public static final String ENTRY_KEY_ERROR_DESCRIPTION
        = "ExternalResourcesTest.entry.key.error.description";

    public static final String ENTRY_KEY_INSERTION_POINT_ID
        = "ExternalResourcesTest.entry.key.insertion.point.id";

    public static final String ENTRY_KEY_TARGET_ID
        = "ExternalResourcesTest.entry.target.id";

    public static final String ENTRY_KEY_EXPECTED_EXCEPTION_ON
        = "ExternalResourcesTest.entry.key.expected.exception.on";

    public static final String ENTRY_KEY_UNEXPECTED_EXCEPTION_ON
        = "ExternalResourcesTest.entry.key.unexpected.exception.on";

    /**
     * Pseudo id for the external stylesheet test
     */
    public static final String EXTERNAL_STYLESHEET_ID
        = "external-stylesheet";

    /**
     * Test Namespace
     */
    public static final String testNS = "http://xml.apache.org/batik/test";

    /**
     * Id of the element where unsecure content is inserted
     */
    public static final String INSERTION_POINT_ID = "insertionPoint";

    /**
     * Location of test files in filesystem.
     */
    public static final String FILE_DIR =
        "test-resources/org/apache/batik/bridge/";
    /**
     * Controls whether the test works in secure mode or not
     */
    protected boolean secure = true;

    String svgURL;

    public void setId(String id){
        super.setId(id);
        String file = id;
        int idx = file.indexOf('.');
        if (idx != -1) {
            file = file.substring(0,idx);
        }
        svgURL = resolveURL(FILE_DIR + file + ".svg");
    }

    public Boolean getSecure(){
        return secure ? Boolean.TRUE : Boolean.FALSE;
    }

    public void setSecure(Boolean secure) {
        this.secure = secure;
    }

    /**
     * Resolves the input string as follows.
     * + First, the string is interpreted as a file description.
     *   If the file exists, then the file name is turned into
     *   a URL.
     * + Otherwise, the string is supposed to be a URL. If it
     *   is an invalid URL, an IllegalArgumentException is thrown.
     */
    protected String resolveURL(String url){
        // Is url a file?
        File f = (new File(url)).getAbsoluteFile();
        if(f.getParentFile().exists()){
            try{
                return f.toURI().toURL().toString();
            }catch(MalformedURLException e){
                throw new IllegalArgumentException();
            }
        }

        // url is not a file. It must be a regular URL...
        try{
            return (new URL(url)).toString();
        }catch(MalformedURLException e){
            throw new IllegalArgumentException(url);
        }
    }

    /**
     * This test uses a list of ids found in the test document. These ids reference
     * elements found in a defs section. For each such element, the test will
     * attempt to insert the target id at a given insertion point. That insertion
     * should cause a SecurityException. If so, the test passes. Otherwise, the test
     * will fail
     */
    public TestReport runImpl() throws Exception{
        DefaultTestReport report
            = new DefaultTestReport(this);

        //
        // First step:
        //
        // Load the input SVG into a Document object
        //
        String parserClassName = XMLResourceDescriptor.getXMLParserClassName();
        SAXSVGDocumentFactory f = new SAXSVGDocumentFactory(parserClassName);
        Document doc = null;

        try {
            doc = f.createDocument(svgURL);
        } catch(IOException e){
            report.setErrorCode(ERROR_CANNOT_LOAD_SVG_DOCUMENT);
            report.addDescriptionEntry(ENTRY_KEY_ERROR_DESCRIPTION,
                                       e.getMessage());
            report.setPassed(false);
            return report;
        } catch(Exception e){
            report.setErrorCode(ERROR_CANNOT_LOAD_SVG_DOCUMENT);
            report.addDescriptionEntry(ENTRY_KEY_ERROR_DESCRIPTION,
                                       e.getMessage());
            report.setPassed(false);
            return report;
        }

        List failures = new ArrayList();

        //
        // Do an initial processing to validate that the external
        // stylesheet causes a SecurityException
        //
        MyUserAgent userAgent = buildUserAgent();
        GVTBuilder builder = new GVTBuilder();
        BridgeContext ctx = new BridgeContext(userAgent);
        ctx.setDynamic(true);

        // We expect either a SecurityException or a BridgeException
        // with ERR_URI_UNSECURE.
        Throwable th = null;
        try {
            GraphicsNode gn = builder.build(ctx, doc);
            gn.getBounds();
            th = userAgent.getDisplayError();
        } catch (BridgeException e){
            th = e;
        } catch (SecurityException e) {
            th = e;
        } catch (Throwable t) {
            th = t;
        }
        if (th == null) {
            if (secure)
                failures.add(EXTERNAL_STYLESHEET_ID);
        } else if (th instanceof SecurityException) {
            if (!secure)
                failures.add(EXTERNAL_STYLESHEET_ID);
        } else if (th instanceof BridgeException) {
            BridgeException be = (BridgeException)th;
            if (!secure  ||
                (secure && !ERR_URI_UNSECURE.equals(be.getCode()))) {
                report.setErrorCode(ERROR_WHILE_PROCESSING_SVG_DOCUMENT);
                report.addDescriptionEntry(ENTRY_KEY_ERROR_DESCRIPTION,
                                           be.getMessage());
                report.setPassed(false);
                return report;
            }
        }

        //
        // Remove the stylesheet from the document
        //
        Node child = doc.getFirstChild();
        Node next = null;
        while (child != null) {
            next = child.getNextSibling();
            if (child.getNodeType() == Node.PROCESSING_INSTRUCTION_NODE) {
                doc.removeChild(child);
            }
            child = next;
        }

        //
        // Now, get the list of ids to be checked
        //
        Element root = doc.getDocumentElement();
        String idList = root.getAttributeNS(testNS, "targetids");
        if (idList == null || "".equals(idList)) {
            report.setErrorCode(ERROR_NO_ID_LIST);
            report.setPassed(false);
            return report;
        }

        StringTokenizer st = new StringTokenizer(idList, ",");
        String[] ids = new String[st.countTokens()];
        for (int i=0; i<ids.length; i++) {
            ids[i] = st.nextToken().trim();
        }

        for (String id : ids) {
            userAgent = buildUserAgent();
            builder = new GVTBuilder();
            ctx = new BridgeContext(userAgent);
            ctx.setDynamic(true);

            Document cloneDoc = (Document) doc.cloneNode(true);
            Element insertionPoint = cloneDoc.getElementById(INSERTION_POINT_ID);

            if (insertionPoint == null) {
                report.setErrorCode(ERROR_NO_INSERTION_POINT_IN_DOCUMENT);
                report.addDescriptionEntry(ENTRY_KEY_INSERTION_POINT_ID,
                        INSERTION_POINT_ID);
                report.setPassed(false);
                return report;
            }

            Element target = cloneDoc.getElementById(id);

            if (target == null) {
                report.setErrorCode(ERROR_TARGET_ID_NOT_FOUND);
                report.addDescriptionEntry(ENTRY_KEY_TARGET_ID,
                        id);
                report.setPassed(false);
                return report;
            }

            insertionPoint.appendChild(target);
            th = null;
            try {
                GraphicsNode gn = builder.build(ctx, cloneDoc);
                gn.getBounds();
                th = userAgent.getDisplayError();
            } catch (BridgeException e) {
                th = e;
            } catch (SecurityException e) {
                th = e;
            } catch (Throwable t) {
                th = t;
            }
            if (th == null) {
                if (secure)
                    failures.add(id);
            } else if (th instanceof SecurityException) {
                if (!secure)
                    failures.add(id);
            } else if (th instanceof BridgeException) {
                BridgeException be = (BridgeException) th;
                if (!secure ||
                        (secure && !ERR_URI_UNSECURE.equals(be.getCode()))) {
                    report.setErrorCode(ERROR_WHILE_PROCESSING_SVG_DOCUMENT);
                    report.addDescriptionEntry(ENTRY_KEY_ERROR_DESCRIPTION,
                            be.getMessage());
                    report.setPassed(false);
                    return report;
                }
            } else {
                // Some unknown exception was displayed...
                report.setErrorCode(ERROR_WHILE_PROCESSING_SVG_DOCUMENT);
                report.addDescriptionEntry(ENTRY_KEY_ERROR_DESCRIPTION,
                        th.getMessage());
                report.setPassed(false);
                return report;
            }

        }

        if (failures.size() == 0) {
            return reportSuccess();
        }

        if (secure) {
            report.setErrorCode(ERROR_UNTHROWN_SECURITY_EXCEPTIONS);
            for (Object failure : failures) {
                report.addDescriptionEntry(ENTRY_KEY_EXPECTED_EXCEPTION_ON,
                        failure);
            }
        } else {
            report.setErrorCode(ERROR_THROWN_SECURITY_EXCEPTIONS);
            for (Object failure : failures) {
                report.addDescriptionEntry(ENTRY_KEY_UNEXPECTED_EXCEPTION_ON,
                        failure);
            }
        }

        report.setPassed(false);
        return report;
    }

    protected interface MyUserAgent extends UserAgent {
        Exception getDisplayError();
    }

    protected MyUserAgent buildUserAgent(){
        if (secure) {
            return new SecureUserAgent();
        } else {
            return new RelaxedUserAgent();
        }
    }

    static class MyUserAgentAdapter extends UserAgentAdapter implements MyUserAgent {
        Exception ex = null;
        public void displayError(Exception ex) {
            this.ex = ex;
            super.displayError(ex);
        }

        public Exception getDisplayError() { return ex; }
    }

    static class SecureUserAgent extends MyUserAgentAdapter {
        public ExternalResourceSecurity
            getExternalResourceSecurity(ParsedURL resourcePURL,
                                        ParsedURL docPURL){
            return new NoLoadExternalResourceSecurity();

        }
    }

    static class RelaxedUserAgent extends MyUserAgentAdapter {
        public ExternalResourceSecurity
            getExternalResourceSecurity(ParsedURL resourcePURL,
                                        ParsedURL docPURL){
            return new RelaxedExternalResourceSecurity(resourcePURL,
                                                       docPURL);

        }
    }

}
