Exemplo n.º 1
0
def fxa_oauth_token(request):
    """Return OAuth token from authorization code.
    """
    state = request.validated['state']
    code = request.validated['code']

    # Require on-going session
    stored_redirect = request.cache.get(state)

    # Make sure we cannot try twice with the same code
    request.registry.cache.delete(state)
    if not stored_redirect:
        return authorization_required(request)

    # Trade the OAuth code for a longer-lived token
    auth_client = OAuthClient(server_url=fxa_conf(request, 'oauth_uri'),
                              client_id=fxa_conf(request, 'client_id'),
                              client_secret=fxa_conf(request, 'client_secret'))
    try:
        token = auth_client.trade_code(code)
    except fxa_errors.OutOfProtocolError:
        raise httpexceptions.HTTPServiceUnavailable()
    except fxa_errors.InProtocolError as error:
        logger.error(error)
        error_details = {
            'name': 'code',
            'location': 'querystring',
            'description': 'Firefox Account code validation failed.'
        }
        errors.raise_invalid(request, **error_details)

    return httpexceptions.HTTPFound(location='%s%s' % (stored_redirect, token))
Exemplo n.º 2
0
def collection_get(request):
    collection_name = request.matchdict['collection_name']
    sync_client = build_sync_client(request)

    headers = import_headers(request)

    params = {}
    if '_since' in request.GET:
        try:
            params['newer'] = '%.2f' % (int(request.GET['_since']) / 1000.0)
        except ValueError:
            error_msg = ("_since should be a number.")
            raise_invalid(request,
                          location="querystring",
                          name="_since",
                          description=error_msg)

    if '_limit' in request.GET:
        params['limit'] = request.GET['_limit']

    if '_token' in request.GET:
        params['offset'] = request.GET['_token']

    if '_sort' in request.GET:
        if request.GET['_sort'] in ('-last_modified', 'newest'):
            params['sort'] = 'newest'

        elif request.GET['_sort'] in ('-sortindex', 'index'):
            params['sort'] = 'index'

        elif request.GET['_sort'] in ('last_modified', 'oldest'):
            params['sort'] = 'oldest'

        else:
            error_msg = ("_sort should be one of ('-last_modified', 'newest', "
                         "'-sortindex', 'index', 'last_modified', 'oldest')")
            raise_invalid(request,
                          location="querystring",
                          name="_sort",
                          description=error_msg)

    if 'in_ids' in request.GET:
        params['ids'] = [record_id.strip() for record_id in
                         request.GET['in_ids'].split(',') if record_id]

    records = sync_client.get_records(collection_name, full=True,
                                      headers=headers, **params)

    statsd_count(request, "syncclient.status_code.200")

    for r in records:
        r['last_modified'] = int(r.pop('modified') * 1000)

    # Configure headers
    export_headers(sync_client.raw_resp, request)

    if '_limit' in request.GET and 'Total-Records' in request.response.headers:
        del request.response.headers['Total-Records']

    return {'data': records or []}
Exemplo n.º 3
0
    def _raise_304_if_not_modified(self, record=None):
        """Raise 304 if current timestamp is inferior to the one specified
        in headers.

        :raises: :exc:`~pyramid:pyramid.httpexceptions.HTTPNotModified`
        """
        if_none_match = self.request.headers.get('If-None-Match')

        if not if_none_match:
            return

        if_none_match = decode_header(if_none_match)

        try:
            if not (if_none_match[0] == if_none_match[-1] == '"'):
                raise ValueError()
            modified_since = int(if_none_match[1:-1])
        except (IndexError, ValueError):
            if if_none_match == '*':
                return
            error_details = {
                'location': 'headers',
                'description': "Invalid value for If-None-Match"
            }
            raise_invalid(self.request, **error_details)

        if record:
            current_timestamp = record[self.model.modified_field]
        else:
            current_timestamp = self.model.timestamp()

        if current_timestamp <= modified_since:
            response = HTTPNotModified()
            self._add_timestamp_header(response, timestamp=current_timestamp)
            raise response
Exemplo n.º 4
0
    def _extract_pagination_rules_from_token(self, sorting):
        """Get pagination params."""
        queryparams = self.request.GET
        paginate_by = self.request.registry.settings['cliquet.paginate_by']
        limit = queryparams.get('_limit', paginate_by)
        if limit:
            try:
                limit = int(limit)
            except ValueError:
                error_details = {
                    'location': 'querystring',
                    'description': "_limit should be an integer"
                }
                raise_invalid(self.request, **error_details)

        # If limit is higher than paginate_by setting, ignore it.
        if limit and paginate_by:
            limit = min(limit, paginate_by)

        token = queryparams.get('_token', None)
        filters = []
        if token:
            try:
                last_record = decode_token(token)
                assert isinstance(last_record, dict)
            except (ValueError, TypeError, AssertionError):
                error_msg = '_token has invalid content'
                error_details = {
                    'location': 'querystring',
                    'description': error_msg
                }
                raise_invalid(self.request, **error_details)

            filters = self._build_pagination_rules(sorting, last_record)
        return filters, limit
Exemplo n.º 5
0
    def delete(self):
        """Record ``DELETE`` endpoint: delete a record and return it.

        :raises: :exc:`~pyramid:pyramid.httpexceptions.HTTPNotFound` if
            the record is not found.

        :raises:
            :exc:`~pyramid:pyramid.httpexceptions.HTTPPreconditionFailed` if
            ``If-Match`` header is provided and record modified
            in the iterim.
        """
        self._raise_400_if_invalid_id(self.record_id)
        record = self._get_record_or_404(self.record_id)
        self._raise_412_if_modified(record)

        # Retreive the last_modified information from a querystring if present.
        last_modified = self.request.GET.get('last_modified')
        if last_modified:
            last_modified = native_value(last_modified.strip('"'))
            if not isinstance(last_modified, six.integer_types):
                error_details = {
                    'name': 'last_modified',
                    'location': 'querystring',
                    'description': 'Invalid value for %s' % last_modified
                }
                raise_invalid(self.request, **error_details)

            # If less or equal than current record. Ignore it.
            if last_modified <= record[self.model.modified_field]:
                last_modified = None

        deleted = self.model.delete_record(record, last_modified=last_modified)
        return self.postprocess(deleted, action=ACTIONS.DELETE)
Exemplo n.º 6
0
def resource_create_object(request, resource_cls, uri, resource_name, obj_id):
    """In the default bucket, the bucket and collection are implicitly
    created. This helper instantiate the resource and simulate a request
    with its RootFactory on the instantiated resource.
    :returns: the created object
    :rtype: dict
    """
    # Fake context to instantiate a resource.
    context = RouteFactory(request)
    context.get_permission_object_id = lambda r, i: uri

    resource = resource_cls(request, context)

    # Check that provided id is valid for this resource.
    if not resource.model.id_generator.match(obj_id):
        error_details = {
            'location': 'path',
            'description': "Invalid %s id" % resource_name
        }
        raise_invalid(resource.request, **error_details)

    data = {'id': obj_id}
    try:
        obj = resource.model.create_record(data)
        # Since the current request is not a resource (but a straight Service),
        # we simulate a request on a resource.
        # This will be used in the resource event payload.
        resource.request.current_resource_name = resource_name
        resource.postprocess(data, action=ACTIONS.CREATE)
    except storage_exceptions.UnicityError as e:
        obj = e.record
    return obj
