Skip to content

Commit

Permalink
Merge pull request #166 from Wadeck/integrate-1.257.x
Browse files Browse the repository at this point in the history
Integrate 1.257.x
  • Loading branch information
daniel-beck authored Jul 24, 2019
2 parents 11ad5af + ffcd496 commit 12b55cb
Show file tree
Hide file tree
Showing 15 changed files with 587 additions and 157 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
*.iml
*.iws
target
pom.xml.versionsBackup

.project
.settings/
.classpath
.classpath
97 changes: 97 additions & 0 deletions core/src/main/java/org/kohsuke/stapler/DispatchValidator.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
/*
* The MIT License
*
* Copyright (c) 2019 CloudBees, Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/

package org.kohsuke.stapler;

import javax.annotation.CheckForNull;
import javax.annotation.Nonnull;

/**
* Validates dispatch requests. This validator is configured through {@link WebApp#setDispatchValidator(DispatchValidator)}
* and is automatically used by dispatchers created through {@link Facet#createValidatingDispatcher(AbstractTearOff, ScriptExecutor)}.
* Extends {@linkplain Facet#createValidatingDispatcher(AbstractTearOff, ScriptExecutor) facet dispatchers} to provide validation
* of views before they dispatch, thus allowing a final veto before dispatch begins writing any response body to the
* client.
*
* @see WebApp#setDispatchValidator(DispatchValidator)
* @since TODO
*/
public interface DispatchValidator {

/**
* Checks if the given request and response should be allowed to dispatch. Returns {@code null} to indicate an
* unknown or neutral result.
*
* @param req the HTTP request to validate
* @param rsp the HTTP response
* @return true if the request should be dispatched, false if not, or null if unknown or neutral
*/
@CheckForNull Boolean isDispatchAllowed(@Nonnull StaplerRequest req, @Nonnull StaplerResponse rsp);

/**
* Checks if the given request and response should be allowed to dispatch a view on an optionally present node
* object. Returns {@code null} to indicate an unknown or neutral result.
*
* @param req the HTTP request to validate
* @param rsp the HTTP response
* @param viewName the name of the view to dispatch
* @param node the node being dispatched if present
* @return true if the view should be allowed to dispatch, false if it should not, or null if unknown
*/
default @CheckForNull Boolean isDispatchAllowed(@Nonnull StaplerRequest req, @Nonnull StaplerResponse rsp, @Nonnull String viewName, @CheckForNull Object node) {
return isDispatchAllowed(req, rsp);
}

/**
* Allows the given request to be dispatched. Further calls to {@link #isDispatchAllowed(StaplerRequest, StaplerResponse)}
* should return true for the same request.
*/
void allowDispatch(@Nonnull StaplerRequest req, @Nonnull StaplerResponse rsp);

/**
* Throws a {@link CancelRequestHandlingException} if the given request is not
* {@linkplain #isDispatchAllowed(StaplerRequest, StaplerResponse) allowed}.
*/
default void requireDispatchAllowed(@Nonnull StaplerRequest req, @Nonnull StaplerResponse rsp) throws CancelRequestHandlingException {
Boolean allowed = isDispatchAllowed(req, rsp);
if (allowed == null || !allowed) {
throw new CancelRequestHandlingException();
}
}

/**
* Default validator implementation that explicitly allows all dispatch requests to proceed.
*/
DispatchValidator DEFAULT = new DispatchValidator() {
@Override
public Boolean isDispatchAllowed(@Nonnull StaplerRequest req, @Nonnull StaplerResponse rsp) {
return true;
}

@Override
public void allowDispatch(@Nonnull StaplerRequest req, @Nonnull StaplerResponse rsp) {
// no-op
}
};
}
160 changes: 152 additions & 8 deletions core/src/main/java/org/kohsuke/stapler/Facet.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,11 @@
import org.apache.commons.discovery.resource.ClassLoaders;
import org.apache.commons.discovery.resource.names.DiscoverServiceNames;
import org.kohsuke.MetaInfServices;
import org.kohsuke.stapler.event.FilteredDispatchTriggerListener;
import org.kohsuke.stapler.lang.Klass;

