blob: b62795beb8853f041efa212244b81a1df03be5ad [file] [log] [blame]
/*
* 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.solr.response;
import java.io.File;
import java.io.FilePermission;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.StringWriter;
import java.io.Writer;
import java.lang.invoke.MethodHandles;
import java.nio.charset.StandardCharsets;
import java.security.AccessControlContext;
import java.security.AccessController;
import java.security.Permissions;
import java.security.PrivilegedActionException;
import java.security.PrivilegedExceptionAction;
import java.security.ProtectionDomain;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.Properties;
import java.util.PropertyPermission;
import java.util.ResourceBundle;
import org.apache.solr.client.solrj.SolrResponse;
import org.apache.solr.client.solrj.response.QueryResponse;
import org.apache.solr.client.solrj.response.SolrResponseBase;
import org.apache.solr.common.SolrException;
import org.apache.solr.common.params.CommonParams;
import org.apache.solr.common.util.NamedList;
import org.apache.solr.core.SolrCore;
import org.apache.solr.request.SolrQueryRequest;
import org.apache.solr.util.plugin.SolrCoreAware;
import org.apache.velocity.Template;
import org.apache.velocity.VelocityContext;
import org.apache.velocity.app.VelocityEngine;
import org.apache.velocity.runtime.RuntimeConstants;
import org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader;
import org.apache.velocity.tools.generic.CollectionTool;
import org.apache.velocity.tools.generic.ComparisonDateTool;
import org.apache.velocity.tools.generic.DisplayTool;
import org.apache.velocity.tools.generic.EscapeTool;
import org.apache.velocity.tools.generic.LocaleConfig;
import org.apache.velocity.tools.generic.MathTool;
import org.apache.velocity.tools.generic.NumberTool;
import org.apache.velocity.tools.generic.ResourceTool;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import static org.apache.solr.common.params.CommonParams.SORT;
/**
* @deprecated since 8.4; see <a href="https://cwiki.apache.org/confluence/display/SOLR/Deprecations">Deprecations</a>
*/
public class VelocityResponseWriter implements QueryResponseWriter, SolrCoreAware {
// init param names, these are _only_ loaded at init time (no per-request control of these)
// - multiple different named writers could be created with different init params
public static final String TEMPLATE_BASE_DIR = "template.base.dir";
public static final String PROPERTIES_FILE = "init.properties.file";
// System property names, these are _only_ loaded at node startup (no per-request control of these)
public static final String SOLR_RESOURCE_LOADER_ENABLED = "velocity.resourceloader.solr.enabled";
// request param names
public static final String TEMPLATE = "v.template";
public static final String LAYOUT = "v.layout";
public static final String LAYOUT_ENABLED = "v.layout.enabled";
public static final String CONTENT_TYPE = "v.contentType";
public static final String JSON = "v.json";
public static final String LOCALE = "v.locale";
public static final String TEMPLATE_EXTENSION = ".vm";
public static final String DEFAULT_CONTENT_TYPE = "text/html;charset=UTF-8";
public static final String JSON_CONTENT_TYPE = "application/json;charset=UTF-8";
private File fileResourceLoaderBaseDir;
private String initPropertiesFileName; // used just to hold from init() to inform()
private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
private Properties velocityInitProps = new Properties();
private Map<String,String> customTools = new HashMap<String,String>();
@Override
public void init(@SuppressWarnings({"rawtypes"})NamedList args) {
log.warn("VelocityResponseWriter is deprecated. This may be removed in future Solr releases. Please SOLR-14065.");
fileResourceLoaderBaseDir = null;
String templateBaseDir = (String) args.get(TEMPLATE_BASE_DIR);
if (templateBaseDir != null && !templateBaseDir.isEmpty()) {
fileResourceLoaderBaseDir = new File(templateBaseDir).getAbsoluteFile();
if (!fileResourceLoaderBaseDir.exists()) { // "*not* exists" condition!
log.warn("{} specified does not exist: {}", TEMPLATE_BASE_DIR, fileResourceLoaderBaseDir);
fileResourceLoaderBaseDir = null;
} else {
if (!fileResourceLoaderBaseDir.isDirectory()) { // "*not* a directory" condition
log.warn("{} specified is not a directory: {}", TEMPLATE_BASE_DIR, fileResourceLoaderBaseDir);
fileResourceLoaderBaseDir = null;
}
}
}
initPropertiesFileName = (String) args.get(PROPERTIES_FILE);
@SuppressWarnings({"rawtypes"})
NamedList tools = (NamedList)args.get("tools");
if (tools != null) {
for(Object t : tools) {
@SuppressWarnings({"rawtypes"})
Map.Entry tool = (Map.Entry)t;
customTools.put(tool.getKey().toString(), tool.getValue().toString());
}
}
}
@Override
public void inform(SolrCore core) {
// need to leverage SolrResourceLoader, so load init.properties.file here instead of init()
if (initPropertiesFileName != null) {
try {
velocityInitProps.load(new InputStreamReader(core.getResourceLoader().openResource(initPropertiesFileName), StandardCharsets.UTF_8));
} catch (IOException e) {
log.warn("Error loading {} specified property file: {}", PROPERTIES_FILE, initPropertiesFileName, e);
}
}
}
@Override
public String getContentType(SolrQueryRequest request, SolrQueryResponse response) {
String contentType = request.getParams().get(CONTENT_TYPE);
// Use the v.contentType specified, or either of the default content types depending on the presence of v.json
return (contentType != null) ? contentType : ((request.getParams().get(JSON) == null) ? DEFAULT_CONTENT_TYPE : JSON_CONTENT_TYPE);
}
@Override
public void write(Writer writer, SolrQueryRequest request, SolrQueryResponse response) throws IOException {
// run doWrite() with the velocity sandbox
try {
AccessController.doPrivileged(new PrivilegedExceptionAction<Void>() {
@Override
public Void run() throws IOException {
doWrite(writer, request, response);
return null;
}
}, VELOCITY_SANDBOX);
} catch (PrivilegedActionException e) {
throw (IOException) e.getException();
}
}
// sandbox for velocity code
// TODO: we could read in a policy file instead, in case someone needs to tweak it?
private static final AccessControlContext VELOCITY_SANDBOX;
static {
Permissions permissions = new Permissions();
// TODO: restrict the scope of this! we probably only need access to classpath
permissions.add(new FilePermission("<<ALL FILES>>", "read,readlink"));
// properties needed by SolrResourceLoader (called from velocity code)
permissions.add(new PropertyPermission("jetty.testMode", "read"));
permissions.add(new PropertyPermission("solr.allow.unsafe.resourceloading", "read"));
// properties needed by log4j (called from velocity code)
permissions.add(new PropertyPermission("java.version", "read"));
// needed by velocity duck-typing
permissions.add(new RuntimePermission("accessDeclaredMembers"));
permissions.setReadOnly();
VELOCITY_SANDBOX = new AccessControlContext(new ProtectionDomain[] { new ProtectionDomain(null, permissions) });
}
private void doWrite(Writer writer, SolrQueryRequest request, SolrQueryResponse response) throws IOException {
VelocityEngine engine = createEngine(request); // TODO: have HTTP headers available for configuring engine
Template template = getTemplate(engine, request);
VelocityContext context = createContext(request, response);
context.put("engine", engine); // for $engine.resourceExists(...)
String layoutTemplate = request.getParams().get(LAYOUT);
boolean layoutEnabled = request.getParams().getBool(LAYOUT_ENABLED, true) && layoutTemplate != null;
String jsonWrapper = request.getParams().get(JSON);
boolean wrapResponse = layoutEnabled || jsonWrapper != null;
// create output
if (!wrapResponse) {
// straight-forward template/context merge to output
template.merge(context, writer);
}
else {
// merge to a string buffer, then wrap with layout and finally as JSON
StringWriter stringWriter = new StringWriter();
template.merge(context, stringWriter);
if (layoutEnabled) {
context.put("content", stringWriter.toString());
stringWriter = new StringWriter();
try {
engine.getTemplate(layoutTemplate + TEMPLATE_EXTENSION).merge(context, stringWriter);
} catch (Exception e) {
throw new IOException(e.getMessage());
}
}
if (jsonWrapper != null) {
for (int i=0; i<jsonWrapper.length(); i++) {
if (!Character.isJavaIdentifierPart(jsonWrapper.charAt(i))) {
throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Invalid function name for " + JSON + ": '" + jsonWrapper + "'");
}
}
writer.write(jsonWrapper + "(");
writer.write(getJSONWrap(stringWriter.toString()));
writer.write(')');
} else { // using a layout, but not JSON wrapping
writer.write(stringWriter.toString());
}
}
}
@SuppressWarnings({"unchecked"})
private VelocityContext createContext(SolrQueryRequest request, SolrQueryResponse response) {
VelocityContext context = new VelocityContext();
// Register useful Velocity "tools"
String locale = request.getParams().get(LOCALE);
@SuppressWarnings({"rawtypes"})
Map toolConfig = new HashMap();
toolConfig.put("locale", locale);
context.put("log", log); // TODO: add test; TODO: should this be overridable with a custom "log" named tool?
context.put("esc", new EscapeTool());
context.put("date", new ComparisonDateTool());
context.put(SORT, new CollectionTool());
MathTool mathTool = new MathTool();
mathTool.configure(toolConfig);
context.put("math", mathTool);
NumberTool numberTool = new NumberTool();
numberTool.configure(toolConfig);
context.put("number", numberTool);
DisplayTool displayTool = new DisplayTool();
displayTool.configure(toolConfig);
context.put("display", displayTool);
ResourceTool resourceTool = new SolrVelocityResourceTool(request.getCore().getSolrConfig().getResourceLoader().getClassLoader());
resourceTool.configure(toolConfig);
context.put("resource", resourceTool);
if (request.getCore().getCoreDescriptor().isConfigSetTrusted()) {
// Load custom tools, only if in a trusted configset
/*
// Custom tools, specified in config as:
<queryResponseWriter name="velocityWithCustomTools" class="solr.VelocityResponseWriter">
<lst name="tools">
<str name="mytool">com.example.solr.velocity.MyTool</str>
</lst>
</queryResponseWriter>
*/
// Custom tools can override any of the built-in tools provided above, by registering one with the same name
if (request.getCore().getCoreDescriptor().isConfigSetTrusted()) {
for (Map.Entry<String, String> entry : customTools.entrySet()) {
String name = entry.getKey();
// TODO: at least log a warning when one of the *fixed* tools classes is same name with a custom one, currently silently ignored
Object customTool = SolrCore.createInstance(entry.getValue(), Object.class, "VrW custom tool: " + name, request.getCore(), request.getCore().getResourceLoader());
if (customTool instanceof LocaleConfig) {
((LocaleConfig) customTool).configure(toolConfig);
}
context.put(name, customTool);
}
}
// custom tools _cannot_ override context objects added below, like $request and $response
}
// Turn the SolrQueryResponse into a SolrResponse.
// QueryResponse has lots of conveniences suitable for a view
// Problem is, which SolrResponse class to use?
// One patch to SOLR-620 solved this by passing in a class name as
// as a parameter and using reflection and Solr's class loader to
// create a new instance. But for now the implementation simply
// uses QueryResponse, and if it chokes in a known way, fall back
// to bare bones SolrResponseBase.
// Can this writer know what the handler class is? With echoHandler=true it can get its string name at least
SolrResponse rsp = new QueryResponse();
NamedList<Object> parsedResponse = BinaryResponseWriter.getParsedResponse(request, response);
try {
rsp.setResponse(parsedResponse);
// page only injected if QueryResponse works
context.put("page", new PageTool(request, response)); // page tool only makes sense for a SearchHandler request
context.put("debug",((QueryResponse)rsp).getDebugMap());
} catch (ClassCastException e) {
// known edge case where QueryResponse's extraction assumes "response" is a SolrDocumentList
// (AnalysisRequestHandler emits a "response")
rsp = new SolrResponseBase();
rsp.setResponse(parsedResponse);
}
context.put("request", request);
context.put("response", rsp);
return context;
}
private VelocityEngine createEngine(SolrQueryRequest request) {
boolean trustedMode = request.getCore().getCoreDescriptor().isConfigSetTrusted();
VelocityEngine engine = new VelocityEngine();
// load the built-in _macros.vm first, then load VM_global_library.vm for legacy (pre-5.0) support,
// and finally allow macros.vm to have the final say and override anything defined in the preceding files.
engine.setProperty(RuntimeConstants.VM_LIBRARY, "_macros.vm,VM_global_library.vm,macros.vm");
// Standard templates autoload, but not the macro one(s), by default, so let's just make life
// easier, and consistent, for macro development too.
engine.setProperty(RuntimeConstants.VM_LIBRARY_AUTORELOAD, "true");
/*
Set up Velocity resource loader(s)
terminology note: "resource loader" is overloaded here, there is Solr's resource loader facility for plugins,
and there are Velocity template resource loaders. It's confusing, they overlap: there is a Velocity resource
loader that loads templates from Solr's resource loader (SolrVelocityResourceLoader).
The Velocity resource loader order is `[file,][solr],builtin` intentionally ordered in this manner.
The "file" resource loader, enabled when the configset is trusted and `template.base.dir` is specified as a
response writer init property.
The "solr" resource loader, enabled when the configset is trusted, and provides templates from a velocity/
sub-tree in either the classpath or under conf/.
By default, only "builtin" resource loader is enabled, providing tenplates from builtin Solr .jar files.
The basic browse templates are built into
this plugin, but can be individually overridden by placing a same-named template in the template.base.dir specified
directory, or within a trusted configset's velocity/ directory.
*/
ArrayList<String> loaders = new ArrayList<String>();
if ((fileResourceLoaderBaseDir != null) && trustedMode) {
loaders.add("file");
engine.setProperty(RuntimeConstants.FILE_RESOURCE_LOADER_PATH, fileResourceLoaderBaseDir.getAbsolutePath());
}
if (trustedMode) {
// The solr resource loader serves templates under a velocity/ subtree from <lib>, conf/,
// or SolrCloud's configuration tree. Or rather the other way around, other resource loaders are rooted
// from the top, whereas this is velocity/ sub-tree rooted.
loaders.add("solr");
engine.setProperty("solr.resource.loader.instance", new SolrVelocityResourceLoader(request.getCore().getSolrConfig().getResourceLoader()));
}
// Always have the built-in classpath loader. This is needed when using VM_LIBRARY macros, as they are required
// to be present if specified, and we want to have a nice macros facility built-in for users to use easily, and to
// extend in custom ways.
loaders.add("builtin");
engine.setProperty("builtin.resource.loader.instance", new ClasspathResourceLoader());
engine.setProperty(RuntimeConstants.RESOURCE_LOADER, String.join(",", loaders));
engine.setProperty(RuntimeConstants.INPUT_ENCODING, "UTF-8");
engine.setProperty(RuntimeConstants.SPACE_GOBBLING, RuntimeConstants.SpaceGobbling.LINES.toString());
// install a class/package restricting uberspector
engine.setProperty(RuntimeConstants.UBERSPECT_CLASSNAME,"org.apache.velocity.util.introspection.SecureUberspector");
engine.addProperty(RuntimeConstants.INTROSPECTOR_RESTRICT_PACKAGES,"java.lang.reflect");
engine.addProperty(RuntimeConstants.INTROSPECTOR_RESTRICT_CLASSES,"java.lang.Class");
engine.addProperty(RuntimeConstants.INTROSPECTOR_RESTRICT_CLASSES,"java.lang.ClassLoader");
engine.addProperty(RuntimeConstants.INTROSPECTOR_RESTRICT_CLASSES,"java.lang.Compiler");
engine.addProperty(RuntimeConstants.INTROSPECTOR_RESTRICT_CLASSES,"java.lang.InheritableThreadLocal");
engine.addProperty(RuntimeConstants.INTROSPECTOR_RESTRICT_CLASSES,"java.lang.Package");
engine.addProperty(RuntimeConstants.INTROSPECTOR_RESTRICT_CLASSES,"java.lang.Process");
engine.addProperty(RuntimeConstants.INTROSPECTOR_RESTRICT_CLASSES,"java.lang.Runtime");
engine.addProperty(RuntimeConstants.INTROSPECTOR_RESTRICT_CLASSES,"java.lang.RuntimePermission");
engine.addProperty(RuntimeConstants.INTROSPECTOR_RESTRICT_CLASSES,"java.lang.SecurityManager");
engine.addProperty(RuntimeConstants.INTROSPECTOR_RESTRICT_CLASSES,"java.lang.System");
engine.addProperty(RuntimeConstants.INTROSPECTOR_RESTRICT_CLASSES,"java.lang.Thread");
engine.addProperty(RuntimeConstants.INTROSPECTOR_RESTRICT_CLASSES,"java.lang.ThreadGroup");
engine.addProperty(RuntimeConstants.INTROSPECTOR_RESTRICT_CLASSES,"java.lang.ThreadLocal");
engine.addProperty(RuntimeConstants.INTROSPECTOR_RESTRICT_CLASSES,"org.apache.solr.core.SolrResourceLoader");
engine.addProperty(RuntimeConstants.INTROSPECTOR_RESTRICT_CLASSES,"org.apache.solr.core.CoreContainer");
if (trustedMode) {
// Work around VELOCITY-908 with Velocity not handling locales properly
Object spaceGobblingInitProperty = velocityInitProps.get(RuntimeConstants.SPACE_GOBBLING);
if (spaceGobblingInitProperty != null) {
// If there is an init property, uppercase it before Velocity.
velocityInitProps.put(RuntimeConstants.SPACE_GOBBLING,
String.valueOf(spaceGobblingInitProperty).toUpperCase(Locale.ROOT));
}
// bring in any custom properties too
engine.setProperties(velocityInitProps);
}
engine.init();
return engine;
}
private Template getTemplate(VelocityEngine engine, SolrQueryRequest request) throws IOException {
Template template;
String templateName = request.getParams().get(TEMPLATE);
String qt = request.getParams().get(CommonParams.QT);
String path = (String) request.getContext().get("path");
if (templateName == null && path != null) {
templateName = path;
} // TODO: path is never null, so qt won't get picked up maybe special case for '/select' to use qt, otherwise use path?
if (templateName == null && qt != null) {
templateName = qt;
}
if (templateName == null) templateName = "index";
try {
template = engine.getTemplate(templateName + TEMPLATE_EXTENSION);
} catch (Exception e) {
throw new IOException(e.getMessage());
}
return template;
}
private String getJSONWrap(String xmlResult) { // maybe noggit or Solr's JSON utilities can make this cleaner?
// escape the double quotes and backslashes
String replace1 = xmlResult.replaceAll("\\\\", "\\\\\\\\");
replace1 = replace1.replaceAll("\\n", "\\\\n");
replace1 = replace1.replaceAll("\\r", "\\\\r");
String replaced = replace1.replaceAll("\"", "\\\\\"");
// wrap it in a JSON object
return "{\"result\":\"" + replaced + "\"}";
}
// see: https://github.com/apache/velocity-tools/blob/trunk/velocity-tools-generic/src/main/java/org/apache/velocity/tools/generic/ResourceTool.java
private static class SolrVelocityResourceTool extends ResourceTool {
private ClassLoader solrClassLoader;
public SolrVelocityResourceTool(ClassLoader cl) {
this.solrClassLoader = cl;
}
@Override
protected ResourceBundle getBundle(String baseName, Object loc) {
// resource bundles for this tool must be in velocity "package"
return ResourceBundle.getBundle(
"velocity." + baseName,
(loc == null) ? this.getLocale() : this.toLocale(loc),
solrClassLoader);
}
}
}