Exemplo n.º 7
0
    def delete(self):
        """Record ``DELETE`` endpoint: delete a record and return it.

        :raises: :exc:`~pyramid:pyramid.httpexceptions.HTTPNotFound` if
            the record is not found.

        :raises:
            :exc:`~pyramid:pyramid.httpexceptions.HTTPPreconditionFailed` if
            ``If-Match`` header is provided and record modified
            in the iterim.
        """
        self._raise_400_if_invalid_id(self.record_id)
        record = self._get_record_or_404(self.record_id)
        self._raise_412_if_modified(record)

        # Retreive the last_modified information from a querystring if present.
        last_modified = self.request.GET.get('last_modified')
        if last_modified:
            last_modified = native_value(last_modified.strip('"'))
            if not isinstance(last_modified, six.integer_types):
                error_details = {
                    'name': 'last_modified',
                    'location': 'querystring',
                    'description': 'Invalid value for %s' % last_modified
                }
                raise_invalid(self.request, **error_details)

            # If less or equal than current record. Ignore it.
            if last_modified <= record[self.model.modified_field]:
                last_modified = None

        deleted = self.model.delete_record(record, last_modified=last_modified)
        return self.postprocess(deleted, action=ACTIONS.DELETE)
Exemplo n.º 8
0
def resource_create_object(request, resource_cls, uri, resource_name, obj_id):
    """In the default bucket, the bucket and collection are implicitly
    created. This helper instantiate the resource and simulate a request
    with its RootFactory on the instantiated resource.
    :returns: the created object
    :rtype: dict
    """
    # Fake context to instantiate a resource.
    context = RouteFactory(request)
    context.get_permission_object_id = lambda r, i: uri

    resource = resource_cls(request, context)

    # Check that provided id is valid for this resource.
    if not resource.model.id_generator.match(obj_id):
        error_details = {
            'location': 'path',
            'description': "Invalid %s id" % resource_name
        }
        raise_invalid(resource.request, **error_details)

    data = {'id': obj_id}
    try:
        obj = resource.model.create_record(data)
        # Since the current request is not a resource (but a straight Service),
        # we simulate a request on a resource.
        # This will be used in the resource event payload.
        resource.request.current_resource_name = resource_name
        resource.postprocess(data, action=ACTIONS.CREATE)
    except storage_exceptions.UnicityError as e:
        obj = e.record
    return obj
Exemplo n.º 9
0
    def _extract_sorting(self, limit):
        """Extracts filters from QueryString parameters."""
        specified = self.request.GET.get('_sort', '').split(',')
        sorting = []
        modified_field_used = self.model.modified_field in specified
        for field in specified:
            field = field.strip()
            m = re.match(r'^([\-+]?)(\w+)$', field)
            if m:
                order, field = m.groups()

                if not self.is_known_field(field):
                    error_details = {
                        'location': 'querystring',
                        'description': "Unknown sort field '{0}'".format(field)
                    }
                    raise_invalid(self.request, **error_details)

                direction = -1 if order == '-' else 1
                sorting.append(Sort(field, direction))

        if not modified_field_used:
            # Add a sort by the ``modified_field`` in descending order
            # useful for pagination
            sorting.append(Sort(self.model.modified_field, -1))
        return sorting
Exemplo n.º 10
0
    def _extract_sorting(self, limit):
        """Extracts filters from QueryString parameters."""
        specified = self.request.GET.get('_sort', '').split(',')
        sorting = []
        modified_field_used = self.model.modified_field in specified
        for field in specified:
            field = field.strip()
            m = re.match(r'^([\-+]?)(\w+)$', field)
            if m:
                order, field = m.groups()

                if not self.is_known_field(field):
                    error_details = {
                        'location': 'querystring',
                        'description': "Unknown sort field '{0}'".format(field)
                    }
                    raise_invalid(self.request, **error_details)

                direction = -1 if order == '-' else 1
                sorting.append(Sort(field, direction))

        if not modified_field_used:
            # Add a sort by the ``modified_field`` in descending order
            # useful for pagination
            sorting.append(Sort(self.model.modified_field, -1))
        return sorting
Exemplo n.º 11
0
    def process_record(self, new, old=None):
        """Validate records against collection schema, if any."""
        new = super(Record, self).process_record(new, old)

        schema = self._collection.get('schema')
        settings = self.request.registry.settings
        schema_validation = 'experimental_collection_schema_validation'
        if not schema or not asbool(settings.get(schema_validation)):
            return new

        collection_timestamp = self._collection[self.model.modified_field]

        try:
            stripped = copy.deepcopy(new)
            stripped.pop(self.model.id_field, None)
            stripped.pop(self.model.modified_field, None)
            stripped.pop(self.model.permissions_field, None)
            stripped.pop(self.schema_field, None)
            jsonschema.validate(stripped, schema)
        except jsonschema_exceptions.ValidationError as e:
            try:
                field = e.path.pop() if e.path else e.validator_value.pop()
            except AttributeError:
                field = None
            raise_invalid(self.request, name=field, description=e.message)

        new[self.schema_field] = collection_timestamp
        return new
Exemplo n.º 12
0
    def _raise_304_if_not_modified(self, record=None):
        """Raise 304 if current timestamp is inferior to the one specified
        in headers.

        :raises: :exc:`~pyramid:pyramid.httpexceptions.HTTPNotModified`
        """
        if_none_match = self.request.headers.get('If-None-Match')

        if not if_none_match:
            return

        if_none_match = decode_header(if_none_match)

        try:
            if not (if_none_match[0] == if_none_match[-1] == '"'):
                raise ValueError()
            modified_since = int(if_none_match[1:-1])
        except (IndexError, ValueError):
            if if_none_match == '*':
                return
            error_details = {
                'location': 'headers',
                'description': "Invalid value for If-None-Match"
            }
            raise_invalid(self.request, **error_details)

        if record:
            current_timestamp = record[self.model.modified_field]
        else:
            current_timestamp = self.model.timestamp()

        if current_timestamp <= modified_since:
            response = HTTPNotModified()
            self._add_timestamp_header(response, timestamp=current_timestamp)
            raise response
