001package ca.uhn.fhir.rest.server.interceptor;
002
003/*-
004 * #%L
005 * HAPI FHIR - Server Framework
006 * %%
007 * Copyright (C) 2014 - 2022 Smile CDR, Inc.
008 * %%
009 * Licensed under the Apache License, Version 2.0 (the "License");
010 * you may not use this file except in compliance with the License.
011 * You may obtain a copy of the License at
012 *
013 *      http://www.apache.org/licenses/LICENSE-2.0
014 *
015 * Unless required by applicable law or agreed to in writing, software
016 * distributed under the License is distributed on an "AS IS" BASIS,
017 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
018 * See the License for the specific language governing permissions and
019 * limitations under the License.
020 * #L%
021 */
022
023import ca.uhn.fhir.context.RuntimeSearchParam;
024import ca.uhn.fhir.i18n.HapiLocalizer;
025import ca.uhn.fhir.i18n.Msg;
026import ca.uhn.fhir.interceptor.api.Hook;
027import ca.uhn.fhir.interceptor.api.Interceptor;
028import ca.uhn.fhir.interceptor.api.Pointcut;
029import ca.uhn.fhir.rest.api.Constants;
030import ca.uhn.fhir.rest.api.PreferHandlingEnum;
031import ca.uhn.fhir.rest.api.PreferHeader;
032import ca.uhn.fhir.rest.api.server.IRestfulServer;
033import ca.uhn.fhir.rest.api.server.RequestDetails;
034import ca.uhn.fhir.rest.server.RestfulServer;
035import ca.uhn.fhir.rest.server.RestfulServerUtils;
036import ca.uhn.fhir.rest.server.exceptions.AuthenticationException;
037import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
038import ca.uhn.fhir.rest.server.method.SearchMethodBinding;
039import ca.uhn.fhir.rest.server.util.ISearchParamRegistry;
040import org.apache.commons.lang3.Validate;
041
042import javax.annotation.Nonnull;
043import javax.annotation.Nullable;
044import javax.servlet.http.HttpServletRequest;
045import javax.servlet.http.HttpServletResponse;
046import java.util.HashMap;
047import java.util.List;
048import java.util.stream.Collectors;
049
050import static org.apache.commons.lang3.StringUtils.isNotBlank;
051
052/**
053 * @since 5.4.0
054 */
055@Interceptor
056public class SearchPreferHandlingInterceptor {
057
058        @Nonnull
059        private PreferHandlingEnum myDefaultBehaviour;
060        @Nullable
061        private ISearchParamRegistry mySearchParamRegistry;
062
063        /**
064         * Constructor that uses the {@link RestfulServer} itself to determine
065         * the allowable search params.
066         */
067        public SearchPreferHandlingInterceptor() {
068                setDefaultBehaviour(PreferHandlingEnum.STRICT);
069        }
070
071        /**
072         * Constructor that uses a dedicated {@link ISearchParamRegistry} instance. This is mainly
073         * intended for the JPA server.
074         */
075        public SearchPreferHandlingInterceptor(ISearchParamRegistry theSearchParamRegistry) {
076                this();
077                mySearchParamRegistry = theSearchParamRegistry;
078        }
079
080        @Hook(Pointcut.SERVER_INCOMING_REQUEST_PRE_HANDLER_SELECTED)
081        public void incomingRequestPostProcessed(RequestDetails theRequestDetails, HttpServletRequest theRequest, HttpServletResponse theResponse) throws AuthenticationException {
082                if (!SearchMethodBinding.isPlainSearchRequest(theRequestDetails)) {
083                        return;
084                }
085
086                String resourceName = theRequestDetails.getResourceName();
087                if (!theRequestDetails.getFhirContext().getResourceTypes().contains(resourceName)) {
088                        // This is an error. Let the server handle it normally.
089                        return;
090                }
091
092                String preferHeader = theRequestDetails.getHeader(Constants.HEADER_PREFER);
093                PreferHandlingEnum handling = null;
094                if (isNotBlank(preferHeader)) {
095                        PreferHeader parsedPreferHeader = RestfulServerUtils.parsePreferHeader((IRestfulServer<?>) theRequestDetails.getServer(), preferHeader);
096                        handling = parsedPreferHeader.getHanding();
097                }
098
099                // Default behaviour
100                if (handling == null) {
101                        handling = getDefaultBehaviour();
102                }
103
104                removeUnwantedParams(handling, theRequestDetails);
105        }
106
107        private void removeUnwantedParams(PreferHandlingEnum theHandling, RequestDetails theRequestDetails) {
108
109                ISearchParamRegistry searchParamRetriever = mySearchParamRegistry;
110                if (searchParamRetriever == null) {
111                        searchParamRetriever = ((RestfulServer) theRequestDetails.getServer()).createConfiguration();
112                }
113
114                String resourceName = theRequestDetails.getResourceName();
115                HashMap<String, String[]> newMap = null;
116                for (String paramName : theRequestDetails.getParameters().keySet()) {
117                        if (paramName.startsWith("_")) {
118                                continue;
119                        }
120
121                        // Strip modifiers and chains
122                        for (int i = 0; i < paramName.length(); i++) {
123                                char nextChar = paramName.charAt(i);
124                                if (nextChar == '.' || nextChar == ':') {
125                                        paramName = paramName.substring(0, i);
126                                        break;
127                                }
128                        }
129
130                        RuntimeSearchParam activeSearchParam = searchParamRetriever.getActiveSearchParam(resourceName, paramName);
131                        if (activeSearchParam == null) {
132
133                                if (theHandling == PreferHandlingEnum.LENIENT) {
134
135                                        if (newMap == null) {
136                                                newMap = new HashMap<>(theRequestDetails.getParameters());
137                                        }
138
139                                        newMap.remove(paramName);
140
141                                } else {
142
143                                        // Strict handling
144                                        List<String> allowedParams = searchParamRetriever.getActiveSearchParams(resourceName).getSearchParamNames().stream().sorted().distinct().collect(Collectors.toList());
145                                        HapiLocalizer localizer = theRequestDetails.getFhirContext().getLocalizer();
146                                        String msg = localizer.getMessage("ca.uhn.fhir.jpa.dao.BaseStorageDao.invalidSearchParameter", paramName, resourceName, allowedParams);
147                                        throw new InvalidRequestException(Msg.code(323) + msg);
148
149                                }
150                        }
151
152                }
153
154                if (newMap != null) {
155                        theRequestDetails.setParameters(newMap);
156                }
157        }
158
159        public PreferHandlingEnum getDefaultBehaviour() {
160                return myDefaultBehaviour;
161        }
162
163        public void setDefaultBehaviour(@Nonnull PreferHandlingEnum theDefaultBehaviour) {
164                Validate.notNull(theDefaultBehaviour, "theDefaultBehaviour must not be null");
165                myDefaultBehaviour = theDefaultBehaviour;
166        }
167}