Exemplo n.º 1
0
class ReviewViewSet(AddonChildMixin, ModelViewSet):
    serializer_class = ReviewSerializer
    permission_classes = [
        ByHttpMethod({
            'get':
            AllowAny,
            'head':
            AllowAny,
            'options':
            AllowAny,  # Needed for CORS.

            # Deletion requires a specific permission check.
            'delete':
            CanDeleteReviewPermission,

            # To post a review you just need to be authenticated.
            'post':
            IsAuthenticated,

            # To edit a review you need to be the author or be an admin.
            'patch':
            AnyOf(AllowOwner, GroupPermission(amo.permissions.ADDONS_EDIT)),

            # Implementing PUT would be a little incoherent as we don't want to
            # allow users to change `version` but require it at creation time.
            # So only PATCH is allowed for editing.
        }),
    ]
    reply_permission_classes = [
        AnyOf(
            GroupPermission(amo.permissions.ADDONS_EDIT),
            AllowRelatedObjectPermissions('addon', [AllowAddonAuthor]),
        )
    ]
    reply_serializer_class = ReviewSerializerReply

    def set_addon_object_from_review(self, review):
        """Set addon object on the instance from a review object."""
        # At this point it's likely we didn't have an addon in the request, so
        # if we went through get_addon_object() before it's going to be set
        # to None already. We delete the addon_object property cache and set
        # addon_pk in kwargs to force get_addon_object() to reset
        # self.addon_object.
        del self.addon_object
        self.kwargs['addon_pk'] = str(review.addon.pk)
        return self.get_addon_object()

    def get_addon_object(self):
        """Return addon object associated with the request, or None if not
        relevant.

        Will also fire permission checks on the addon object when it's loaded.
        """
        if hasattr(self, 'addon_object'):
            return self.addon_object

        if 'addon_pk' not in self.kwargs:
            self.kwargs['addon_pk'] = (self.request.data.get('addon')
                                       or self.request.GET.get('addon'))
        if not self.kwargs['addon_pk']:
            # If we don't have an addon object, set it as None on the instance
            # and return immediately, that's fine.
            self.addon_object = None
            return
        else:
            # AddonViewSet.get_lookup_field() expects a string.
            self.kwargs['addon_pk'] = force_text(self.kwargs['addon_pk'])
        # When loading the add-on, pass a specific permission class - the
        # default from AddonViewSet is too restrictive, we are not modifying
        # the add-on itself so we don't need all the permission checks it does.
        return super(ReviewViewSet,
                     self).get_addon_object(permission_classes=[AllowIfPublic])

    def check_permissions(self, request):
        """Perform permission checks.

        The regular DRF permissions checks are made, but also, before that, if
        an addon was requested, verify that it exists, is public and listed,
        through AllowIfPublic permission, that get_addon_object() uses."""
        self.get_addon_object()

        # Proceed with the regular permission checks.
        return super(ReviewViewSet, self).check_permissions(request)

    def get_serializer(self, *args, **kwargs):
        if self.action in ('partial_update', 'update'):
            instance = args[0]
            if instance.reply_to is not None:
                self.review_object = instance.reply_to
                self.serializer_class = self.reply_serializer_class
        return super(ReviewViewSet, self).get_serializer(*args, **kwargs)

    def filter_queryset(self, qs):
        if self.action == 'list':
            addon_identifier = self.request.GET.get('addon')
            user_identifier = self.request.GET.get('user')
            version_identifier = self.request.GET.get('version')
            if addon_identifier:
                qs = qs.filter(addon=self.get_addon_object())
            if user_identifier:
                try:
                    user_identifier = int(user_identifier)
                except ValueError:
                    raise ParseError('user parameter should be an integer.')
                qs = qs.filter(user=user_identifier)
            if version_identifier:
                try:
                    version_identifier = int(version_identifier)
                except ValueError:
                    raise ParseError('version parameter should be an integer.')
                qs = qs.filter(version=version_identifier)
            elif addon_identifier:
                # When filtering on addon but not on version, only return the
                # latest review posted by each user.
                qs = qs.filter(is_latest=True)
            if not addon_identifier and not user_identifier:
                # Don't allow listing reviews without filtering by add-on or
                # user.
                raise ParseError('Need an addon or user parameter')
            if user_identifier and addon_identifier and version_identifier:
                # When user, addon and version identifiers are set, we are
                # effectively only looking for one or zero objects. Fake
                # pagination in that case, avoiding all count() calls and
                # therefore related cache-machine invalidation issues. Needed
                # because the frontend wants to call this before and after
                # having posted a new review, and needs accurate results.
                self.pagination_class = OneOrZeroPageNumberPagination
        return super(ReviewViewSet, self).filter_queryset(qs)

    def get_paginated_response(self, data):
        response = super(ReviewViewSet, self).get_paginated_response(data)
        if 'show_grouped_ratings' in self.request.GET:
            try:
                show_grouped_ratings = (
                    serializers.BooleanField().to_internal_value(
                        self.request.GET['show_grouped_ratings']))
            except serializers.ValidationError:
                raise ParseError(
                    'show_grouped_ratings parameter should be a boolean')
            if show_grouped_ratings and self.get_addon_object():
                response.data['grouped_ratings'] = dict(
                    GroupedRating.get(self.addon_object.id))
        return response

    def get_queryset(self):
        requested = self.request.GET.get('filter', '').split(',')
        has_addons_edit = acl.action_allowed(self.request,
                                             amo.permissions.ADDONS_EDIT)

        # Add this as a property of the view, because we need to pass down the
        # information to the serializer to show/hide delete replies.
        if not hasattr(self, 'should_access_deleted_reviews'):
            self.should_access_deleted_reviews = (
                ('with_deleted' in requested or self.action != 'list')
                and self.request.user.is_authenticated() and has_addons_edit)

        should_access_only_top_level_reviews = (self.action == 'list'
                                                and self.get_addon_object())

        if self.should_access_deleted_reviews:
            # For admins or add-on authors replying. When listing, we include
            # deleted reviews but still filter out out replies, because they'll
            # be in the serializer anyway. For other actions, we simply remove
            # any filtering, allowing them to access any review out of the box
            # with no extra parameter needed.
            if self.action == 'list':
                queryset = Review.unfiltered.filter(reply_to__isnull=True)
            else:
                queryset = Review.unfiltered.all()
        elif should_access_only_top_level_reviews:
            # When listing add-on reviews, exclude replies, they'll be
            # included during serialization as children of the relevant
            # reviews instead.
            queryset = Review.without_replies.all()
        else:
            queryset = Review.objects.all()

        # Filter out empty reviews if specified.
        # Should the users own empty reviews be filtered back in?
        if 'with_yours' in requested and self.request.user.is_authenticated():
            user_filter = Q(user=self.request.user.pk)
        else:
            user_filter = Q()
        # Apply the filter(s)
        if 'without_empty_body' in requested:
            queryset = queryset.filter(~Q(body=None) | user_filter)

        # The serializer needs reply, version (only the "version" field) and
        # user. We don't need much for version and user, so we can make joins
        # with select_related(), but for replies additional queries will be
        # made for translations anyway so we're better off using
        # prefetch_related() to make a separate query to fetch them all.
        queryset = queryset.select_related('version__version', 'user')
        replies_qs = Review.unfiltered.select_related('user')
        return queryset.prefetch_related(Prefetch('reply',
                                                  queryset=replies_qs))

    @detail_route(methods=['post'],
                  permission_classes=reply_permission_classes,
                  serializer_class=reply_serializer_class)
    def reply(self, *args, **kwargs):
        # A reply is just like a regular post, except that we set the reply
        # FK to the current review object and only allow add-on authors/admins.
        # Call get_object() to trigger 404 if it does not exist.
        self.review_object = self.get_object()
        self.set_addon_object_from_review(self.review_object)
        if Review.unfiltered.filter(reply_to=self.review_object).exists():
            # A reply already exists, just edit it.
            # We set should_access_deleted_reviews so that it works even if
            # the reply has been deleted.
            self.kwargs['pk'] = kwargs['pk'] = self.review_object.reply.pk
            self.should_access_deleted_reviews = True
            return self.partial_update(*args, **kwargs)
        return self.create(*args, **kwargs)

    @detail_route(methods=['post'])
    def flag(self, request, *args, **kwargs):
        # We load the add-on object from the review to trigger permission
        # checks.
        self.review_object = self.get_object()
        self.set_addon_object_from_review(self.review_object)

        # Re-use flag view since it's already returning json. We just need to
        # pass it the addon slug (passing it the PK would result in a redirect)
        # and make sure request.POST is set with whatever data was sent to the
        # DRF view.
        request._request.POST = request.data
        request = request._request
        response = flag(request, self.addon_object.slug, kwargs.get('pk'))
        if response.status_code == 200:
            response.content = ''
            response.status_code = 202
        return response

    def perform_destroy(self, instance):
        instance.delete(user_responsible=self.request.user)