Exemplo n.º 13
0
    def _extract_filters(self, queryparams=None):
        """Extracts filters from QueryString parameters."""
        if not queryparams:
            queryparams = self.request.GET

        filters = []

        for param, value in queryparams.items():
            param = param.strip()
            value = native_value(value)

            # Ignore specific fields
            if param.startswith('_') and param not in ('_since',
                                                       '_to',
                                                       '_before'):
                continue

            # Handle the _since specific filter.
            if param in ('_since', '_to', '_before'):
                if not isinstance(value, six.integer_types):
                    error_details = {
                        'name': param,
                        'location': 'querystring',
                        'description': 'Invalid value for %s' % param
                    }
                    raise_invalid(self.request, **error_details)

                if param == '_since':
                    operator = COMPARISON.GT
                else:
                    if param == '_to':
                        message = ('_to is now deprecated, '
                                   'you should use _before instead')
                        url = ('http://cliquet.rtfd.org/en/2.4.0/api/resource'
                               '.html#list-of-available-url-parameters')
                        send_alert(self.request, message, url)
                    operator = COMPARISON.LT
                filters.append(
                    Filter(self.collection.modified_field, value, operator)
                )
                continue

            m = re.match(r'^(min|max|not|lt|gt)_(\w+)$', param)
            if m:
                keyword, field = m.groups()
                operator = getattr(COMPARISON, keyword.upper())
            else:
                operator, field = COMPARISON.EQ, param

            if not self.is_known_field(field):
                error_details = {
                    'location': 'querystring',
                    'description': "Unknown filter field '{0}'".format(param)
                }
                raise_invalid(self.request, **error_details)

            filters.append(Filter(field, value, operator))

        return filters
Exemplo n.º 14
0
def collection_get(request):
    collection_name = request.matchdict["collection_name"]
    sync_client = build_sync_client(request)

    headers = import_headers(request)

    params = {}
    if "_since" in request.GET:
        try:
            params["newer"] = "%.2f" % (int(request.GET["_since"]) / 1000.0)
        except ValueError:
            error_msg = "_since should be a number."
            raise_invalid(request, location="querystring", name="_since", description=error_msg)

    if "_limit" in request.GET:
        params["limit"] = request.GET["_limit"]

    if "_token" in request.GET:
        params["offset"] = request.GET["_token"]

    if "_sort" in request.GET:
        if request.GET["_sort"] in ("-last_modified", "newest"):
            params["sort"] = "newest"

        elif request.GET["_sort"] in ("-sortindex", "index"):
            params["sort"] = "index"

        elif request.GET["_sort"] in ("last_modified", "oldest"):
            params["sort"] = "oldest"

        else:
            error_msg = (
                "_sort should be one of ('-last_modified', 'newest', "
                "'-sortindex', 'index', 'last_modified', 'oldest')"
            )
            raise_invalid(request, location="querystring", name="_sort", description=error_msg)

    if "in_ids" in request.GET:
        params["ids"] = [record_id.strip() for record_id in request.GET["in_ids"].split(",") if record_id]

    records = sync_client.get_records(collection_name, full=True, headers=headers, **params)

    status_code(request, str="syncclient.status_code.200")

    for r in records:
        r["last_modified"] = int(r.pop("modified") * 1000)

    # Configure headers
    export_headers(sync_client.raw_resp, request)

    if "_limit" in request.GET and "Total-Records" in request.response.headers:
        del request.response.headers["Total-Records"]

    if records:
        return {"data": records}
Exemplo n.º 15
0
    def _raise_400_if_invalid_id(self, record_id):
        """Raise 400 if specified record id does not match the format excepted
        by storage backends.

        :raises: :class:`pyramid.httpexceptions.HTTPBadRequest`
        """
        if not self.model.id_generator.match(six.text_type(record_id)):
            error_details = {
                'location': 'path',
                'description': "Invalid record id"
            }
            raise_invalid(self.request, **error_details)
Exemplo n.º 16
0
    def _raise_400_if_invalid_id(self, record_id):
        """Raise 400 if specified record id does not match the format excepted
        by storage backends.

        :raises: :class:`pyramid.httpexceptions.HTTPBadRequest`
        """
        if not self.model.id_generator.match(six.text_type(record_id)):
            error_details = {
                'location': 'path',
                'description': "Invalid record id"
            }
            raise_invalid(self.request, **error_details)
Exemplo n.º 17
0
    def _raise_400_if_id_mismatch(self, new_id, record_id):
        """Raise 400 if the `new_id`, within the request body, does not match
        the `record_id`, obtained from request path.

        :raises: :class:`pyramid.httpexceptions.HTTPBadRequest`
        """
        if new_id != record_id:
            error_msg = 'Record id does not match existing record'
            error_details = {
                'name': self.model.id_field,
                'description': error_msg
            }
            raise_invalid(self.request, **error_details)
Exemplo n.º 18
0
    def _raise_400_if_id_mismatch(self, new_id, record_id):
        """Raise 400 if the `new_id`, within the request body, does not match
        the `record_id`, obtained from request path.

        :raises: :class:`pyramid.httpexceptions.HTTPBadRequest`
        """
        if new_id != record_id:
            error_msg = 'Record id does not match existing record'
            error_details = {
                'name': self.model.id_field,
                'description': error_msg
            }
            raise_invalid(self.request, **error_details)
Exemplo n.º 19
0
    def process_record(self, new, old=None):
        """Operate changes on submitted record.
        This implementation represents the specifities of the *Reading List*
        article resource.

        In a future version, URL resolution (*redirects*) and article title
        obtention (*HTML content*) will be performed here.

        Contrary to article content fetching, this fields resolution has to
        be performed synchronously (i.e. withing request/response cycle) during
        article creation, otherwise unicity of ``resolved_url`` cannot be
        guaranteed.

        :note:

            This could moved to a specific end-point, in order to keep the
            article API aligned with behaviour generic resources.
        """
        if old:
            # Read position should be superior
            if old['read_position'] > new['read_position']:
                new['read_position'] = old['read_position']

            # Marking as read requires device info
            if old['unread'] and not new['unread']:
                if not any((new['marked_read_on'], new['marked_read_by'])):
                    error = 'Missing marked_read_by or marked_read_on fields'
                    errors.raise_invalid(self.request, name='unread',
                                         description=error)

            # Device info is ignored if already read
            if not old['unread']:
                new['marked_read_on'] = old['marked_read_on']
                new['marked_read_by'] = old['marked_read_by']
        else:
            # Date of creation is set
            new['stored_on'] = TimeStamp().deserialize()

        # In this first version, do not resolve url and title.
        if new['resolved_title'] is None:
            new['resolved_title'] = new['title']
        if new['resolved_url'] is None:
            new['resolved_url'] = new['url']

        # Reset info when article is marked as unread
        if new['unread'] and (old and not old['unread']):
            new['marked_read_on'] = None
            new['marked_read_by'] = None
            new['read_position'] = 0

        return new
Exemplo n.º 20
0
    def _raise_412_if_modified(self, record=None):
        """Raise 412 if current timestamp is superior to the one
        specified in headers.

        :raises:
            :exc:`~pyramid:pyramid.httpexceptions.HTTPPreconditionFailed`
        """
        if_match = self.request.headers.get('If-Match')
        if_none_match = self.request.headers.get('If-None-Match')

        if not if_match and not if_none_match:
            return

        if_match = decode_header(if_match) if if_match else None

        if record and if_none_match and decode_header(if_none_match) == '*':
            if record.get(self.model.deleted_field, False):
                # Tombstones should not prevent creation.
                return
            modified_since = -1  # Always raise.
        elif if_match:
            try:
                if not (if_match[0] == if_match[-1] == '"'):
                    raise ValueError()
                modified_since = int(if_match[1:-1])
            except (IndexError, ValueError):
                message = ("Invalid value for If-Match. The value should "
                           "be integer between double quotes.")
                error_details = {
                    'location': 'headers',
                    'description': message
                }
                raise_invalid(self.request, **error_details)
        else:
            # In case _raise_304_if_not_modified() did not raise.
            return

        if record:
            current_timestamp = record[self.model.modified_field]
        else:
            current_timestamp = self.model.timestamp()

        if current_timestamp > modified_since:
            error_msg = 'Resource was modified meanwhile'
            details = {'existing': record} if record else {}
            response = http_error(HTTPPreconditionFailed(),
                                  errno=ERRORS.MODIFIED_MEANWHILE,
                                  message=error_msg,
                                  details=details)
            self._add_timestamp_header(response, timestamp=current_timestamp)
            raise response
