Package org.xwiki.test.escaping.framework

Source Code of org.xwiki.test.escaping.framework.AbstractEscapingTest

/*
* See the NOTICE file distributed with this work for additional
* information regarding copyright ownership.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/

package org.xwiki.test.escaping.framework;

import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.commons.codec.binary.Base64;
import org.apache.commons.httpclient.Credentials;
import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.HttpStatus;
import org.apache.commons.httpclient.UsernamePasswordCredentials;
import org.apache.commons.httpclient.auth.AuthScope;
import org.apache.commons.httpclient.methods.GetMethod;
import org.apache.commons.httpclient.params.HttpClientParams;
import org.apache.commons.httpclient.params.HttpConnectionManagerParams;
import org.junit.AfterClass;
import org.junit.Assert;
import org.junit.BeforeClass;
import org.xwiki.test.escaping.suite.FileTest;
import org.xwiki.validator.ValidationError;

/**
* Abstract base class for escaping tests. Implements common initialization pattern and some utility methods
* like URL escaping, retrieving page content by URL etc. Subclasses need to implement parsing
* and custom tests.
* <p>
* Note: JUnit4 requires tests to have one public default constructor, subclasses will need to implement
* it and pass pattern matcher to match file names they can handle.</p>
* <p>
* Starting and stopping XWiki server is handled transparently for all subclasses, tests can be run
* alone using -Dtest=ClassName, a parent test suite should start XWiki server before running all
* tests for efficiency using {@link SingleXWikiExecutor}.</p>
* <p>
* The following configuration properties are supported (set in maven):
* <ul>
* <li>pattern (optional): Additional pattern to select files to be tested (use -Dpattern="substring-regex").
*                         Matches all files if empty.</li>
* </ul></p>
* <p>
* Automatic tests (see {@link AbstractAutomaticTest}) additionally support:
* <ul>
* <li>filesProduceNoOutput (optional): List of files that are expected to produce empty response</li>
* <li>patternExcludeFiles (optional): List of RegEx patterns to exclude files from the tests</li>
* </ul></p>
*
* @version $Id$
* @since 2.5M1
*/
public abstract class AbstractEscapingTest implements FileTest
{
    /** Static part of the test URL. */
    private static final String URL_START = "http://127.0.0.1:8080/xwiki/bin/";

    /** Language parameter name. */
    private static final String LANGUAGE = "language";

    /** Secret token parameter name. */
    private static final String SECRET_TOKEN = "form_token";

    /** HTTP client shared between all subclasses. */
    private static HttpClient client;

    /** A flag controlling login. If true, administrator credentials are used. */
    private static boolean loggedIn = true;

    /** Stores two cached tokens, one for each value of loggedIn (false -> 0, true -> 1). */
    private static String[] secretTokens = new String[2];

    /** File name of the template to use. */
    protected String name;

    /** User provided data found in the file. */
    protected Set<String> userInput;

    /**
     * Test fails if response is empty, but output is expected and vice versa.
     * To set to false, add file name to "filesProduceNoOutput"
     */
    protected boolean shouldProduceOutput = true;

    /** Pattern used to match files by name. */
    private Pattern namePattern;

    /**
     * Create new AbstractEscapingTest.
     *
     * @param fileNameMatcher regex pattern used to filter files by name
     */
    protected AbstractEscapingTest(Pattern fileNameMatcher)
    {
        this.namePattern = fileNameMatcher;
    }

    /**
     * Start XWiki server if run alone.
     *
     * @throws Exception on errors
     */
    @BeforeClass
    public static void startExecutor() throws Exception
    {
        SingleXWikiExecutor.getExecutor().start();
    }

    /**
     * Stop XWiki server if run alone.
     *
     * @throws Exception on errors
     */
    @AfterClass
    public static void stopExecutor() throws Exception
    {
        SingleXWikiExecutor.getExecutor().stop();
    }