Exemplo n.º 2
0
 def test_has_permission(self):
     request = RequestFactory().get('/')
     assert AnyOf(AllowNone, AllowAny)().has_permission(request, myview)
     assert AnyOf(AllowAny, AllowNone)().has_permission(request, myview)
Exemplo n.º 3
0
 def test_has_object_permission_fail(self):
     request = RequestFactory().get('/')
     assert not AnyOf(AllowNone, AllowNone)().has_object_permission(
         request, myview, None)
Exemplo n.º 4
0
class AccountNotificationViewSet(ListModelMixin, GenericViewSet):
    """Returns account notifications.

    If not already set by the user, defaults will be returned.
    """

    permission_classes = [IsAuthenticated]
    # We're pushing the primary permission checking to AccountViewSet for ease.
    account_permission_classes = [
        AnyOf(AllowSelf, GroupPermission(amo.permissions.USERS_EDIT))]
    serializer_class = UserNotificationSerializer
    paginator = None

    def get_account_viewset(self):
        if not hasattr(self, 'account_viewset'):
            self.account_viewset = AccountViewSet(
                request=self.request,
                permission_classes=self.account_permission_classes,
                kwargs={'pk': self.kwargs['user_pk']})
        return self.account_viewset

    def _get_default_object(self, notification):
        return UserNotification(
            user=self.get_account_viewset().get_object(),
            notification_id=notification.id,
            enabled=notification.default_checked)

    def get_queryset(self):
        user = self.get_account_viewset().get_object()
        queryset = UserNotification.objects.filter(user=user)

        # Fetch all `UserNotification` instances and then,
        # overwrite their value with the data from basket.

        # Put it into a dict so we can easily check for existence.
        set_notifications = {
            user_nfn.notification.short: user_nfn for user_nfn in queryset
            if user_nfn.notification}
        out = []

        newsletters = None  # Lazy - fetch the first time needed.
        by_basket_id = REMOTE_NOTIFICATIONS_BY_BASKET_ID
        for basket_id, notification in by_basket_id.items():
            if notification.group == 'dev' and not user.is_developer:
                # We only return dev notifications for developers.
                continue
            if newsletters is None:
                newsletters = fetch_subscribed_newsletters(user)
            user_notification = self._get_default_object(notification)
            user_notification.enabled = basket_id in newsletters
            set_notifications[notification.short] = user_notification

        for notification in NOTIFICATIONS_COMBINED:
            if notification.group == 'dev' and not user.is_developer:
                # We only return dev notifications for developers.
                continue
            out.append(set_notifications.get(
                notification.short,  # It's been set by the user.
                self._get_default_object(notification)))  # Or, default.
        return out

    def create(self, request, *args, **kwargs):
        # Loop through possible notifications.
        queryset = self.get_queryset()
        for notification in queryset:
            # Careful with ifs.  Enabled will be None|True|False.
            enabled = request.data.get(notification.notification.short)
            if enabled is not None:
                serializer = self.get_serializer(
                    notification, partial=True, data={'enabled': enabled})
                serializer.is_valid(raise_exception=True)
                serializer.save()
        return Response(self.get_serializer(queryset, many=True).data)
Exemplo n.º 5
0
class AddonViewSet(
    CreateModelMixin, RetrieveModelMixin, UpdateModelMixin, GenericViewSet
):
    write_permission_classes = [
        APIGatePermission('addon-submission-api'),
        AllowAddonAuthor,
        AllowIfNotMozillaDisabled,
        AllowIfNotSitePermission,
        IsSubmissionAllowedFor,
    ]
    permission_classes = [
        AnyOf(
            AllowReadOnlyIfPublic,
            AllowAddonAuthor,
            AllowListedViewerOrReviewer,
            AllowUnlistedViewerOrReviewer,
        )
    ]
    authentication_classes = [
        JWTKeyAuthentication,
        WebTokenAuthentication,
        SessionIDAuthentication,
    ]
    georestriction_classes = [
        RegionalRestriction | GroupPermission(amo.permissions.ADDONS_EDIT)
    ]
    serializer_class = AddonSerializer
    serializer_class_with_unlisted_data = AddonSerializerWithUnlistedData
    lookup_value_regex = '[^/]+'  # Allow '.' for email-like guids.
    throttle_classes = addon_submission_throttles

    def get_queryset(self):
        """Return queryset to be used for the view."""
        # Special case: admins - and only admins - can see deleted add-ons.
        # This is handled outside a permission class because that condition
        # would pollute all other classes otherwise.
        if self.request.user.is_authenticated and acl.action_allowed(
            self.request, amo.permissions.ADDONS_VIEW_DELETED
        ):
            qs = Addon.unfiltered.all()
        else:
            # Permission classes disallow access to non-public/unlisted add-ons
            # unless logged in as a reviewer/addon owner/admin, so we don't
            # have to filter the base queryset here.
            qs = Addon.objects.all()
        if self.action == 'retrieve_from_related':
            # Avoid default transformers if we're fetching a single instance
            # from a related view: We're unlikely to need the preloading they
            # bring, this would only cause extra useless queries. Still include
            # translations because at least the addon name is likely to be
            # needed in most cases.
            qs = qs.only_translations()
        return qs

    def get_serializer_class(self):
        # Override serializer to use serializer_class_with_unlisted_data if
        # we are allowed to access unlisted data.
        obj = getattr(self, 'instance', None)
        request = self.request
        if acl.check_unlisted_addons_viewer_or_reviewer(request) or (
            obj
            and request.user.is_authenticated
            and obj.authors.filter(pk=request.user.pk).exists()
        ):
            return self.serializer_class_with_unlisted_data
        return self.serializer_class

    def get_lookup_field(self, identifier):
        return Addon.get_lookup_field(identifier)

    def get_object(self):
        identifier = self.kwargs.get('pk')
        self.lookup_field = self.get_lookup_field(identifier)
        self.kwargs[self.lookup_field] = identifier
        self.instance = super().get_object()
        return self.instance

    def check_permissions(self, request):
        for restriction in self.get_georestrictions():
            if not restriction.has_permission(request, self):
                raise UnavailableForLegalReasons()
        if self.action in ('create', 'update', 'partial_update'):
            self.permission_classes = self.write_permission_classes
        super().check_permissions(request)

    def check_object_permissions(self, request, obj):
        """
        Check if the request should be permitted for a given object.
        Raises an appropriate exception if the request is not permitted.

        Calls DRF implementation, but adds `is_disabled_by_developer` and
        `is_disabled_by_mozilla` to the exception being thrown so that clients
        can tell the difference between a 401/403 returned because an add-on
        has been disabled by their developer or something else.
        """
        for restriction in self.get_georestrictions():
            if not restriction.has_object_permission(request, self, obj):
                raise UnavailableForLegalReasons()

        if self.action in ('update', 'partial_update'):
            self.permission_classes = self.write_permission_classes
        try:
            super().check_object_permissions(request, obj)
        except exceptions.APIException as exc:
            # Override exc.detail with a dict so that it's returned as-is in
            # the response. The base implementation for exc.get_codes() does
            # not expect dicts in that format, so override it as well with a
            # lambda that returns what would have been returned before our
            # changes.
            codes = exc.get_codes()
            exc.get_codes = lambda: codes
            exc.detail = {
                'detail': exc.detail,
                'is_disabled_by_developer': obj.disabled_by_user,
                'is_disabled_by_mozilla': obj.status == amo.STATUS_DISABLED,
            }
            raise exc

    def get_georestrictions(self):
        return [perm() for perm in self.georestriction_classes]

    @action(detail=True)
    def eula_policy(self, request, pk=None):
        obj = self.get_object()
        serializer = AddonEulaPolicySerializer(
            obj, context=self.get_serializer_context()
        )
        return Response(serializer.data)