Exemplo n.º 21
0
    def _extract_filters(self, queryparams=None):
        """Extracts filters from QueryString parameters."""
        if not queryparams:
            queryparams = self.request.GET

        filters = []

        for param, value in queryparams.items():
            param = param.strip()
            value = native_value(value)

            # Ignore specific fields
            if param.startswith('_') and param not in ('_since', '_to'):
                continue

            # Handle the _since specific filter.
            if param in ('_since', '_to'):
                if not isinstance(value, six.integer_types):
                    error_details = {
                        'name': param,
                        'location': 'querystring',
                        'description': 'Invalid value for _since'
                    }
                    raise_invalid(self.request, **error_details)

                if param == '_since':
                    operator = COMPARISON.GT
                else:
                    operator = COMPARISON.LT
                filters.append(
                    Filter(self.collection.modified_field, value, operator)
                )
                continue

            m = re.match(r'^(min|max|not|lt|gt)_(\w+)$', param)
            if m:
                keyword, field = m.groups()
                operator = getattr(COMPARISON, keyword.upper())
            else:
                operator, field = COMPARISON.EQ, param

            if not self.is_known_field(field):
                error_details = {
                    'location': 'querystring',
                    'description': "Unknown filter field '{0}'".format(param)
                }
                raise_invalid(self.request, **error_details)

            filters.append(Filter(field, value, operator))

        return filters
Exemplo n.º 22
0
def collection_get(request):
    collection_name = request.matchdict['collection_name']
    sync_client = build_sync_client(request)

    params = {}
    if '_since' in request.GET:
        params['newer'] = request.GET['_since']

    if '_limit' in request.GET:
        params['limit'] = request.GET['_limit']

    if '_token' in request.GET:
        params['offset'] = request.GET['_token']

    if '_sort' in request.GET:
        if request.GET['_sort'] in ('-last_modified', 'newest'):
            params['sort'] = 'newest'

        elif request.GET['_sort'] in ('-sortindex', 'index'):
            params['sort'] = 'index'

        else:
            error_msg = ("_sort should be one of ('-last_modified', 'newest', "
                         "'-sortindex', 'index')")
            raise_invalid(request,
                          location="querystring",
                          name="_sort",
                          description=error_msg)

    if 'ids' in request.GET:
        try:
            params['ids'] = [uuid4_to_base64(record_id.strip())
                             for record_id in request.GET['ids'].split(',')
                             if record_id]
        except ValueError:
            raise_invalid(request,
                          location="querystring",
                          name="ids",
                          description="Invalid id in ids list.")

    records = sync_client.get_records(collection_name, full=True, **params)

    for r in records:
        r['last_modified'] = int(r.pop('modified') * 1000)
        r['id'] = base64_to_uuid4(r.pop('id'))

    # Configure headers
    convert_headers(sync_client.raw_resp, request.response)

    return {'data': records}
Exemplo n.º 23
0
def attachment_post(request):
    # Remove potential existing attachment.
    delete_attachment(request)

    # Store file locally.
    folder_pattern = request.registry.settings.get('attachment.folder', '')
    folder = folder_pattern.format(**request.matchdict) or None
    content = request.POST[FILE_FIELD]
    try:
        location = request.attachment.save(content,
                                           randomize=True,
                                           folder=folder)
    except FileNotAllowed:
        error_msg = 'File extension is not allowed.'
        raise_invalid(request, location='body', description=error_msg)

    # Read file to compute hash.
    content.file.seek(0)
    filecontent = content.file.read()

    # File metadata.
    fullurl = request.attachment.url(location)
    size = len(filecontent)
    filehash = sha256(filecontent)
    attachment = {
        'filename': content.filename,
        'location': fullurl,
        'hash': filehash,
        'mimetype': content.type,
        'size': size
    }

    # Store link between record and attachment (for later deletion).
    request.registry.storage.create("", FILE_LINKS, {
        'location': location,  # store relative location.
        'bucket_uri': bucket_uri(request),
        'collection_uri': collection_uri(request),
        'record_uri': record_uri(request)
    })

    # Update related record.
    record = {k: v for k, v in request.POST.items() if k != FILE_FIELD}
    for k, v in record.items():
        record[k] = json.loads(v)
    record.setdefault('data', {})[FILE_FIELD] = attachment
    save_record(record, request)

    # Return attachment data (with location header)
    request.response.headers['Location'] = record_uri(request, prefix=True)
    return attachment
Exemplo n.º 24
0
    def _raise_412_if_modified(self, record=None):
        """Raise 412 if current timestamp is superior to the one
        specified in headers.

        :raises:
            :exc:`~pyramid:pyramid.httpexceptions.HTTPPreconditionFailed`
        """
        if_match = self.request.headers.get('If-Match')
        if_none_match = self.request.headers.get('If-None-Match')

        if not if_match and not if_none_match:
            return

        if_match = decode_header(if_match) if if_match else None

        if record and if_none_match and decode_header(if_none_match) == '*':
            if record.get(self.model.deleted_field, False):
                # Tombstones should not prevent creation.
                return
            modified_since = -1  # Always raise.
        elif if_match:
            try:
                if not (if_match[0] == if_match[-1] == '"'):
                    raise ValueError()
                modified_since = int(if_match[1:-1])
            except (IndexError, ValueError):
                message = ("Invalid value for If-Match. The value should "
                           "be integer between double quotes.")
                error_details = {'location': 'headers', 'description': message}
                raise_invalid(self.request, **error_details)
        else:
            # In case _raise_304_if_not_modified() did not raise.
            return

        if record:
            current_timestamp = record[self.model.modified_field]
        else:
            current_timestamp = self.model.timestamp()

        if current_timestamp > modified_since:
            error_msg = 'Resource was modified meanwhile'
            details = {'existing': record} if record else {}
            response = http_error(HTTPPreconditionFailed(),
                                  errno=ERRORS.MODIFIED_MEANWHILE,
                                  message=error_msg,
                                  details=details)
            self._add_timestamp_header(response, timestamp=current_timestamp)
            raise response