    /**
     * Change multi-language mode. Note: XWiki server must already be started.
     *
     * @param enabled enable the multi-language mode if true, disable otherwise
     */
    protected static void setMultiLanguageMode(boolean enabled)
    {
        String url = AbstractEscapingTest.URL_START + "save/XWiki/XWikiPreferences?";
        url += SECRET_TOKEN + "=" + getSecretToken();
        url += "&XWiki.XWikiPreferences_0_languages=&XWiki.XWikiPreferences_0_multilingual=";
        AbstractEscapingTest.getUrlContent(url + (enabled ? 1 : 0));
        // set language=en to prevent false positives coming from the cookies
        String langUrl = AbstractEscapingTest.URL_START + "view/Main/?" + LANGUAGE + "=en";
        AbstractEscapingTest.getUrlContent(langUrl);
    }

    /**
     * {@inheritDoc}
     *
     * The implementation for escaping tests checks if the given file name matches the supported name pattern and parses
     * the file.
     *
     * @see org.xwiki.test.escaping.suite.FileTest#initialize(java.lang.String, java.io.Reader)
     */
    @Override
    public boolean initialize(String name, final Reader reader)
    {
        this.name = name;
        if (!fileNameMatches(name) || !patternMatches(name) || isExcludedFile(name)) {
            // TODO debug log the reason why the test was skipped
            return false;
        }

        this.shouldProduceOutput = isOutputProducingFile(name);
        this.userInput = parse(reader);
        return true;
    }

    /**
     * Check if the internal file name pattern matches the given file name.
     *
     * @param fileName file name to check
     * @return true if the name matches, false otherwise
     */
    protected boolean fileNameMatches(String fileName)
    {
        return this.namePattern != null && this.namePattern.matcher(fileName).matches();
    }

    /**
     * Check if the system property "pattern" matches (substring regular expression) the file name.
     * Empty pattern matches everything.
     *
     * @param fileName file name to check
     * @return true if the pattern matches, false otherwise
     */
    protected boolean patternMatches(String fileName)
    {
        String pattern = System.getProperty("pattern", "");
        if (pattern == null || pattern.equals("")) {
            return true;
        }
        return Pattern.matches(".*" + pattern + ".*", fileName);
    }

    /**
     * Check if the given file should be excluded from the tests.
     *
     * @param fileName file name to check
     * @return true if the file should be excluded, false otherwise
     */
    protected abstract boolean isExcludedFile(String fileName);

    /**
     * Check if the given file name should produce output.
     *
     * @param fileName file name to check
     * @return true if the file is expected to produce some output when requested from the server, false otherwise
     */
    protected abstract boolean isOutputProducingFile(String fileName);

    /**
     * Parse the file and collect parameters controlled by the user.
     *
     * @param reader the reader associated with the file
     * @return collection of user-controlled input parameters
     */
    protected abstract Set<String> parse(Reader reader);

    /**
     * Check if the authentication status.
     *
     * @return true if the requests will be sent authenticated as admin, false otherwise
     */
    protected static boolean isLoggedIn()
    {
        return loggedIn;
    }

    /**
     * Set authentication status.
     *
     * @param value the value to set
     */
    protected static void setLoggedIn(boolean value)
    {
        loggedIn = value;
    }