Exemplo n.º 6
0
class RatingViewSet(AddonChildMixin, ModelViewSet):
    serializer_class = RatingSerializer
    permission_classes = [
        ByHttpMethod({
            'get':
            AllowAny,
            'head':
            AllowAny,
            'options':
            AllowAny,  # Needed for CORS.
            # Deletion requires a specific permission check.
            'delete':
            CanDeleteRatingPermission,
            # To post a rating you just need to be authenticated.
            'post':
            IsAuthenticated,
            # To edit a rating you need to be the author or be an admin.
            'patch':
            AnyOf(AllowOwner, GroupPermission(amo.permissions.ADDONS_EDIT)),
            # Implementing PUT would be a little incoherent as we don't want to
            # allow users to change `version` but require it at creation time.
            # So only PATCH is allowed for editing.
        }),
    ]
    reply_permission_classes = [
        AnyOf(
            GroupPermission(amo.permissions.ADDONS_EDIT),
            AllowRelatedObjectPermissions('addon', [AllowAddonAuthor]),
        )
    ]
    reply_serializer_class = RatingSerializerReply
    flag_permission_classes = [AllowNotOwner]
    throttle_classes = (RatingThrottle, )

    def set_addon_object_from_rating(self, rating):
        """Set addon object on the instance from a rating object."""
        # At this point it's likely we didn't have an addon in the request, so
        # if we went through get_addon_object() before it's going to be set
        # to None already. We delete the addon_object property cache and set
        # addon_pk in kwargs to force get_addon_object() to reset
        # self.addon_object.
        del self.addon_object
        self.kwargs['addon_pk'] = str(rating.addon.pk)
        return self.get_addon_object()

    def get_addon_object(self):
        """Return addon object associated with the request, or None if not
        relevant.

        Will also fire permission checks on the addon object when it's loaded.
        """
        if hasattr(self, 'addon_object'):
            return self.addon_object

        if 'addon_pk' not in self.kwargs:
            self.kwargs['addon_pk'] = self.request.data.get(
                'addon') or self.request.GET.get('addon')
        if not self.kwargs['addon_pk']:
            # If we don't have an addon object, set it as None on the instance
            # and return immediately, that's fine.
            self.addon_object = None
            return
        else:
            # AddonViewSet.get_lookup_field() expects a string.
            self.kwargs['addon_pk'] = force_str(self.kwargs['addon_pk'])
        # When loading the add-on, pass a specific permission class - the
        # default from AddonViewSet is too restrictive, we are not modifying
        # the add-on itself so we don't need all the permission checks it does.
        return super(RatingViewSet,
                     self).get_addon_object(permission_classes=[AllowIfPublic])

    def should_include_flags(self):
        if not hasattr(self, '_should_include_flags'):
            request = self.request
            self._should_include_flags = (
                'show_flags_for' in request.GET
                and not is_gate_active(request, 'del-ratings-flags'))
            if self._should_include_flags:
                # Check the parameter was sent correctly
                try:
                    show_flags_for = serializers.IntegerField(
                    ).to_internal_value(request.GET['show_flags_for'])
                    if show_flags_for != request.user.pk:
                        raise serializers.ValidationError
                except serializers.ValidationError:
                    raise ParseError(
                        'show_flags_for parameter value should be equal to '
                        'the user id of the authenticated user')
        return self._should_include_flags

    def check_permissions(self, request):
        """Perform permission checks.

        The regular DRF permissions checks are made, but also, before that, if
        an addon was requested, verify that it exists, is public and listed,
        through AllowIfPublic permission, that get_addon_object() uses."""
        self.get_addon_object()

        # Proceed with the regular permission checks.
        return super(RatingViewSet, self).check_permissions(request)

    def get_serializer(self, *args, **kwargs):
        if self.action in ('partial_update', 'update'):
            instance = args[0]
            if instance.reply_to is not None:
                self.rating_object = instance.reply_to
                self.serializer_class = self.reply_serializer_class
        return super(RatingViewSet, self).get_serializer(*args, **kwargs)

    def filter_queryset(self, qs):
        if self.action == 'list':
            addon_identifier = self.request.GET.get('addon')
            user_identifier = self.request.GET.get('user')
            version_identifier = self.request.GET.get('version')
            score_filter = (self.request.GET.get('score') if is_gate_active(
                self.request, 'ratings-score-filter') else None)
            exclude_ratings = self.request.GET.get('exclude_ratings')
            if addon_identifier:
                qs = qs.filter(addon=self.get_addon_object())
            if user_identifier:
                try:
                    user_identifier = int(user_identifier)
                except ValueError:
                    raise ParseError('user parameter should be an integer.')
                qs = qs.filter(user=user_identifier)
            if version_identifier:
                try:
                    version_identifier = int(version_identifier)
                except ValueError:
                    raise ParseError('version parameter should be an integer.')
                qs = qs.filter(version=version_identifier)
            elif addon_identifier:
                # When filtering on addon but not on version, only return the
                # latest rating posted by each user.
                qs = qs.filter(is_latest=True)
            if not addon_identifier and not user_identifier:
                # Don't allow listing ratings without filtering by add-on or
                # user.
                raise ParseError('Need an addon or user parameter')
            if user_identifier and addon_identifier and version_identifier:
                # When user, addon and version identifiers are set, we are
                # effectively only looking for one or zero objects. Fake
                # pagination in that case, avoiding all count() calls and
                # therefore related cache-machine invalidation issues. Needed
                # because the frontend wants to call this before and after
                # having posted a new rating, and needs accurate results.
                self.pagination_class = OneOrZeroPageNumberPagination
            if score_filter:
                try:
                    scores = [int(score) for score in score_filter.split(',')]
                except ValueError:
                    raise ParseError(
                        'score parameter should be an integer or a list of '
                        'integers (separated by a comma).')
                qs = qs.filter(rating__in=scores)
            if exclude_ratings:
                try:
                    exclude_ratings = [
                        int(rating) for rating in exclude_ratings.split(',')
                    ]
                except ValueError:
                    raise ParseError('exclude_ratings parameter should be an '
                                     'integer or a list of integers '
                                     '(separated by a comma).')
                qs = qs.exclude(pk__in=exclude_ratings)
        return super(RatingViewSet, self).filter_queryset(qs)

    def get_paginated_response(self, data):
        request = self.request
        extra_data = {}
        if 'show_grouped_ratings' in request.GET:
            try:
                show_grouped_ratings = serializers.BooleanField(
                ).to_internal_value(request.GET['show_grouped_ratings'])
            except serializers.ValidationError:
                raise ParseError(
                    'show_grouped_ratings parameter should be a boolean')
            if show_grouped_ratings and self.get_addon_object():
                extra_data['grouped_ratings'] = dict(
                    GroupedRating.get(self.addon_object.id))
        if 'show_permissions_for' in request.GET and is_gate_active(
                self.request, 'ratings-can_reply'):
            if 'addon' not in request.GET:
                raise ParseError(
                    'show_permissions_for parameter is only valid if the '
                    'addon parameter is also present')
            try:
                show_permissions_for = serializers.IntegerField(
                ).to_internal_value(request.GET['show_permissions_for'])
                if show_permissions_for != request.user.pk:
                    raise serializers.ValidationError
            except serializers.ValidationError:
                raise ParseError(
                    'show_permissions_for parameter value should be equal to '
                    'the user id of the authenticated user')
            extra_data[
                'can_reply'] = self.check_can_reply_permission_for_ratings_list(
                )
        # Call this here so the validation checks on the `show_flags_for` are
        # carried out even when there are no results to serialize.
        self.should_include_flags()
        response = super(RatingViewSet, self).get_paginated_response(data)
        if extra_data:
            response.data.update(extra_data)
        return response

    def check_can_reply_permission_for_ratings_list(self):
        """Check whether or not the current request contains an user that can
        reply to ratings we're about to return.

        Used to populate the `can_reply` property in ratings list, when an
        addon is passed."""
        # Clone the current viewset, but change permission_classes.
        viewset = self.__class__(**self.__dict__)
        viewset.permission_classes = self.reply_permission_classes

        # Create a fake rating with the addon object attached, to be passed to
        # check_object_permissions().
        dummy_rating = Rating(addon=self.get_addon_object())

        try:
            viewset.check_permissions(self.request)
            viewset.check_object_permissions(self.request, dummy_rating)
            return True
        except (PermissionDenied, NotAuthenticated):
            return False

    def get_queryset(self):
        requested = self.request.GET.get('filter', '').split(',')
        has_addons_edit = acl.action_allowed(self.request,
                                             amo.permissions.ADDONS_EDIT)

        # Add this as a property of the view, because we need to pass down the
        # information to the serializer to show/hide delete replies.
        if not hasattr(self, 'should_access_deleted_ratings'):
            self.should_access_deleted_ratings = (
                ('with_deleted' in requested or self.action != 'list')
                and self.request.user.is_authenticated and has_addons_edit)

        should_access_only_top_level_ratings = (self.action == 'list'
                                                and self.get_addon_object())

        if self.should_access_deleted_ratings:
            # For admins or add-on authors replying. When listing, we include
            # deleted ratings but still filter out out replies, because they'll
            # be in the serializer anyway. For other actions, we simply remove
            # any filtering, allowing them to access any rating out of the box
            # with no extra parameter needed.
            if self.action == 'list':
                queryset = Rating.unfiltered.filter(reply_to__isnull=True)
            else:
                queryset = Rating.unfiltered.all()
        elif should_access_only_top_level_ratings:
            # When listing add-on ratings, exclude replies, they'll be
            # included during serialization as children of the relevant
            # ratings instead.
            queryset = Rating.without_replies.all()
        else:
            queryset = Rating.objects.all()

        # Filter out empty ratings if specified.
        # Should the users own empty ratings be filtered back in?
        if 'with_yours' in requested and self.request.user.is_authenticated:
            user_filter = Q(user=self.request.user.pk)
        else:
            user_filter = Q()
        # Apply the filter(s)
        if 'without_empty_body' in requested:
            queryset = queryset.filter(~Q(body=None) | user_filter)

        # The serializer needs reply, version and user. We don't need much
        # for version and user, so we can make joins with select_related(),
        # but for replies additional queries will be made for translations
        # anyway so we're better off using prefetch_related() to make a
        # separate query to fetch them all.
        queryset = queryset.select_related('version', 'user')
        replies_qs = Rating.unfiltered.select_related('user')
        return queryset.prefetch_related(Prefetch('reply',
                                                  queryset=replies_qs))

    @action(
        detail=True,
        methods=['post'],
        permission_classes=reply_permission_classes,
        serializer_class=reply_serializer_class,
        throttle_classes=[RatingReplyThrottle],
    )
    def reply(self, *args, **kwargs):
        # A reply is just like a regular post, except that we set the reply
        # FK to the current rating object and only allow add-on authors/admins.
        # Call get_object() to trigger 404 if it does not exist.
        self.rating_object = self.get_object()
        self.set_addon_object_from_rating(self.rating_object)
        if Rating.unfiltered.filter(reply_to=self.rating_object).exists():
            # A reply already exists, just edit it.
            # We set should_access_deleted_ratings so that it works even if
            # the reply has been deleted.
            self.kwargs['pk'] = kwargs['pk'] = self.rating_object.reply.pk
            self.should_access_deleted_ratings = True
            return self.partial_update(*args, **kwargs)
        return self.create(*args, **kwargs)

    @action(
        detail=True,
        methods=['post'],
        permission_classes=flag_permission_classes,
        throttle_classes=[],
    )
    def flag(self, request, *args, **kwargs):
        # We load the add-on object from the rating to trigger permission
        # checks.
        self.rating_object = self.get_object()
        self.set_addon_object_from_rating(self.rating_object)

        try:
            flag_instance = RatingFlag.objects.get(rating=self.rating_object,
                                                   user=self.request.user)
        except RatingFlag.DoesNotExist:
            flag_instance = None
        if flag_instance is None:
            serializer = RatingFlagSerializer(
                data=request.data, context=self.get_serializer_context())
        else:
            serializer = RatingFlagSerializer(
                flag_instance,
                data=request.data,
                partial=False,
                context=self.get_serializer_context(),
            )
        serializer.is_valid(raise_exception=True)
        serializer.save()
        headers = self.get_success_headers(serializer.data)
        return Response(serializer.data,
                        status=HTTP_202_ACCEPTED,
                        headers=headers)

    def perform_destroy(self, instance):
        instance.delete(user_responsible=self.request.user)