Exemplo n.º 25
0
def import_headers(syncto_request, sync_request_headers=None):
    """Convert incoming Kinto headers into Sync headers."""
    request_headers = syncto_request.headers
    headers = sync_request_headers or {}

    # For server-side analytics, keep original User-Agent.

    ua = "Syncto/%s" % __version__
    original_ua = request_headers.get('User-Agent')
    if original_ua:
        ua += " (on behalf of %s)" % original_ua
    headers['User-Agent'] = ua

    # Handle concurrency control.
    if 'If-Match' in request_headers:
        if_match = request_headers['If-Match']
        try:
            assert if_match[0] == if_match[-1] == '"'
            unmodified_since = int(if_match[1:-1])
        except (IndexError, AssertionError, ValueError):
            error_details = {
                'location': 'headers',
                'description': "Invalid value for If-Match"
            }
            raise_invalid(syncto_request, **error_details)
        else:
            headers['X-If-Unmodified-Since'] = '%.2f' % (
                int(unmodified_since) / 1000.0)

    if 'If-None-Match' in request_headers:
        if_none_match = request_headers['If-None-Match']
        if if_none_match == '*':
            headers['X-If-Unmodified-Since'] = 0
        else:
            try:
                assert if_none_match[0] == if_none_match[-1] == '"'
                modified_since = int(if_none_match[1:-1])
            except (IndexError, AssertionError, ValueError):
                error_details = {
                    'location': 'headers',
                    'description': "Invalid value for If-None-Match"
                }
                raise_invalid(syncto_request, **error_details)

            headers['X-If-Modified-Since'] = '%.2f' % (int(modified_since) /
                                                       1000.0)

    return headers
Exemplo n.º 26
0
    def patch(self):
        """Record `PATCH` endpoint."""
        record = self.get_record(self.record_id)
        self._raise_412_if_modified(record)

        if not self.request.body:
            error_details = {
                'description': 'Empty body'
            }
            raise_invalid(self.request, **error_details)

        changes = self.request.json

        updated = self.apply_changes(record, changes=changes)
        updated = self.process_record(updated, old=record)

        record = self.update_record(record, updated, changes)
        return record
Exemplo n.º 27
0
    def apply_changes(self, record, changes):
        """Merge `changes` into `record` fields.

        .. note::

            This is used in the context of PATCH only.

        Override this to control field changes at record level, for example:

        .. code-block:: python

            def apply_changes(self, record, changes):
                # Ignore value change if inferior
                if record['position'] > changes.get('position', -1):
                    changes.pop('position', None)
                return super(MyResource, self).apply_changes(record, changes)

        :raises: :exc:`~pyramid:pyramid.httpexceptions.HTTPBadRequest`
            if result does not comply with resource schema.

        :returns: the new record with `changes` applied.
        :rtype: dict
        """
        for field, value in changes.items():
            has_changed = record.get(field, value) != value
            if self.mapping.is_readonly(field) and has_changed:
                error_details = {
                    'name': field,
                    'description': 'Cannot modify {0}'.format(field)
                }
                raise_invalid(self.request, **error_details)

        updated = record.copy()
        updated.update(**changes)

        try:
            return self.mapping.deserialize(updated)
        except colander.Invalid as e:
            # Transform the errors we got from colander into Cornice errors.
            # We could not rely on Service schema because the record should be
            # validated only once the changes are applied
            for field, error in e.asdict().items():
                raise_invalid(self.request, name=field, description=error)
Exemplo n.º 28
0
    def _extract_limit(self):
        """Extract limit value from QueryString parameters."""
        paginate_by = self.request.registry.settings['paginate_by']
        limit = self.request.GET.get('_limit', paginate_by)
        if limit:
            try:
                limit = int(limit)
            except ValueError:
                error_details = {
                    'location': 'querystring',
                    'description': "_limit should be an integer"
                }
                raise_invalid(self.request, **error_details)

        # If limit is higher than paginate_by setting, ignore it.
        if limit and paginate_by:
            limit = min(limit, paginate_by)

        return limit
Exemplo n.º 29
0
    def _extract_pagination_rules_from_token(self, limit, sorting):
        """Get pagination params."""
        queryparams = self.request.GET
        token = queryparams.get('_token', None)
        filters = []
        if token:
            try:
                last_record = json.loads(decode64(token))
                assert isinstance(last_record, dict)
            except (ValueError, TypeError, AssertionError):
                error_msg = '_token has invalid content'
                error_details = {
                    'location': 'querystring',
                    'description': error_msg
                }
                raise_invalid(self.request, **error_details)

            filters = self._build_pagination_rules(sorting, last_record)
        return filters
Exemplo n.º 30
0
    def _raise_412_if_modified(self, record=None):
        """Raise 412 if current timestamp is superior to the one
        specified in headers.

        :raises:
            :exc:`~pyramid:pyramid.httpexceptions.HTTPPreconditionFailed`
        """
        if_match = self.request.headers.get('If-Match')
        if_none_match = self.request.headers.get('If-None-Match')

        if not if_match and not if_none_match:
            return

        if_match = if_match.decode('utf-8') if if_match else None

        if if_none_match and if_none_match.decode('utf-8') == '*':
            modified_since = -1  # Always raise.
        elif if_match:
            try:
                assert if_match[0] == if_match[-1] == '"'
                modified_since = int(if_match[1:-1])
            except (IndexError, AssertionError, ValueError):
                error_details = {
                    'location': 'headers',
                    'description': "Invalid value for If-Match"
                }
                raise_invalid(self.request, **error_details)
        else:
            # In case _raise_304_if_not_modified() did not raise.
            return

        if record:
            current_timestamp = record[self.collection.modified_field]
        else:
            current_timestamp = self.collection.timestamp()

        if current_timestamp > modified_since:
            error_msg = 'Resource was modified meanwhile'
            response = http_error(HTTPPreconditionFailed(),
                                  errno=ERRORS.MODIFIED_MEANWHILE,
                                  message=error_msg)
            self._add_timestamp_header(response, timestamp=current_timestamp)
            raise response
Exemplo n.º 31
0
    def _extract_limit(self):
        """Extract limit value from QueryString parameters."""
        paginate_by = self.request.registry.settings['paginate_by']
        limit = self.request.GET.get('_limit', paginate_by)
        if limit:
            try:
                limit = int(limit)
            except ValueError:
                error_details = {
                    'location': 'querystring',
                    'description': "_limit should be an integer"
                }
                raise_invalid(self.request, **error_details)

        # If limit is higher than paginate_by setting, ignore it.
        if limit and paginate_by:
            limit = min(limit, paginate_by)

        return limit
Exemplo n.º 32
0
    def apply_changes(self, record, changes):
        """Merge `changes` into `record` fields.

        .. note::

            This is used in the context of PATCH only.

        Override this to control field changes at record level, for example:

        .. code-block:: python

            def apply_changes(self, record, changes):
                # Ignore value change if inferior
                if record['position'] > changes.get('position', -1):
                    changes.pop('position', None)
                return super(MyResource, self).apply_changes(record, changes)

        :raises: :exc:`~pyramid:pyramid.httpexceptions.HTTPBadRequest`
            if result does not comply with resource schema.

        :returns: the new record with `changes` applied.
        :rtype: dict
        """
        for field, value in changes.items():
            has_changed = record.get(field, value) != value
            if self.mapping.is_readonly(field) and has_changed:
                error_details = {
                    'name': field,
                    'description': 'Cannot modify {0}'.format(field)
                }
                raise_invalid(self.request, **error_details)

        updated = record.copy()
        updated.update(**changes)

        try:
            return self.mapping.deserialize(updated)
        except colander.Invalid as e:
            # Transform the errors we got from colander into Cornice errors.
            # We could not rely on Service schema because the record should be
            # validated only once the changes are applied
            for field, error in e.asdict().items():
                raise_invalid(self.request, name=field, description=error)