    /**
     * Download a page from the server and return its content. Throws a {@link RuntimeException}
     * on connection problems etc.
     *
     * @param url URL of the page
     * @return content of the page
     */
    protected static InputStream getUrlContent(String url)
    {
        GetMethod get = new GetMethod(url);
        get.setFollowRedirects(true);
        if (isLoggedIn()) {
            get.setDoAuthentication(true);
            get.addRequestHeader("Authorization", "Basic " + new String(Base64.encodeBase64("Admin:admin".getBytes())));
        }

        try {
            int statusCode = AbstractEscapingTest.getClient().executeMethod(get);
            switch (statusCode) {
                case HttpStatus.SC_OK:
                    // everything is fine
                    break;
                case HttpStatus.SC_UNAUTHORIZED:
                    // do not fail on 401 (unauthorized), used in some tests
                    System.out.println("WARNING, Ignoring status 401 (unauthorized) for URL: " + url);
                    break;
                case HttpStatus.SC_CONFLICT:
                    // do not fail on 409 (conflict), used in some templates
                    System.out.println("WARNING, Ignoring status 409 (conflict) for URL: " + url);
                    break;
                case HttpStatus.SC_NOT_FOUND:
                    // ignore 404 (the page is still rendered)
                    break;
                default:
                    throw new RuntimeException("HTTP GET request returned status " + statusCode + " ("
                        + get.getStatusText() + ") for URL: " + url);
            }

            // get the data, converting to utf-8
            String str = get.getResponseBodyAsString();
            if (str == null) {
                return null;
            }
            return new ByteArrayInputStream(str.getBytes("utf-8"));
        } catch (IOException exception) {
            throw new RuntimeException("Error retrieving URL: " + url, exception);
        } finally {
            get.releaseConnection();
        }
    }

    /**
     * URL-escape given string.
     *
     * @param str string to escape, "" is used if null
     * @return URL-escaped {@code str}
     */
    protected static String escapeUrl(String str)
    {
        try {
            return URLEncoder.encode(str == null ? "" : str, "UTF-8");
        } catch (UnsupportedEncodingException exception) {
            // should not happen
            throw new RuntimeException("Should not happen: ", exception);
        }
    }

    /**
     * Get an instance of the HTTP client to use.
     *
     * @return HTTP client initialized with admin credentials
     */
    protected static HttpClient getClient()
    {
        if (AbstractEscapingTest.client == null) {
            HttpClient adminClient = new HttpClient();

            // set up admin credentials
            Credentials defaultcreds = new UsernamePasswordCredentials("Admin", "admin");
            adminClient.getState().setCredentials(AuthScope.ANY, defaultcreds);

            // set up client parameters
            HttpClientParams clientParams = new HttpClientParams();
            clientParams.setSoTimeout(20000);
            // We need to allow circular redirects, because some templates redirect to the same location with different
            // query parameters and the check for circular redirect in HttpClient only checks the URI path without the
            // parameters.
            // Note that actual circular redirects are still aborted after following them for some fixed number of times
            clientParams.setBooleanParameter(HttpClientParams.ALLOW_CIRCULAR_REDIRECTS, true);
            adminClient.setParams(clientParams);

            // set up connections parameters
            HttpConnectionManagerParams connectionParams = new HttpConnectionManagerParams();
            connectionParams.setConnectionTimeout(30000);
            adminClient.getHttpConnectionManager().setParams(connectionParams);

            AbstractEscapingTest.client = adminClient;
        }
        return AbstractEscapingTest.client;
    }

    /**
     * {@inheritDoc}
     *
     * @see java.lang.Object#toString()
     */
    @Override
    public String toString()
    {
        return this.name + (this.shouldProduceOutput ? " " : " (NO OUTPUT) ") + this.userInput;
    }

    /**
     * Check for unescaped data in the given {@code content}. Throws {@link RuntimeException} on errors.
     *
     * @param url URL used in the test
     * @return list of found validation errors
     */
    protected List<ValidationError> getUnderEscapingErrors(String url)
    {
        // TODO better use XWiki logging
        System.out.println("Testing URL: " + url);

        InputStream content = AbstractEscapingTest.getUrlContent(url);
        String where = "  Template: " + this.name + "\n  URL: " + url;
        Assert.assertNotNull("Response is null\n" + where, content);
        XMLEscapingValidator validator = new XMLEscapingValidator();
        validator.setShouldBeEmpty(!this.shouldProduceOutput);
        validator.setDocument(content);
        try {
            return validator.validate();
        } catch (EscapingError error) {
            // most probably false positive, generate an error instead of failing the test
            throw new RuntimeException(EscapingError.formatMessage(error.getMessage(), this.name, url, null));
        }
    }