Exemplo n.º 7
0
class AccountViewSet(RetrieveModelMixin, UpdateModelMixin, DestroyModelMixin,
                     GenericViewSet):
    permission_classes = [
        ByHttpMethod({
            'get': AllowAny,
            'head': AllowAny,
            'options': AllowAny,  # Needed for CORS.
            # To edit a profile it has to yours, or be an admin.
            'patch': AnyOf(AllowSelf, GroupPermission(
                amo.permissions.USERS_EDIT)),
            'delete': AnyOf(AllowSelf, GroupPermission(
                amo.permissions.USERS_EDIT)),
        }),
    ]
    # Periods are not allowed in username, but we still have some in the
    # database so relax the lookup regexp to allow them to load their profile.
    lookup_value_regex = '[^/]+'

    def get_queryset(self):
        return UserProfile.objects.exclude(deleted=True).all()

    def get_object(self):
        if hasattr(self, 'instance'):
            return self.instance
        identifier = self.kwargs.get('pk')
        self.lookup_field = self.get_lookup_field(identifier)
        self.kwargs[self.lookup_field] = identifier
        self.instance = super(AccountViewSet, self).get_object()
        # action won't exist for other classes that are using this ViewSet.
        can_view_instance = (
            not getattr(self, 'action', None) or
            self.self_view or
            self.admin_viewing or
            self.instance.is_public)
        if can_view_instance:
            return self.instance
        else:
            raise Http404

    def get_lookup_field(self, identifier):
        lookup_field = 'pk'
        if identifier and not identifier.isdigit():
            # If the identifier contains anything other than a digit, it's
            # the username.
            lookup_field = 'username'
        return lookup_field

    @property
    def self_view(self):
        return (
            self.request.user.is_authenticated and
            self.get_object() == self.request.user)

    @property
    def admin_viewing(self):
        return acl.action_allowed_user(
            self.request.user, amo.permissions.USERS_EDIT)

    def get_serializer_class(self):
        if self.self_view or self.admin_viewing:
            return UserProfileSerializer
        else:
            return PublicUserProfileSerializer

    def perform_destroy(self, instance):
        if instance.is_developer:
            raise serializers.ValidationError(ugettext(
                u'Developers of add-ons or themes cannot delete their '
                u'account. You must delete all add-ons and themes linked to '
                u'this account, or transfer them to other users.'))
        return super(AccountViewSet, self).perform_destroy(instance)

    def destroy(self, request, *args, **kwargs):
        instance = self.get_object()
        self.perform_destroy(instance)
        response = Response(status=HTTP_204_NO_CONTENT)
        if instance == request.user:
            logout_user(request, response)
        return response

    @action(
        detail=True,
        methods=['delete'], permission_classes=[
            AnyOf(AllowSelf, GroupPermission(amo.permissions.USERS_EDIT))])
    def picture(self, request, pk=None):
        user = self.get_object()
        user.delete_picture()
        log.debug(u'User (%s) deleted photo' % user)
        return self.retrieve(request)