Exemplo n.º 33
0
    def process_record(self, new, old=None):
        """Validate records against collection schema, if any."""
        new = super(Record, self).process_record(new, old)

        schema = self._collection.get('schema')
        settings = self.request.registry.settings
        schema_validation = 'kinto.experimental_collection_schema_validation'
        if not schema or not asbool(settings.get(schema_validation)):
            return new

        collection_timestamp = self._collection[self.collection.modified_field]

        try:
            jsonschema.validate(new, schema)
            new[self.schema_field] = collection_timestamp
        except jsonschema_exceptions.ValidationError as e:
            field = e.path.pop() if e.path else e.validator_value.pop()
            raise_invalid(self.request, name=field, description=e.message)

        return new
Exemplo n.º 34
0
    def process_record(self, new, old=None):
        """Validate records against collection schema, if any."""
        new = super(Record, self).process_record(new, old)

        schema = self._collection.get('schema')
        settings = self.request.registry.settings
        schema_validation = 'experimental_collection_schema_validation'
        if not schema or not asbool(settings.get(schema_validation)):
            return new

        collection_timestamp = self._collection[self.model.modified_field]

        try:
            jsonschema.validate(new, schema)
            new[self.schema_field] = collection_timestamp
        except jsonschema_exceptions.ValidationError as e:
            field = e.path.pop() if e.path else e.validator_value.pop()
            raise_invalid(self.request, name=field, description=e.message)

        return new
Exemplo n.º 35
0
def create_collection(request, bucket_id):
    # Do nothing if current request does not involve a collection.
    subpath = request.matchdict.get('subpath')
    if not (subpath and subpath.startswith('collections/')):
        return

    collection_id = subpath.split('/')[1]
    collection_uri = '/buckets/%s/collections/%s' % (bucket_id, collection_id)

    # Do not intent to create multiple times per request (e.g. in batch).
    already_created = request.bound_data.setdefault('collections', {})
    if collection_uri in already_created:
        return

    # Do nothing if current request will already create the collection.
    collection_put = (request.method.lower() == 'put' and
                      request.path.endswith(collection_id))
    if collection_put:
        return

    # Fake context to instantiate a Collection resource.
    context = RouteFactory(request)
    context.get_permission_object_id = lambda r, i: collection_uri

    backup = request.matchdict
    request.matchdict = dict(bucket_id=bucket_id,
                             id=collection_id,
                             **request.matchdict)
    resource = Collection(request, context)
    if not resource.model.id_generator.match(collection_id):
        error_details = {
            'location': 'path',
            'description': "Invalid collection_id id"
        }
        raise_invalid(request, **error_details)
    try:
        collection = resource.model.create_record({'id': collection_id})
    except storage_exceptions.UnicityError as e:
        collection = e.record
    already_created[collection_uri] = collection
    request.matchdict = backup
Exemplo n.º 36
0
def create_collection(request, bucket_id):
    # Do nothing if current request does not involve a collection.
    subpath = request.matchdict.get('subpath')
    if not (subpath and subpath.startswith('collections/')):
        return

    collection_id = subpath.split('/')[1]
    collection_uri = '/buckets/%s/collections/%s' % (bucket_id, collection_id)

    # Do not intent to create multiple times per request (e.g. in batch).
    already_created = request.bound_data.setdefault('collections', {})
    if collection_uri in already_created:
        return

    # Do nothing if current request will already create the collection.
    collection_put = (request.method.lower() == 'put'
                      and request.path.endswith(collection_id))
    if collection_put:
        return

    # Fake context to instantiate a Collection resource.
    context = RouteFactory(request)
    context.get_permission_object_id = lambda r, i: collection_uri

    backup = request.matchdict
    request.matchdict = dict(bucket_id=bucket_id,
                             id=collection_id,
                             **request.matchdict)
    resource = Collection(request, context)
    if not resource.model.id_generator.match(collection_id):
        error_details = {
            'location': 'path',
            'description': "Invalid collection_id id"
        }
        raise_invalid(request, **error_details)
    try:
        collection = resource.model.create_record({'id': collection_id})
    except storage_exceptions.UnicityError as e:
        collection = e.record
    already_created[collection_uri] = collection
    request.matchdict = backup
Exemplo n.º 37
0
def import_headers(syncto_request, sync_request_headers=None):
    """Convert incoming Kinto headers into Sync headers."""
    request_headers = syncto_request.headers
    headers = sync_request_headers or {}

    if 'If-Match' in request_headers:
        if_match = request_headers['If-Match']
        try:
            assert if_match[0] == if_match[-1] == '"'
            unmodified_since = int(if_match[1:-1])
        except (IndexError, AssertionError, ValueError):
            error_details = {
                'location': 'headers',
                'description': "Invalid value for If-Match"
            }
            raise_invalid(syncto_request, **error_details)
        else:
            headers['X-If-Unmodified-Since'] = '%.2f' % (
                int(unmodified_since) / 1000.0)

    if 'If-None-Match' in request_headers:
        if_none_match = request_headers['If-None-Match']
        if if_none_match == '"*"':
            headers['X-If-Unmodified-Since'] = 0
        else:
            try:
                assert if_none_match[0] == if_none_match[-1] == '"'
                modified_since = int(if_none_match[1:-1])
            except (IndexError, AssertionError, ValueError):
                error_details = {
                    'location': 'headers',
                    'description': "Invalid value for If-None-Match"
                }
                raise_invalid(syncto_request, **error_details)

            headers['X-If-Modified-Since'] = '%.2f' % (
                int(modified_since) / 1000.0)

    return headers
Exemplo n.º 38
0
    def put(self):
        """Record `PUT` endpoint."""
        try:
            existing = self.get_record(self.record_id)
            self._raise_412_if_modified(existing)
        except HTTPNotFound:
            existing = None

        new_record = self.request.validated

        new_id = new_record.setdefault(self.id_field, self.record_id)
        if new_id != self.record_id:
            error_msg = 'Record id does not match existing record'
            error_details = {
                'name': self.id_field,
                'location': 'querystring',
                'description': error_msg
            }
            raise_invalid(self.request, **error_details)

        new_record = self.process_record(new_record, old=existing)
        record = self.update_record(existing, new_record)
        return record
Exemplo n.º 39
0
    def _extract_partial_fields(self):
        """Extract the fields to do the projection from QueryString parameters.
        """
        fields = self.request.GET.get('_fields', None)
        if fields:
            fields = fields.split(',')
            root_fields = [f.split('.')[0] for f in fields]
            known_fields = self._get_known_fields()
            invalid_fields = set(root_fields) - set(known_fields)
            preserve_unknown = self.mapping.get_option('preserve_unknown')
            if not preserve_unknown and invalid_fields:
                error_msg = "Fields %s do not exist" % ','.join(invalid_fields)
                error_details = {
                    'name': "Invalid _fields parameter",
                    'description': error_msg
                }
                raise_invalid(self.request, **error_details)

            # Since id and last_modified are part of the synchronisation
            # protocol, force their presence in payloads.
            fields = fields + [self.model.id_field, self.model.modified_field]

        return fields