import javax.annotation.CheckForNull;
import javax.annotation.Nonnull;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import java.io.IOException;
Expand All @@ -50,6 +53,7 @@ public abstract class Facet {
/**
* Adds {@link Dispatcher}s that look at one token and binds that
* to the views associated with the 'it' object.
* @see #createValidatingDispatcher(AbstractTearOff, ScriptExecutor)
*/
public abstract void buildViewDispatchers(MetaClass owner, List<Dispatcher> dispatchers);

Expand Down Expand Up @@ -89,18 +93,16 @@ public static <T> List<T> discoverExtensions(Class<T> type, ClassLoader... cls)
String name = itr.nextResourceName();
if (!classNames.add(name)) continue; // avoid duplication

Class<?> c;
Class<? extends T> c;
try {
c = cl.loadClass(name);
c = cl.loadClass(name).asSubclass(type);
} catch (ClassNotFoundException e) {
LOGGER.log(Level.WARNING, "Failed to load "+name,e);
continue;
}
try {
r.add((T)c.newInstance());
} catch (InstantiationException e) {
LOGGER.log(Level.WARNING, "Failed to instantiate "+c,e);
} catch (IllegalAccessException e) {
r.add(c.newInstance());
} catch (InstantiationException | IllegalAccessException e) {
LOGGER.log(Level.WARNING, "Failed to instantiate "+c,e);
}
}
Expand All @@ -119,12 +121,13 @@ public static <T> List<T> discoverExtensions(Class<T> type, ClassLoader... cls)
* @param type
* If "it" is non-null, {@code it.getClass()}. Otherwise the class
* from which the view is searched.
* @see #createRequestDispatcher(AbstractTearOff, ScriptExecutor, Object, String)
*/
public RequestDispatcher createRequestDispatcher(RequestImpl request, Klass<?> type, Object it, String viewName) throws IOException {
@CheckForNull public RequestDispatcher createRequestDispatcher(RequestImpl request, Klass<?> type, Object it, String viewName) throws IOException {
return null; // should be really abstract, but not
}

public RequestDispatcher createRequestDispatcher(RequestImpl request, Class type, Object it, String viewName) throws IOException {
@CheckForNull public RequestDispatcher createRequestDispatcher(RequestImpl request, Class type, Object it, String viewName) throws IOException {
return createRequestDispatcher(request,Klass.java(type),it,viewName);
}

Expand All @@ -133,6 +136,7 @@ public RequestDispatcher createRequestDispatcher(RequestImpl request, Class type
*
* @return
* true if the processing succeeds. Otherwise false.
* @see #handleIndexRequest(AbstractTearOff, ScriptExecutor, RequestImpl, ResponseImpl, Object)
*/
public abstract boolean handleIndexRequest(RequestImpl req, ResponseImpl rsp, Object node, MetaClass nodeMetaClass) throws IOException, ServletException;

Expand Down Expand Up @@ -162,4 +166,144 @@ protected boolean isBasename(String potentialPath){
return true;
}
}

/**
* For Facets that require a particular file extension to be put in any case.
* Just return an empty String if the Facet does not want to have such behavior.
*
* If you do want to have an extension added, you must ensure you provide the dot at the first character position,
* see JellyFacet
*/
protected @Nonnull String getExtensionSuffix() {
return "";
}

/**
* Creates a Dispatcher that integrates {@link DispatchValidator} with the provided script loader and executor.
* If an exception or one of its causes is a {@link CancelRequestHandlingException}, this will cause the
* Dispatcher to cancel and return false, thus allowing for further dispatchers to attempt to handle the request.
* This also requires validation to pass before any output can be written to the response.
* In any error case, the configured {@link FilteredDispatchTriggerListener} will be notified.
*
* @param scriptLoader tear-off script loader to find views
* @param scriptExecutor script executor for rendering views
* @param <S> type of script
* @return dispatcher that handles scripts
* @see WebApp#setDispatchValidator(DispatchValidator)
* @see WebApp#setFilteredDispatchTriggerListener(FilteredDispatchTriggerListener)
* @since TODO
*/
@Nonnull protected <S> Dispatcher createValidatingDispatcher(@Nonnull AbstractTearOff<?, ? extends S, ?> scriptLoader,
@Nonnull ScriptExecutor<? super S> scriptExecutor) {
return new Dispatcher() {
@Override
public boolean dispatch(@Nonnull RequestImpl req, @Nonnull ResponseImpl rsp, @CheckForNull Object node) throws ServletException {
String next = req.tokens.peek();
if (next == null) {
return false;
}
// only match end of URL
if (req.tokens.countRemainingTokens() > 1) {
return false;
}
// avoid serving both foo and foo/ as they have different URL semantics
if (req.tokens.endsWithSlash) {
return false;
}
// prevent potential path traversal
if (!isBasename(next)) {
return false;
}
DispatchValidator validator = req.getWebApp().getDispatchValidator();
FilteredDispatchTriggerListener listener = req.getWebApp().getFilteredDispatchTriggerListener();
Boolean valid = validator.isDispatchAllowed(req, rsp, next, node);
if (valid != null && !valid) {
return listener.onDispatchTrigger(req, rsp, node, next);
}
S script;
try {
script = scriptLoader.findScript(next + getExtensionSuffix());
} catch (Exception e) {
throw new ServletException(e);
}
if (script == null) {
return false;
}
req.tokens.next();
anonymizedTraceEval(req, rsp, node, "%s: View: %s%s", next, scriptLoader.getDefaultScriptExtension());
if (traceable()) {
trace(req, rsp, "-> %s on <%s>", next, node);
}
try {
scriptExecutor.execute(req, rsp, script, node);
return true;
} catch (Exception e) {
req.tokens.prev();
for (Throwable cause = e; cause != null; cause = cause.getCause()) {
if (cause instanceof CancelRequestHandlingException) {
return listener.onDispatchTrigger(req, rsp, node, next);
}
}
throw new ServletException(e);
}
}

@Override
public String toString() {
return "VIEW" + scriptLoader.getDefaultScriptExtension() + " for url=/VIEW";
}
};
}

/**
* Handles an index request by dispatching a script.
* @since TODO
*/
protected <S> boolean handleIndexRequest(@Nonnull AbstractTearOff<?, ? extends S, ?> scriptLoader,
@Nonnull ScriptExecutor<? super S> scriptExecutor,
@Nonnull RequestImpl req,
@Nonnull ResponseImpl rsp,
@CheckForNull Object node)
throws ServletException, IOException {
S script;
try {
script = scriptLoader.findScript("index");
} catch (Exception e) {
throw new ServletException(e);
}
if (script == null) {
return false;
}
Dispatcher.anonymizedTraceEval(req, rsp, node, "Index: index%s", scriptLoader.getDefaultScriptExtension());
if (Dispatcher.traceable()) {
Dispatcher.trace(req, rsp, "-> index on <%s>", node);
}
try {
scriptExecutor.execute(req, rsp, script, node);
return true;
} catch (Exception e) {
throw new ServletException(e);
}
}

/**
* Creates a RequestDispatcher that integrates with {@link DispatchValidator} and
* {@link FilteredDispatchTriggerListener}.
*
* @param scriptLoader tear-off script loader for finding views
* @param scriptExecutor script executor for rendering views
* @param it the model node being dispatched against
* @param viewName name of the view to load and execute
* @param <S> view type
* @return a RequestDispatcher that performs similar validation to {@link #createValidatingDispatcher(AbstractTearOff, ScriptExecutor)}
* @see WebApp#setDispatchValidator(DispatchValidator)
* @see WebApp#setFilteredDispatchTriggerListener(FilteredDispatchTriggerListener)
* @since TODO
*/
@CheckForNull protected <S> RequestDispatcher createRequestDispatcher(@Nonnull AbstractTearOff<?, ? extends S, ?> scriptLoader,
@Nonnull ScriptExecutor<? super S> scriptExecutor,
@CheckForNull Object it,
@Nonnull String viewName) {
return ScriptRequestDispatcher.newRequestDispatcher(scriptLoader, scriptExecutor, viewName, it);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ public boolean dispatch(RequestImpl req, ResponseImpl rsp, Object node) throws I
if (req.tokens.hasMore())
return false;

// always allow index views to be dispatched
req.getWebApp().getDispatchValidator().allowDispatch(req, rsp);
return facet.handleIndexRequest(req, rsp, node, metaClass);
}

Expand Down
42 changes: 42 additions & 0 deletions core/src/main/java/org/kohsuke/stapler/ScriptExecutor.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*
* The MIT License
*
* Copyright (c) 2019 CloudBees, Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/

package org.kohsuke.stapler;

import javax.annotation.CheckForNull;
import javax.annotation.Nonnull;

/**
* Execution strategy for handling views written in other scripting languages.
*
* @param <S> script type
* @since TODO
*/
public interface ScriptExecutor<S> {

/**
* Executes the given script on the given node and request, rendering output to the given response.
*/
void execute(@Nonnull StaplerRequest req, @Nonnull StaplerResponse rsp, @Nonnull S script, @CheckForNull Object it) throws Exception;
}
Loading

0 comments on commit 12b55cb

Please sign in to comment.