Exemplo n.º 8
0
class RatingViewSet(AddonChildMixin, ModelViewSet):
    serializer_class = RatingSerializer
    permission_classes = [
        ByHttpMethod({
            'get':
            AllowAny,
            'head':
            AllowAny,
            'options':
            AllowAny,  # Needed for CORS.
            # Deletion requires a specific permission check.
            'delete':
            CanDeleteRatingPermission,
            # To post a rating you just need to be authenticated.
            'post':
            IsAuthenticated,
            # To edit a rating you need to be the author or be an admin.
            'patch':
            AnyOf(AllowOwner, GroupPermission(amo.permissions.ADDONS_EDIT)),
            # Implementing PUT would be a little incoherent as we don't want to
            # allow users to change `version` but require it at creation time.
            # So only PATCH is allowed for editing.
        }),
    ]
    reply_permission_classes = [
        AnyOf(
            GroupPermission(amo.permissions.ADDONS_EDIT),
            AllowRelatedObjectPermissions('addon', [AllowAddonAuthor]),
        )
    ]
    reply_serializer_class = RatingSerializerReply
    flag_permission_classes = [AllowNotOwner]
    throttle_classes = (RatingUserThrottle, RatingIPThrottle)

    def set_addon_object_from_rating(self, rating):
        """Set addon object on the instance from a rating object."""
        # At this point it's likely we didn't have an addon in the request, so
        # if we went through get_addon_object() before it's going to be set
        # to None already. We delete the addon_object property cache and set
        # addon_pk in kwargs to force get_addon_object() to reset
        # self.addon_object.
        del self.addon_object
        self.kwargs['addon_pk'] = str(rating.addon.pk)
        return self.get_addon_object()

    def get_addon_object(self):
        """Return addon object associated with the request, or None if not
        relevant.

        Will also fire permission checks on the addon object when it's loaded.
        """
        if hasattr(self, 'addon_object'):
            return self.addon_object

        if 'addon_pk' not in self.kwargs:
            self.kwargs['addon_pk'] = self.request.data.get(
                'addon') or self.request.GET.get('addon')
        if not self.kwargs['addon_pk']:
            # If we don't have an addon object, set it as None on the instance
            # and return immediately, that's fine.
            self.addon_object = None
            return
        else:
            # AddonViewSet.get_lookup_field() expects a string.
            self.kwargs['addon_pk'] = force_str(self.kwargs['addon_pk'])
        # When loading the add-on, pass a specific permission class - the
        # default from AddonViewSet is too restrictive, we are not modifying
        # the add-on itself so we don't need all the permission checks it does.
        return super(RatingViewSet,
                     self).get_addon_object(permission_classes=[AllowIfPublic])

    def should_include_flags(self):
        if not hasattr(self, '_should_include_flags'):
            request = self.request
            self._should_include_flags = (
                'show_flags_for' in request.GET
                and not is_gate_active(request, 'del-ratings-flags'))
            if self._should_include_flags:
                # Check the parameter was sent correctly
                try:
                    show_flags_for = serializers.IntegerField(
                    ).to_internal_value(request.GET['show_flags_for'])
                    if show_flags_for != request.user.pk:
                        raise serializers.ValidationError
                except serializers.ValidationError:
                    raise ParseError(
                        'show_flags_for parameter value should be equal to '
                        'the user id of the authenticated user')
        return self._should_include_flags

    def check_permissions(self, request):
        """Perform permission checks.

        The regular DRF permissions checks are made, but also, before that, if
        an addon was requested, verify that it exists, is public and listed,
        through AllowIfPublic permission, that get_addon_object() uses."""
        self.get_addon_object()

        # Proceed with the regular permission checks.
        return super(RatingViewSet, self).check_permissions(request)

    def get_serializer(self, *args, **kwargs):
        if self.action in ('partial_update', 'update'):
            instance = args[0]
            if instance.reply_to is not None:
                self.rating_object = instance.reply_to
                self.serializer_class = self.reply_serializer_class
        return super(RatingViewSet, self).get_serializer(*args, **kwargs)

    def filter_queryset(self, qs):
        if self.action == 'list':
            addon_identifier = self.request.GET.get('addon')
            user_identifier = self.request.GET.get('user')
            version_identifier = self.request.GET.get('version')
            score_filter = (self.request.GET.get('score') if is_gate_active(
                self.request, 'ratings-score-filter') else None)
            exclude_ratings = self.request.GET.get('exclude_ratings')
            if addon_identifier:
                qs = qs.filter(addon=self.get_addon_object())
            if user_identifier:
                try:
                    user_identifier = int(user_identifier)
                except ValueError:
                    raise ParseError('user parameter should be an integer.')
                qs = qs.filter(user=user_identifier)
            if version_identifier:
                try:
                    version_identifier = int(version_identifier)
                except ValueError:
                    raise ParseError('version parameter should be an integer.')
                qs = qs.filter(version=version_identifier)
            elif addon_identifier:
                # When filtering on addon but not on version, only return the
                # latest rating posted by each user.
                qs = qs.filter(is_latest=True)
            if not addon_identifier and not user_identifier:
                # Don't allow listing ratings without filtering by add-on or
                # user.
                raise ParseError('Need an addon or user parameter')
            if user_identifier and addon_identifier and version_identifier:
                # When user, addon and version identifiers are set, we are
                # effectively only looking for one or zero objects. Fake
                # pagination in that case, avoiding all count() calls and
                # therefore related cache-machine invalidation issues. Needed
                # because the frontend wants to call this before and after
                # having posted a new rating, and needs accurate results.
                self.pagination_class = OneOrZeroPageNumberPagination
            if score_filter:
                try:
                    scores = [int(score) for score in score_filter.split(',')]
                except ValueError:
                    raise ParseError(
                        'score parameter should be an integer or a list of '
                        'integers (separated by a comma).')
                qs = qs.filter(rating__in=scores)
            if exclude_ratings:
                try:
                    exclude_ratings = [
                        int(rating) for rating in exclude_ratings.split(',')
                    ]
                except ValueError:
                    raise ParseError('exclude_ratings parameter should be an '
                                     'integer or a list of integers '
                                     '(separated by a comma).')
                qs = qs.exclude(pk__in=exclude_ratings)
        return super(RatingViewSet, self).filter_queryset(qs)

    def get_paginated_response(self, data):
        request = self.request
        extra_data = {}
        if grouped_rating := get_grouped_ratings(request,
                                                 self.get_addon_object()):
            extra_data['grouped_ratings'] = grouped_rating
        if 'show_permissions_for' in request.GET and is_gate_active(
                self.request, 'ratings-can_reply'):
            if 'addon' not in request.GET:
                raise ParseError(
                    'show_permissions_for parameter is only valid if the '
                    'addon parameter is also present')
            try:
                show_permissions_for = serializers.IntegerField(
                ).to_internal_value(request.GET['show_permissions_for'])
                if show_permissions_for != request.user.pk:
                    raise serializers.ValidationError
            except serializers.ValidationError:
                raise ParseError(
                    'show_permissions_for parameter value should be equal to '
                    'the user id of the authenticated user')
            extra_data[
                'can_reply'] = self.check_can_reply_permission_for_ratings_list(
                )
        # Call this here so the validation checks on the `show_flags_for` are
        # carried out even when there are no results to serialize.
        self.should_include_flags()
        response = super(RatingViewSet, self).get_paginated_response(data)
        if extra_data:
            response.data.update(extra_data)
        return response