Exemplo n.º 40
0
    def _extract_pagination_rules_from_token(self, limit, sorting):
        """Get pagination params."""
        queryparams = self.request.GET
        token = queryparams.get('_token', None)
        filters = []
        offset = 0
        if token:
            try:
                tokeninfo = json.loads(decode64(token))
                if not isinstance(tokeninfo, dict):
                    raise ValueError()
                last_record = tokeninfo['last_record']
                offset = tokeninfo['offset']
            except (ValueError, KeyError, TypeError):
                error_msg = '_token has invalid content'
                error_details = {
                    'location': 'querystring',
                    'description': error_msg
                }
                raise_invalid(self.request, **error_details)

            filters = self._build_pagination_rules(sorting, last_record)
        return filters, offset
Exemplo n.º 41
0
    def _extract_partial_fields(self):
        """Extract the fields to do the projection from QueryString parameters.
        """
        fields = self.request.GET.get('_fields', None)
        if fields:
            fields = fields.split(',')
            root_fields = [f.split('.')[0] for f in fields]
            known_fields = self._get_known_fields()
            invalid_fields = set(root_fields) - set(known_fields)
            preserve_unknown = self.mapping.get_option('preserve_unknown')
            if not preserve_unknown and invalid_fields:
                error_msg = "Fields %s do not exist" % ','.join(invalid_fields)
                error_details = {
                    'name': "Invalid _fields parameter",
                    'description': error_msg
                }
                raise_invalid(self.request, **error_details)

            # Since id and last_modified are part of the synchronisation
            # protocol, force their presence in payloads.
            fields = fields + [self.model.id_field, self.model.modified_field]

        return fields
Exemplo n.º 42
0
    def _extract_pagination_rules_from_token(self, limit, sorting):
        """Get pagination params."""
        queryparams = self.request.GET
        token = queryparams.get('_token', None)
        filters = []
        offset = 0
        if token:
            try:
                tokeninfo = json.loads(decode64(token))
                if not isinstance(tokeninfo, dict):
                    raise ValueError()
                last_record = tokeninfo['last_record']
                offset = tokeninfo['offset']
            except (ValueError, KeyError, TypeError):
                error_msg = '_token has invalid content'
                error_details = {
                    'location': 'querystring',
                    'description': error_msg
                }
                raise_invalid(self.request, **error_details)

            filters = self._build_pagination_rules(sorting, last_record)
        return filters, offset
Exemplo n.º 43
0
    def patch(self):
        """Record ``PATCH`` endpoint: modify a record and return its
        new version.

        :raises:
            :exc:`~pyramid:pyramid.httpexceptions.HTTPPreconditionFailed` if
            ``If-Unmodified-Since`` header is provided and record modified
            in the iterim.

        .. seealso::
            Add custom behaviour by overriding
            :meth:`cliquet.resource.BaseResource.get_record`,
            :meth:`cliquet.resource.BaseResource.apply_changes`,
            :meth:`cliquet.resource.BaseResource.process_record` or
            :meth:`cliquet.resource.BaseResource.update_record`.
        """
        self._raise_400_if_invalid_id(self.record_id)
        record = self.get_record(self.record_id)
        self._raise_412_if_modified(record)

        if not self.request.body:
            error_details = {
                'description': 'Empty body'
            }
            raise_invalid(self.request, **error_details)

        changes = self.request.json

        updated = self.apply_changes(record, changes=changes)

        record_id = updated.setdefault(self.id_field, self.record_id)
        self._raise_400_if_id_mismatch(record_id, self.record_id)

        updated = self.process_record(updated, old=record)

        record = self.update_record(record, updated, changes)
        return record
Exemplo n.º 44
0
def save_file(content, request, randomize=True):
    folder_pattern = request.registry.settings.get('attachment.folder', '')
    folder = folder_pattern.format(**request.matchdict) or None

    try:
        location = request.attachment.save(content, folder=folder,
                                           randomize=randomize)
    except FileNotAllowed:
        error_msg = 'File extension is not allowed.'
        raise_invalid(request, location='body', description=error_msg)

    # Read file to compute hash.
    content.file.seek(0)
    filecontent = content.file.read()

    # File metadata.
    fullurl = request.attachment.url(location)
    size = len(filecontent)
    filehash = sha256(filecontent)
    attachment = {
        'filename': content.filename,
        'location': fullurl,
        'hash': filehash,
        'mimetype': content.type,
        'size': size
    }

    # Store link between record and attachment (for later deletion).
    request.registry.storage.create("", FILE_LINKS, {
        'location': location,  # store relative location.
        'bucket_uri': bucket_uri(request),
        'collection_uri': collection_uri(request),
        'record_uri': record_uri(request)
    })

    return attachment
Exemplo n.º 45
0
def collection_get(request):
    collection_name = request.matchdict['collection_name']
    sync_client = build_sync_client(request)

    headers = import_headers(request)

    params = {}
    if '_since' in request.GET:
        try:
            params['newer'] = '%.2f' % (int(request.GET['_since']) / 1000.0)
        except ValueError:
            error_msg = ("_since should be a number.")
            raise_invalid(request,
                          location="querystring",
                          name="_since",
                          description=error_msg)

    if '_limit' in request.GET:
        params['limit'] = request.GET['_limit']

    if '_token' in request.GET:
        params['offset'] = request.GET['_token']

    if '_sort' in request.GET:
        if request.GET['_sort'] in ('-last_modified', 'newest'):
            params['sort'] = 'newest'

        elif request.GET['_sort'] in ('-sortindex', 'index'):
            params['sort'] = 'index'

        elif request.GET['_sort'] in ('last_modified', 'oldest'):
            params['sort'] = 'oldest'

        else:
            error_msg = ("_sort should be one of ('-last_modified', 'newest', "
                         "'-sortindex', 'index', 'last_modified', 'oldest')")
            raise_invalid(request,
                          location="querystring",
                          name="_sort",
                          description=error_msg)

    if 'in_ids' in request.GET:
        params['ids'] = [
            record_id.strip() for record_id in request.GET['in_ids'].split(',')
            if record_id
        ]

    records = sync_client.get_records(collection_name,
                                      full=True,
                                      headers=headers,
                                      **params)

    statsd_count(request, "syncclient.status_code.200")

    for r in records:
        r['last_modified'] = int(r.pop('modified') * 1000)

    # Configure headers
    export_headers(sync_client.raw_resp, request)

    if '_limit' in request.GET and 'Total-Records' in request.response.headers:
        del request.response.headers['Total-Records']

    return {'data': records or []}
