88from rest_framework import status
99from rest_framework .permissions import SAFE_METHODS
1010from rest_framework .response import Response
11+ from rest_framework_extensions .exceptions import PreconditionRequiredException
1112
1213from rest_framework_extensions .utils import prepare_header_name
1314from rest_framework_extensions .settings import extensions_api_settings
@@ -29,7 +30,7 @@ def __call__(self, func):
2930 this = self
3031
3132 @wraps (func , assigned = available_attrs (func ))
32- def wrapper (self , request , * args , ** kwargs ):
33+ def inner (self , request , * args , ** kwargs ):
3334 return this .process_conditional_request (
3435 view_instance = self ,
3536 view_method = func ,
@@ -38,7 +39,7 @@ def wrapper(self, request, *args, **kwargs):
3839 kwargs = kwargs ,
3940 )
4041
41- return wrapper
42+ return inner
4243
4344 def process_conditional_request (self ,
4445 view_instance ,
@@ -135,21 +136,68 @@ def _get_and_log_precondition_failed_response(self, request):
135136 )
136137 return Response (status = status .HTTP_412_PRECONDITION_FAILED )
137138
138- #
139- # class APIETAGProcessor(ETAGProcessor):
140- # """
141- # This class is responsible for calculating the ETag value given (a list of) model instance(s).
142- #
143- # It does not make sense to compute a default ETag here, because the processor would always issue a 304 response,
144- # even if the response was modified meanwhile.
145- # Therefore the `APIETAGProcessor` cannot be used without specifying an `etag_func` as keyword argument.
146- # """
147- # def __init__(self, etag_func=None, rebuild_after_method_evaluation=False):
148- # assert etag_func is not None, ('None-type functions are not allowed for processing API ETags.'
149- # 'You must specify a proper function to calculate the API ETags '
150- # 'using the "etag_func" keyword argument.')
151- #
152- # super(APIETAGProcessor, self).__init__(etag_func=etag_func,
153- # rebuild_after_method_evaluation=rebuild_after_method_evaluation)
139+
140+ class APIETAGProcessor (ETAGProcessor ):
141+ """
142+ This class is responsible for calculating the ETag value given (a list of) model instance(s).
143+
144+ It does not make sense to compute a default ETag here, because the processor would always issue a 304 response,
145+ even if the response was modified meanwhile.
146+ Therefore the `APIETAGProcessor` cannot be used without specifying an `etag_func` as keyword argument.
147+
148+ According to RFC 6585, conditional headers may be enforced for certain services that support conditional
149+ requests. For optimistic locking, the server should respond status code 428 including a description on how
150+ to resubmit the request successfully, see https://tools.ietf.org/html/rfc6585#section-3.
151+ """
152+
153+ # require a pre-conditional header (e.g. If-Match) for unsafe HTTP methods (RFC 6585)
154+ # override this defaults, if required
155+ precondition_map = {'PUT' : ['If-Match' ],
156+ 'PATCH' : ['If-Match' ],
157+ 'DELETE' : ['If-Match' ]}
158+
159+ def __init__ (self , etag_func = None , rebuild_after_method_evaluation = False , precondition_map = None ):
160+ assert etag_func is not None , ('None-type functions are not allowed for processing API ETags.'
161+ 'You must specify a proper function to calculate the API ETags '
162+ 'using the "etag_func" keyword argument.' )
163+
164+ if precondition_map is not None :
165+ self .precondition_map = precondition_map
166+ assert isinstance (self .precondition_map , dict ), ('`precondition_map` must be a dict, where '
167+ 'the key is the HTTP verb, and the value is a list of '
168+ 'HTTP headers that must all be present for that request.' )
169+
170+ super (APIETAGProcessor , self ).__init__ (etag_func = etag_func ,
171+ rebuild_after_method_evaluation = rebuild_after_method_evaluation )
172+
173+ def get_etags_and_matchers (self , request ):
174+ """Get the etags from the header and perform a validation against the required preconditions."""
175+ # evaluate the preconditions, raises 428 if condition is not met
176+ self .evaluate_preconditions (request )
177+ # alright, headers are present, extract the values and match the conditions
178+ return super (APIETAGProcessor , self ).get_etags_and_matchers (request )
179+
180+ def evaluate_preconditions (self , request ):
181+ """Evaluate whether the precondition for the request is met."""
182+ if request .method .upper () in self .precondition_map .keys ():
183+ required_headers = self .precondition_map .get (request .method .upper (), [])
184+ # check the required headers
185+ for header in required_headers :
186+ if not request .META .get (prepare_header_name (header )):
187+ # raise an error for each header that does not match
188+ logger .warning ('Precondition required: %s' , request .path ,
189+ extra = {
190+ 'status_code' : status .HTTP_428_PRECONDITION_REQUIRED ,
191+ 'request' : request
192+ }
193+ )
194+ # raise an RFC 6585 compliant exception
195+ raise PreconditionRequiredException (detail = 'Precondition required. This "%s" request '
196+ 'is required to be conditional. '
197+ 'Try again using "%s".' % (request .method , header )
198+ )
199+ return True
200+
154201
155202etag = ETAGProcessor
203+ api_etag = APIETAGProcessor
0 commit comments