Exemplo n.º 9
0
class AddonViewSet(RetrieveModelMixin, GenericViewSet):
    permission_classes = [
        AnyOf(AllowReadOnlyIfPublic, AllowAddonAuthor, AllowReviewer,
              AllowReviewerUnlisted),
    ]
    serializer_class = AddonSerializer
    serializer_class_with_unlisted_data = AddonSerializerWithUnlistedData
    lookup_value_regex = '[^/]+'  # Allow '.' for email-like guids.

    def get_queryset(self):
        """Return queryset to be used for the view. We implement our own that
        does not depend on self.queryset to avoid cache-machine caching the
        queryset too agressively (mozilla/addons-frontend#2497)."""
        # Special case: admins - and only admins - can see deleted add-ons.
        # This is handled outside a permission class because that condition
        # would pollute all other classes otherwise.
        if (self.request.user.is_authenticated() and acl.action_allowed(
                self.request, amo.permissions.ADDONS_VIEW_DELETED)):
            return Addon.unfiltered.all()
        # Permission classes disallow access to non-public/unlisted add-ons
        # unless logged in as a reviewer/addon owner/admin, so we don't have to
        # filter the base queryset here.
        return Addon.objects.all()

    def get_serializer_class(self):
        # Override serializer to use serializer_class_with_unlisted_data if
        # we are allowed to access unlisted data.
        obj = getattr(self, 'instance')
        request = self.request
        if (acl.check_unlisted_addons_reviewer(request)
                or (obj and request.user.is_authenticated()
                    and obj.authors.filter(pk=request.user.pk).exists())):
            return self.serializer_class_with_unlisted_data
        return self.serializer_class

    def get_lookup_field(self, identifier):
        lookup_field = 'pk'
        if identifier and not identifier.isdigit():
            # If the identifier contains anything other than a digit, it's
            # either a slug or a guid. guids need to contain either {} or @,
            # which are invalid in a slug.
            if amo.ADDON_GUID_PATTERN.match(identifier):
                lookup_field = 'guid'
            else:
                lookup_field = 'slug'
        return lookup_field

    def get_object(self):
        identifier = self.kwargs.get('pk')
        self.lookup_field = self.get_lookup_field(identifier)
        self.kwargs[self.lookup_field] = identifier
        self.instance = super(AddonViewSet, self).get_object()
        return self.instance

    def check_object_permissions(self, request, obj):
        """
        Check if the request should be permitted for a given object.
        Raises an appropriate exception if the request is not permitted.

        Calls DRF implementation, but adds `is_disabled_by_developer` to the
        exception being thrown so that clients can tell the difference between
        a 401/403 returned because an add-on has been disabled by their
        developer or something else.
        """
        try:
            super(AddonViewSet, self).check_object_permissions(request, obj)
        except exceptions.APIException as exc:
            exc.detail = {
                'detail': exc.detail,
                'is_disabled_by_developer': obj.disabled_by_user,
                'is_disabled_by_mozilla': obj.status == amo.STATUS_DISABLED,
            }
            raise exc

    @detail_route()
    def feature_compatibility(self, request, pk=None):
        obj = self.get_object()
        serializer = AddonFeatureCompatibilitySerializer(
            obj.feature_compatibility, context=self.get_serializer_context())
        return Response(serializer.data)

    @detail_route()
    def eula_policy(self, request, pk=None):
        obj = self.get_object()
        serializer = AddonEulaPolicySerializer(
            obj, context=self.get_serializer_context())
        return Response(serializer.data)
Exemplo n.º 10
0
class AddonViewSet(RetrieveModelMixin, GenericViewSet):
    permission_classes = [
        AnyOf(AllowReadOnlyIfPublic, AllowAddonAuthor, AllowReviewer,
              AllowReviewerUnlisted),
    ]
    serializer_class = AddonSerializer
    serializer_class_with_unlisted_data = AddonSerializerWithUnlistedData
    # Permission classes disallow access to non-public/unlisted add-ons unless
    # logged in as a reviewer/addon owner/admin, so we don't have to filter the
    # base queryset here.
    queryset = Addon.objects.all()
    lookup_value_regex = '[^/]+'  # Allow '.' for email-like guids.

    def get_queryset(self):
        # Special case: admins - and only admins - can see deleted add-ons.
        # This is handled outside a permission class because that condition
        # would pollute all other classes otherwise.
        if self.request.user.is_authenticated() and self.request.user.is_staff:
            return Addon.unfiltered.all()
        return super(AddonViewSet, self).get_queryset()

    def get_serializer_class(self):
        # Override serializer to use serializer_class_with_unlisted_data if
        # we are allowed to access unlisted data.
        obj = getattr(self, 'instance')
        request = self.request
        if (acl.check_unlisted_addons_reviewer(request)
                or (obj and request.user.is_authenticated()
                    and obj.authors.filter(pk=request.user.pk).exists())):
            return self.serializer_class_with_unlisted_data
        return self.serializer_class

    def get_lookup_field(self, identifier):
        lookup_field = 'pk'
        if identifier and not identifier.isdigit():
            # If the identifier contains anything other than a digit, it's
            # either a slug or a guid. guids need to contain either {} or @,
            # which are invalid in a slug.
            if amo.ADDON_GUID_PATTERN.match(identifier):
                lookup_field = 'guid'
            else:
                lookup_field = 'slug'
        return lookup_field

    def get_object(self):
        identifier = self.kwargs.get('pk')
        self.lookup_field = self.get_lookup_field(identifier)
        self.kwargs[self.lookup_field] = identifier
        self.instance = super(AddonViewSet, self).get_object()
        return self.instance

    @detail_route()
    def feature_compatibility(self, request, pk=None):
        obj = self.get_object()
        serializer = AddonFeatureCompatibilitySerializer(
            obj.feature_compatibility, context=self.get_serializer_context())
        return Response(serializer.data)

    @detail_route()
    def eula_policy(self, request, pk=None):
        obj = self.get_object()
        serializer = AddonEulaPolicySerializer(
            obj, context=self.get_serializer_context())
        return Response(serializer.data)
