FallbackIgnoreEntityResolver2.java
/*
* 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
*
* https://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.commons.xml.secure;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import org.xml.sax.EntityResolver;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.ext.DefaultHandler2;
import org.xml.sax.ext.EntityResolver2;
/**
* Entity resolver that consults an optional caller-supplied resolver and ignores (resolves to empty) whatever the caller does not resolve.
* <p>
* The canonical secure floor, and the entity-resolution counterpart of the JAXP 1.5 {@code ACCESS_EXTERNAL_*} properties. Every floor
* ({@link FallbackIgnoreLSResourceResolver}, {@link FallbackIgnoreURIResolver} and {@link FallbackIgnoreXMLResolver}) shares two defining properties:
* </p>
* <ol>
* <li><strong>Non-removable, and it wraps the resolver the caller sets.</strong> The secure wrappers install one and route a caller-set resolver through
* {@code setDelegate} rather than letting it replace the floor, so the caller's resolver is consulted first but cannot remove the floor underneath it.</li>
* <li><strong>It supplies the default action for a lookup the caller's resolver does not resolve</strong> (a {@code null} return, or no caller resolver at
* all). This is where a floor departs from stock JAXP: normally an unresolved lookup falls back to the processor's built-in resolution and the resource is
* <em>fetched</em>; a floor instead resolves it to <em>empty</em> content, so the parse continues without the external fetch and without a leak.</li>
* </ol>
* <p>
* The secure DOM and SAX wrappers install one of these and, when the caller sets their own {@link EntityResolver}, route it through {@link #setDelegate}
* rather than letting it replace the floor. A caller therefore opts a specific resource in by returning a non-{@code null} {@link InputSource} from their
* resolver; anything they leave unresolved (a {@code null} return, or no caller resolver at all) goes to {@link #onUnresolved}, which returns empty content by
* default.
* </p>
* <p>
* It extends {@link DefaultHandler2} so it is also usable as a {@link org.xml.sax.ext.LexicalHandler}; {@link #getExternalSubset} therefore inherits the
* {@code DefaultHandler2} "no synthetic subset" default. Only {@link #resolveEntity(String, String, String, String) resolveEntity} (the actual external fetch)
* reaches the ignore fallback.
* </p>
*/
class FallbackIgnoreEntityResolver2 extends DefaultHandler2 {
private static final byte[] EMPTY = {};
/**
* Resolves {@code systemId} against {@code baseURI}.
*
* @param baseURI The absolute base URI to resolve against, or {@code null} if none is available.
* @param systemId The system identifier, possibly relative to {@code baseURI}.
* @return The absolutized system identifier, or {@code systemId} unchanged when it cannot or need not be resolved.
*/
private static String absolutize(final String baseURI, final String systemId) {
if (systemId == null || baseURI == null) {
return systemId;
}
try {
final URI system = new URI(systemId);
return system.isAbsolute() ? systemId : new URI(baseURI).resolve(system).toString();
} catch (final URISyntaxException e) {
return systemId;
}
}
/**
* Caller-supplied resolver consulted first, or {@code null} for a pure ignore-all floor.
*/
private EntityResolver delegate;
/**
* Constructs a new ignore-all floor with an optional caller-supplied resolver.
*
* @param delegate The caller-supplied resolver, or {@code null} for a pure ignore-all floor.
*/
FallbackIgnoreEntityResolver2(final EntityResolver delegate) {
this.delegate = delegate;
}
/**
* Gets the delegate provided by the constructor or set by {@link #setDelegate}, may be {@code null}.
*
* @return The delegate provided by the constructor or set by {@link #setDelegate}, may be {@code null}.
*/
final EntityResolver getDelegate() {
return delegate;
}
/**
* Outcome when neither the caller delegate nor this resolver provides the entity. Resolves to empty content by default, so the external resource is neither
* fetched nor leaked and the parse continues with no replacement text. The returned source echoes the requested identifiers (with {@code systemId}
* absolutized): the parser reads the empty byte stream, but Xerces still derives the entity's base URI from the system id and fails on a {@code null} one.
*
* @param name The entity name, or {@code null} on the 2-arg resolution path.
* @param publicId The public identifier, or {@code null} if none.
* @param baseURI The base URI for relative resolution, or {@code null}.
* @param systemId The system identifier of the unresolved entity.
* @return An empty {@link InputSource} carrying the requested identifiers.
* @throws SAXException when {@value SecureException#THROW_ON_UNRESOLVED} is set: unresolved references are rejected instead of resolved to empty.
* @throws IOException never by the default implementation.
*/
protected InputSource onUnresolved(final String name, final String publicId, final String baseURI, final String systemId) throws SAXException, IOException {
if (SecureException.throwOnUnresolved()) {
throw new SAXException(SecureException.forbidden(name, null, publicId, systemId, baseURI));
}
final InputSource empty = new InputSource(new ByteArrayInputStream(EMPTY));
empty.setPublicId(publicId);
empty.setSystemId(absolutize(baseURI, systemId));
return empty;
}
@Override
public final InputSource resolveEntity(final String publicId, final String systemId) throws SAXException, IOException {
return resolveEntity(null, publicId, null, systemId);
}
@Override
public final InputSource resolveEntity(final String name, final String publicId, final String baseURI, final String systemId)
throws SAXException, IOException {
final InputSource resolved = resolveWithDelegate(name, publicId, baseURI, systemId);
return resolved != null ? resolved : onUnresolved(name, publicId, baseURI, systemId);
}
private InputSource resolveWithDelegate(final String name, final String publicId, final String baseURI, final String systemId)
throws SAXException, IOException {
if (delegate != null) {
return delegate instanceof EntityResolver2 ? ((EntityResolver2) delegate).resolveEntity(name, publicId, baseURI, systemId) :
// We need to resolve the systemId against baseURI, because a plain EntityResolver expects an absolute URI.
delegate.resolveEntity(publicId, absolutize(baseURI, systemId));
}
return null;
}
/**
* Replaces the caller resolver consulted ahead of the floor; lets a single floor instance back successive {@code setEntityResolver} calls.
*
* @param delegate The caller-supplied resolver, or {@code null} for a pure ignore-all floor.
*/
final void setDelegate(final EntityResolver delegate) {
this.delegate = delegate;
}
}