Exemplo n.º 46
0
    def _extract_filters(self, queryparams=None):
        """Extracts filters from QueryString parameters."""
        if not queryparams:
            queryparams = self.request.GET

        filters = []

        for param, paramvalue in queryparams.items():
            param = param.strip()

            error_details = {
                'name': param,
                'location': 'querystring',
                'description': 'Invalid value for %s' % param
            }

            # Ignore specific fields
            if param.startswith('_') and param not in ('_since',
                                                       '_to',
                                                       '_before'):
                continue

            # Handle the _since specific filter.
            if param in ('_since', '_to', '_before'):
                value = native_value(paramvalue.strip('"'))

                if not isinstance(value, six.integer_types):
                    raise_invalid(self.request, **error_details)

                if param == '_since':
                    operator = COMPARISON.GT
                else:
                    if param == '_to':
                        message = ('_to is now deprecated, '
                                   'you should use _before instead')
                        url = ('http://cliquet.rtfd.org/en/2.4.0/api/resource'
                               '.html#list-of-available-url-parameters')
                        send_alert(self.request, message, url)
                    operator = COMPARISON.LT
                filters.append(
                    Filter(self.model.modified_field, value, operator)
                )
                continue

            m = re.match(r'^(min|max|not|lt|gt|in|exclude)_(\w+)$', param)
            if m:
                keyword, field = m.groups()
                operator = getattr(COMPARISON, keyword.upper())
            else:
                operator, field = COMPARISON.EQ, param

            if not self.is_known_field(field):
                error_msg = "Unknown filter field '{0}'".format(param)
                error_details['description'] = error_msg
                raise_invalid(self.request, **error_details)

            value = native_value(paramvalue)
            if operator in (COMPARISON.IN, COMPARISON.EXCLUDE):
                value = set([native_value(v) for v in paramvalue.split(',')])

                all_integers = all([isinstance(v, six.integer_types)
                                    for v in value])
                all_strings = all([isinstance(v, six.text_type)
                                   for v in value])
                has_invalid_value = (
                    (field == self.model.id_field and not all_strings) or
                    (field == self.model.modified_field and not all_integers)
                )
                if has_invalid_value:
                    raise_invalid(self.request, **error_details)

            filters.append(Filter(field, value, operator))

        return filters
Exemplo n.º 47
0
    def patch(self):
        """Record ``PATCH`` endpoint: modify a record and return its
        new version.

        If a request header ``Response-Behavior`` is set to ``light``,
        only the fields whose value was changed are returned.
        If set to ``diff``, only the fields whose value became different than
        the one provided are returned.

        :raises: :exc:`~pyramid:pyramid.httpexceptions.HTTPNotFound` if
            the record is not found.

        :raises:
            :exc:`~pyramid:pyramid.httpexceptions.HTTPPreconditionFailed` if
            ``If-Match`` header is provided and record modified
            in the iterim.

        .. seealso::
            Add custom behaviour by overriding
            :meth:`cliquet.resource.UserResource.apply_changes` or
            :meth:`cliquet.resource.UserResource.process_record`.
        """
        self._raise_400_if_invalid_id(self.record_id)
        existing = self._get_record_or_404(self.record_id)
        self._raise_412_if_modified(existing)

        try:
            # `data` attribute may not be present if only perms are patched.
            changes = self.request.json.get('data', {})
        except ValueError:
            # If no `data` nor `permissions` is provided in patch, reject!
            # XXX: This should happen in schema instead (c.f. ShareableViewSet)
            error_details = {
                'name': 'data',
                'description': 'Provide at least one of data or permissions',
            }
            raise_invalid(self.request, **error_details)

        updated = self.apply_changes(existing, changes=changes)

        record_id = updated.setdefault(self.model.id_field, self.record_id)
        self._raise_400_if_id_mismatch(record_id, self.record_id)

        new_record = self.process_record(updated, old=existing)

        changed_fields = [
            k for k in changes.keys() if existing.get(k) != new_record.get(k)
        ]

        # Save in storage if necessary.
        if changed_fields or self.force_patch_update:
            try:
                unique_fields = self.mapping.get_option('unique_fields')
                new_record = self.model.update_record(
                    new_record, unique_fields=unique_fields)
            except storage_exceptions.UnicityError as e:
                self._raise_conflict(e)
        else:
            # Behave as if storage would have added `id` and `last_modified`.
            for extra_field in [
                    self.model.modified_field, self.model.id_field
            ]:
                new_record[extra_field] = existing[extra_field]

        # Adjust response according to ``Response-Behavior`` header
        body_behavior = self.request.headers.get('Response-Behavior', 'full')

        if body_behavior.lower() == 'light':
            # Only fields that were changed.
            data = {k: new_record[k] for k in changed_fields}

        elif body_behavior.lower() == 'diff':
            # Only fields that are different from those provided.
            data = {
                k: new_record[k]
                for k in changed_fields if changes.get(k) != new_record.get(k)
            }
        else:
            data = new_record

        timestamp = new_record.get(self.model.modified_field,
                                   existing[self.model.modified_field])
        self._add_timestamp_header(self.request.response, timestamp=timestamp)

        return self.postprocess(data, action=ACTIONS.UPDATE, old=existing)
Exemplo n.º 48
0
    def _extract_filters(self, queryparams=None):
        """Extracts filters from QueryString parameters."""
        if not queryparams:
            queryparams = self.request.GET

        filters = []

        for param, paramvalue in queryparams.items():
            param = param.strip()

            error_details = {
                'name': param,
                'location': 'querystring',
                'description': 'Invalid value for %s' % param
            }

            # Ignore specific fields
            if param.startswith('_') and param not in ('_since', '_to',
                                                       '_before'):
                continue

            # Handle the _since specific filter.
            if param in ('_since', '_to', '_before'):
                value = native_value(paramvalue.strip('"'))

                if not isinstance(value, six.integer_types):
                    raise_invalid(self.request, **error_details)

                if param == '_since':
                    operator = COMPARISON.GT
                else:
                    if param == '_to':
                        message = ('_to is now deprecated, '
                                   'you should use _before instead')
                        url = ('http://cliquet.rtfd.org/en/2.4.0/api/resource'
                               '.html#list-of-available-url-parameters')
                        send_alert(self.request, message, url)
                    operator = COMPARISON.LT
                filters.append(
                    Filter(self.model.modified_field, value, operator))
                continue

            m = re.match(r'^(min|max|not|lt|gt|in|exclude)_(\w+)$', param)
            if m:
                keyword, field = m.groups()
                operator = getattr(COMPARISON, keyword.upper())
            else:
                operator, field = COMPARISON.EQ, param

            if not self.is_known_field(field):
                error_msg = "Unknown filter field '{0}'".format(param)
                error_details['description'] = error_msg
                raise_invalid(self.request, **error_details)

            value = native_value(paramvalue)
            if operator in (COMPARISON.IN, COMPARISON.EXCLUDE):
                value = set([native_value(v) for v in paramvalue.split(',')])

                all_integers = all(
                    [isinstance(v, six.integer_types) for v in value])
                all_strings = all(
                    [isinstance(v, six.text_type) for v in value])
                has_invalid_value = (
                    (field == self.model.id_field and not all_strings) or
                    (field == self.model.modified_field and not all_integers))
                if has_invalid_value:
                    raise_invalid(self.request, **error_details)

            filters.append(Filter(field, value, operator))

        return filters