Exemplo n.º 11
0
class ReviewViewSet(AddonChildMixin, ModelViewSet):
    serializer_class = ReviewSerializer
    permission_classes = [
        ByHttpMethod({
            'get':
            AllowAny,
            'head':
            AllowAny,
            'options':
            AllowAny,  # Needed for CORS.

            # Deletion requires a specific permission check.
            'delete':
            CanDeleteReviewPermission,

            # To post a review you just need to be authenticated.
            'post':
            IsAuthenticated,

            # To edit a review you need to be the author or be an admin.
            'patch':
            AnyOf(AllowOwner, GroupPermission('Addons', 'Edit')),

            # Implementing PUT would be a little incoherent as we don't want to
            # allow users to change `version` but require it at creation time.
            # So only PATCH is allowed for editing.
        }),
    ]
    reply_permission_classes = [
        AnyOf(
            GroupPermission('Addons', 'Edit'),
            AllowRelatedObjectPermissions('addon', [AllowAddonAuthor]),
        )
    ]
    reply_serializer_class = ReviewSerializerReply

    queryset = Review.objects.all()

    def get_addon_object(self):
        # When loading the add-on, pass a specific permission class - the
        # default from AddonViewSet is too restrictive, we are not modifying
        # the add-on itself so we don't need all the permission checks it does.
        return super(ReviewViewSet, self).get_addon_object(
            permission_classes=[AllowIfReviewedAndListed])

    def check_permissions(self, request):
        if 'addon_pk' in self.kwargs:
            # In addition to the regular permission checks that are made, we
            # need to verify that the add-on exists, is public and listed. Just
            # loading the addon should be enough to do that, since
            # AddonChildMixin implementation calls AddonViewSet.get_object().
            self.get_addon_object()

        # Proceed with the regular permission checks.
        return super(ReviewViewSet, self).check_permissions(request)

    def filter_queryset(self, qs):
        if self.action == 'list':
            if 'addon_pk' in self.kwargs:
                qs = qs.filter(addon=self.get_addon_object())
            elif 'account_pk' in self.kwargs:
                qs = qs.filter(user=self.kwargs.get('account_pk'))
            else:
                # Don't allow listing reviews without filtering by add-on or
                # user.
                raise ParseError('Need an addon or user identifier')
        return qs

    def get_queryset(self):
        if self.action == 'list':
            if self.kwargs.get('addon_pk'):
                # When listing add-on reviews, exclude replies, they'll be
                # included during serialization as children of the relevant
                # reviews instead.
                self.queryset = Review.without_replies.all()

        qs = super(ReviewViewSet, self).get_queryset()
        # The serializer needs user, reply and version, so use
        # prefetch_related() to avoid extra queries (avoid select_related() as
        # we need crazy joins already). Also avoid loading addon since we don't
        # need it, we already loaded it for permission checks through the pk
        # specified in the URL.
        return qs.defer('addon').prefetch_related('reply', 'user', 'version')

    @detail_route(methods=['post'],
                  permission_classes=reply_permission_classes,
                  serializer_class=reply_serializer_class)
    def reply(self, *args, **kwargs):
        # A reply is just like a regular post, except that we set the reply
        # FK to the current review object and only allow add-on authors/admins.
        # Call get_object() to trigger 404 if it does not exist.
        self.review_object = self.get_object()
        return self.create(*args, **kwargs)
Exemplo n.º 12
0
class AccountViewSet(RetrieveModelMixin, UpdateModelMixin, DestroyModelMixin,
                     GenericViewSet):
    permission_classes = [
        ByHttpMethod({
            'get':
            AllowAny,
            'head':
            AllowAny,
            'options':
            AllowAny,  # Needed for CORS.
            # To edit a profile it has to yours, or be an admin.
            'patch':
            AnyOf(AllowSelf, GroupPermission(amo.permissions.USERS_EDIT)),
            'delete':
            AnyOf(AllowSelf, GroupPermission(amo.permissions.USERS_EDIT)),
        }),
    ]

    def get_queryset(self):
        return UserProfile.objects.all()

    def get_object(self):
        if hasattr(self, 'instance'):
            return self.instance
        identifier = self.kwargs.get('pk')
        self.lookup_field = self.get_lookup_field(identifier)
        self.kwargs[self.lookup_field] = identifier
        self.instance = super(AccountViewSet, self).get_object()
        # action won't exist for other classes that are using this ViewSet.
        can_view_instance = (not getattr(self, 'action', None)
                             or self.self_view or self.admin_viewing
                             or self.instance.is_public)
        if can_view_instance:
            return self.instance
        else:
            raise Http404

    def get_lookup_field(self, identifier):
        lookup_field = 'pk'
        if identifier and not identifier.isdigit():
            # If the identifier contains anything other than a digit, it's
            # the username.
            lookup_field = 'username'
        return lookup_field

    @property
    def self_view(self):
        return (self.request.user.is_authenticated()
                and self.get_object() == self.request.user)

    @property
    def admin_viewing(self):
        return acl.action_allowed_user(self.request.user,
                                       amo.permissions.USERS_EDIT)

    def get_serializer_class(self):
        if self.self_view or self.admin_viewing:
            return UserProfileSerializer
        else:
            return PublicUserProfileSerializer

    def perform_destroy(self, instance):
        if instance.is_developer:
            raise serializers.ValidationError(
                ugettext(
                    u'Developers of add-ons or themes cannot delete their '
                    u'account. You must delete all add-ons and themes linked to '
                    u'this account, or transfer them to other users.'))
        return super(AccountViewSet, self).perform_destroy(instance)

    @detail_route(methods=['delete'],
                  permission_classes=[
                      AnyOf(AllowSelf,
                            GroupPermission(amo.permissions.USERS_EDIT))
                  ])
    def picture(self, request, pk=None):
        user = self.get_object()
        user.update(picture_type='')
        log.debug(u'User (%s) deleted photo' % user)
        tasks.delete_photo.delay(user.picture_path)
        return self.retrieve(request)