    /**
     * A convenience method that throws an {@link EscapingError} on failure.
     *
     * @param url URL used in the test
     * @param description description of the test
     */
    protected void checkUnderEscaping(String url, String description)
    {
        List<ValidationError> errors = getUnderEscapingErrors(url);
        if (!errors.isEmpty()) {
            throw new EscapingError("Escaping test for " + description + " failed.", this.name, url, errors);
        }
    }

    /**
     * Create the target URL from the given parameters. URL-escapes everything. Adds language=en if the parameter map
     * does not contain language parameter.
     *
     * @param action action to use, "view" is used if null
     * @param space space name to use, "Main" is used if null
     * @param page page name to use, "WebHome" is used if null
     * @param parameters list of parameters with values, parameters are omitted if null, "" is used is a value is null
     * @return the resulting absolute URL
     */
    protected static String createUrl(String action, String space, String page, Map<String, String> parameters)
    {
        return createUrl(action, space, page, parameters, true);
    }

    /**
     * Create the target URL from the given parameters. URL-escapes everything. Adds secret token if needed.
     *
     * @param action action to use, "view" is used if null
     * @param space space name to use, "Main" is used if null
     * @param page page name to use, "WebHome" is used if null
     * @param parameters list of parameters with values, parameters are omitted if null, "" is used is a value is null
     * @param addLanguage add language=en if it is not set in the parameter map
     * @return the resulting absolute URL
     */
    protected static String createUrl(String action, String space, String page, Map<String, String> parameters,
        boolean addLanguage)
    {
        String url = URL_START + escapeUrl(action == null ? "view" : action) + "/";
        url += escapeUrl(space == null ? "Main" : space) + "/";
        url += escapeUrl(page == null ? "WebHome" : page);

        String delimiter = "?";
        if (parameters != null) {
            for (String parameter : parameters.keySet()) {
                if (parameter != null && !parameter.equals("")) {
                    String value = parameters.get(parameter);
                    url += delimiter + escapeUrl(parameter) + "=" + escapeUrl(value);
                }
                delimiter = "&";
            }
        }
        // special handling for language parameter to exclude false positives (language setting is saved in cookies and
        // sent on subsequent requests)
        if (addLanguage && (parameters == null || !parameters.containsKey(LANGUAGE))) {
            url += delimiter + LANGUAGE + "=en";
        }
        // some tests need to create or delete pages, we add secret token to avoid CSRF protection failures
        if ((action == null || !action.equals("edit"))
                && (parameters == null || !parameters.containsKey(SECRET_TOKEN))) {
            url += delimiter + SECRET_TOKEN + "=" + getSecretToken();
        }
        return url;
    }

    /**
     * Get the secret token used for CSRF protection. Caches 2 tokens (for logged in and logged out) on the first call.
     *
     * @return anti-CSRF secret token, or empty string on error
     * @since 3.2M1
     */
    protected static String getSecretToken()
    {
        int index = isLoggedIn() ? 1 : 0;
        if (secretTokens[index] == null) {
            secretTokens[index] = getSecretTokenFromPage();
        }
        return secretTokens[index];
    }

    /**
     * Parse a wiki page to get the current secret token.
     *
     * @return secret token
     */
    private static String getSecretTokenFromPage()
    {
        Pattern pattern = Pattern.compile("<input[^>]+" + SECRET_TOKEN + "[^>]+value=('|\")([^'\"]+)");
        try {
            String url = createUrl("edit", "Main", "WebHome", null);
            BufferedReader reader = new BufferedReader(new InputStreamReader(AbstractEscapingTest.getUrlContent(url)));
            String line;
            while ((line = reader.readLine()) != null) {
                Matcher matcher = pattern.matcher(line);
                if (matcher.find() && matcher.groupCount() == 2) {
                    return matcher.group(2);
                }
            }
        } catch (IOException exception) {
            exception.printStackTrace();
        }
        // something went really wrong
        System.out.println("WARNING, Failed to cache anti-CSRF secret token, some tests might fail!");
        return "";
    }
}
TOP

Related Classes of org.xwiki.test.escaping.framework.AbstractEscapingTest

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.