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