Exemplo n.º 13
0
class AddonViewSet(RetrieveModelMixin, GenericViewSet):
    permission_classes = [
        AnyOf(AllowReadOnlyIfReviewed, AllowAddonAuthor,
              AllowReviewer, AllowReviewerUnlisted),
    ]
    serializer_class = AddonSerializer
    serializer_class_with_unlisted_data = AddonSerializerWithUnlistedData
    addon_id_pattern = re.compile(
        # Match {uuid} or [email protected] ("something" being optional)
        # guids. Copied from mozilla-central XPIProvider.jsm.
        r'^(\{[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\}'
        r'|[a-z0-9-\._]*\@[a-z0-9-\._]+)$', re.IGNORECASE)
    # Permission classes disallow access to non-public/unlisted add-ons unless
    # logged in as a reviewer/addon owner/admin, so the with_unlisted queryset
    # is fine here.
    queryset = Addon.with_unlisted.all()
    lookup_value_regex = '[^/]+'  # Allow '.' for email-like guids.

    def get_queryset(self):
        # Special case: admins - and only admins - can see deleted add-ons.
        # This is handled outside a permission class because that condition
        # would pollute all other classes otherwise.
        if self.request.user.is_authenticated() and self.request.user.is_staff:
            return Addon.unfiltered.all()
        return super(AddonViewSet, self).get_queryset()

    def get_serializer_class(self):
        # Override serializer to use serializer_class_with_unlisted_data if
        # we are allowed to access unlisted data.
        obj = getattr(self, 'instance')
        request = self.request
        if (acl.check_unlisted_addons_reviewer(request) or
                (obj and request.user.is_authenticated() and
                 obj.authors.filter(pk=request.user.pk).exists())):
            return self.serializer_class_with_unlisted_data
        return self.serializer_class

    def get_lookup_field(self, identifier):
        lookup_field = 'pk'
        if identifier and not identifier.isdigit():
            # If the identifier contains anything other than a digit, it's
            # either a slug or a guid. guids need to contain either {} or @,
            # which are invalid in a slug.
            if self.addon_id_pattern.match(identifier):
                lookup_field = 'guid'
            else:
                lookup_field = 'slug'
        return lookup_field

    def get_object(self):
        identifier = self.kwargs.get('pk')
        self.lookup_field = self.get_lookup_field(identifier)
        self.kwargs[self.lookup_field] = identifier
        self.instance = super(AddonViewSet, self).get_object()
        return self.instance

    @detail_route()
    def feature_compatibility(self, request, pk=None):
        obj = self.get_object()
        serializer = AddonFeatureCompatibilitySerializer(
            obj.feature_compatibility,
            context=self.get_serializer_context())
        return Response(serializer.data)

    @detail_route()
    def eula_policy(self, request, pk=None):
        obj = self.get_object()
        serializer = AddonEulaPolicySerializer(
            obj, context=self.get_serializer_context())
        return Response(serializer.data)
Exemplo n.º 14
0
class ReviewViewSet(AddonChildMixin, ModelViewSet):
    serializer_class = ReviewSerializer
    permission_classes = [
        ByHttpMethod({
            'get':
            AllowAny,
            'head':
            AllowAny,
            'options':
            AllowAny,  # Needed for CORS.

            # Deletion requires a specific permission check.
            'delete':
            CanDeleteReviewPermission,

            # To post a review you just need to be authenticated.
            'post':
            IsAuthenticated,

            # To edit a review you need to be the author or be an admin.
            'patch':
            AnyOf(AllowOwner, GroupPermission('Addons', 'Edit')),

            # Implementing PUT would be a little incoherent as we don't want to
            # allow users to change `version` but require it at creation time.
            # So only PATCH is allowed for editing.
        }),
    ]
    reply_permission_classes = [
        AnyOf(
            GroupPermission('Addons', 'Edit'),
            AllowRelatedObjectPermissions('addon', [AllowAddonAuthor]),
        )
    ]
    reply_serializer_class = ReviewSerializerReply

    queryset = Review.objects.all()

    def get_addon_object(self):
        if 'addon_pk' not in self.kwargs:
            return None
        # When loading the add-on, pass a specific permission class - the
        # default from AddonViewSet is too restrictive, we are not modifying
        # the add-on itself so we don't need all the permission checks it does.
        return super(
            ReviewViewSet,
            self).get_addon_object(permission_classes=[AllowIfReviewed])

    def check_permissions(self, request):
        if 'addon_pk' in self.kwargs:
            # In addition to the regular permission checks that are made, we
            # need to verify that the add-on exists, is public and listed. Just
            # loading the addon should be enough to do that, since
            # AddonChildMixin implementation calls AddonViewSet.get_object().
            self.get_addon_object()

        # Proceed with the regular permission checks.
        return super(ReviewViewSet, self).check_permissions(request)

    def get_serializer(self, *args, **kwargs):
        if self.action in ('partial_update', 'update'):
            instance = args[0]
            if instance.reply_to is not None:
                self.review_object = instance.reply_to
                self.serializer_class = self.reply_serializer_class
        return super(ReviewViewSet, self).get_serializer(*args, **kwargs)

    def filter_queryset(self, qs):
        if self.action == 'list':
            if 'addon_pk' in self.kwargs:
                qs = qs.filter(is_latest=True, addon=self.get_addon_object())
            elif 'account_pk' in self.kwargs:
                qs = qs.filter(user=self.kwargs.get('account_pk'))
            else:
                # Don't allow listing reviews without filtering by add-on or
                # user.
                raise ParseError('Need an addon or user identifier')
        return qs

    def get_paginated_response(self, data):
        response = super(ReviewViewSet, self).get_paginated_response(data)
        show_grouped_ratings = self.request.GET.get('show_grouped_ratings')
        if 'addon_pk' in self.kwargs and show_grouped_ratings:
            response.data['grouped_ratings'] = dict(
                GroupedRating.get(self.addon_object.id))
        return response

    def get_queryset(self):
        requested = self.request.GET.get('filter')

        # Add this as a property of the view, because we need to pass down the
        # information to the serializer to show/hide delete replies.
        if not hasattr(self, 'should_access_deleted_reviews'):
            self.should_access_deleted_reviews = (
                (requested == 'with_deleted' or self.action != 'list')
                and self.request.user.is_authenticated()
                and acl.action_allowed(self.request, 'Addons', 'Edit'))

        should_access_only_top_level_reviews = (self.action == 'list' and
                                                self.kwargs.get('addon_pk'))

        if self.should_access_deleted_reviews:
            # For admins or add-on authors replying. When listing, we include
            # deleted reviews but still filter out out replies, because they'll
            # be in the serializer anyway. For other actions, we simply remove
            # any filtering, allowing them to access any review out of the box
            # with no extra parameter needed.
            if self.action == 'list':
                self.queryset = Review.unfiltered.filter(reply_to__isnull=True)
            else:
                self.queryset = Review.unfiltered.all()
        elif should_access_only_top_level_reviews:
            # When listing add-on reviews, exclude replies, they'll be
            # included during serialization as children of the relevant
            # reviews instead.
            self.queryset = Review.without_replies.all()

        qs = super(ReviewViewSet, self).get_queryset()
        # The serializer needs reply, version (only the "version" field) and
        # user. We don't need much for version and user, so we can make joins
        # with select_related(), but for replies additional queries will be
        # made for translations anyway so we're better off using
        # prefetch_related() to make a separate query to fetch them all.
        qs = qs.select_related('version__version', 'user')
        replies_qs = Review.unfiltered.select_related('user')
        return qs.prefetch_related(Prefetch('reply', queryset=replies_qs))

    @detail_route(methods=['post'],
                  permission_classes=reply_permission_classes,
                  serializer_class=reply_serializer_class)
    def reply(self, *args, **kwargs):
        # A reply is just like a regular post, except that we set the reply
        # FK to the current review object and only allow add-on authors/admins.
        # Call get_object() to trigger 404 if it does not exist.
        self.review_object = self.get_object()
        if Review.unfiltered.filter(reply_to=self.review_object).exists():
            # A reply already exists, just edit it.
            # We set should_access_deleted_reviews so that it works even if
            # the reply has been deleted.
            self.kwargs['pk'] = kwargs['pk'] = self.review_object.reply.pk
            self.should_access_deleted_reviews = True
            return self.partial_update(*args, **kwargs)
        return self.create(*args, **kwargs)

    @detail_route(methods=['post'])
    def flag(self, request, *args, **kwargs):
        # Re-use flag view since it's already returning json. We just need to
        # pass it the addon slug (passing it the PK would result in a redirect)
        # and make sure request.POST is set with whatever data was sent to the
        # DRF view.
        addon = self.get_addon_object()
        request._request.POST = request.data
        request = request._request
        response = flag(request, addon.slug, kwargs.get('pk'))
        if response.status_code == 200:
            response.content = ''
            response.status_code = 202
        return response