diff options
Diffstat (limited to 'app')
12 files changed, 224 insertions, 101 deletions
diff --git a/app/serializers/activity_pub/activity_serializer.rb b/app/serializers/activity_pub/activity_serializer.rb new file mode 100644 index 00000000000..71a1bfece6b --- /dev/null +++ b/app/serializers/activity_pub/activity_serializer.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +module ActivityPub + # Serializer for the `Activity` ActivityStreams model. + # Reference: https://www.w3.org/TR/activitystreams-core/#activities + class ActivitySerializer < ObjectSerializer + MissingActorError = Class.new(StandardError) + MissingObjectError = Class.new(StandardError) + IntransitiveWithObjectError = Class.new(StandardError) + + private + + def validate_response(serialized, opts) + response = super(serialized, opts) + + unless response[:actor].present? + raise MissingActorError, "The serializer does not provide the mandatory 'actor' field." + end + + if opts[:intransitive] && response[:object].present? + raise IntransitiveWithObjectError, <<~ERROR + The serializer does provide both the 'object' field and the :intransitive option. + Intransitive activities are meant precisely for when no object is available. + Please remove either of those. + See https://www.w3.org/TR/activitystreams-vocabulary/#activity-types + ERROR + end + + unless opts[:intransitive] || response[:object].present? + raise MissingObjectError, <<~ERROR + The serializer does not provide the mandatory 'object' field. + Pass the :intransitive option to #represent if this is an intransitive activity. + See https://www.w3.org/TR/activitystreams-vocabulary/#activity-types + ERROR + end + + response + end + end +end diff --git a/app/serializers/activity_pub/activity_streams_serializer.rb b/app/serializers/activity_pub/activity_streams_serializer.rb deleted file mode 100644 index 39caa4a6d10..00000000000 --- a/app/serializers/activity_pub/activity_streams_serializer.rb +++ /dev/null @@ -1,90 +0,0 @@ -# frozen_string_literal: true - -module ActivityPub - class ActivityStreamsSerializer < ::BaseSerializer - MissingIdentifierError = Class.new(StandardError) - MissingTypeError = Class.new(StandardError) - MissingOutboxError = Class.new(StandardError) - - alias_method :base_represent, :represent - - def represent(resource, opts = {}, entity_class = nil) - response = if respond_to?(:paginated?) && paginated? - represent_paginated(resource, opts, entity_class) - else - represent_whole(resource, opts, entity_class) - end - - validate_response(HashWithIndifferentAccess.new(response)) - end - - private - - def validate_response(response) - unless response[:id].present? - raise MissingIdentifierError, "The serializer does not provide the mandatory 'id' field." - end - - unless response[:type].present? - raise MissingTypeError, "The serializer does not provide the mandatory 'type' field." - end - - response - end - - def represent_whole(resource, opts, entity_class) - raise MissingOutboxError, 'Please provide an :outbox option for this actor' unless opts[:outbox].present? - - serialized = base_represent(resource, opts, entity_class) - - { - :@context => "https://www.w3.org/ns/activitystreams", - inbox: opts[:inbox], - outbox: opts[:outbox] - }.merge(serialized) - end - - def represent_paginated(resources, opts, entity_class) - if paginator.params['page'].present? - represent_page(resources, resources.current_page, opts, entity_class) - else - represent_pagination_index(resources) - end - end - - def represent_page(resources, page, opts, entity_class) - opts[:page] = page - serialized = base_represent(resources, opts, entity_class) - - { - :@context => 'https://www.w3.org/ns/activitystreams', - type: 'OrderedCollectionPage', - id: collection_url(page), - prev: page > 1 ? collection_url(page - 1) : nil, - next: page < resources.total_pages ? collection_url(page + 1) : nil, - partOf: collection_url, - orderedItems: serialized - } - end - - def represent_pagination_index(resources) - { - :@context => 'https://www.w3.org/ns/activitystreams', - type: 'OrderedCollection', - id: collection_url, - totalItems: resources.total_count, - first: collection_url(1), - last: collection_url(resources.total_pages) - } - end - - def collection_url(page = nil) - uri = URI.parse(paginator.request.url) - uri.query ||= "" - parts = uri.query.split('&').reject { |part| part =~ /^page=/ } - parts << "page=#{page}" if page - uri.query = parts.join('&') - uri.to_s.sub(/\?$/, '') - end - end -end diff --git a/app/serializers/activity_pub/actor_serializer.rb b/app/serializers/activity_pub/actor_serializer.rb new file mode 100644 index 00000000000..14ab43666ec --- /dev/null +++ b/app/serializers/activity_pub/actor_serializer.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +module ActivityPub + # Serializer for the `Actor` ActivityStreams model. + # Reference: https://www.w3.org/TR/activitystreams-core/#actors + class ActorSerializer < ObjectSerializer + MissingOutboxError = Class.new(StandardError) + + def represent(resource, opts = {}, entity_class = nil) + raise MissingInboxError, 'Please provide an :inbox option for this actor' unless opts[:inbox].present? + raise MissingOutboxError, 'Please provide an :outbox option for this actor' unless opts[:outbox].present? + + super + end + + private + + def validate_response(response, _opts) + unless response[:id].present? + raise MissingIdentifierError, "The serializer does not provide the mandatory 'id' field." + end + + unless response[:type].present? + raise MissingTypeError, "The serializer does not provide the mandatory 'type' field." + end + + response + end + + def wrap(serialized, opts) + parent_value = super(serialized, opts) + + { + inbox: opts[:inbox], + outbox: opts[:outbox] + }.merge(parent_value) + end + end +end diff --git a/app/serializers/activity_pub/collection_serializer.rb b/app/serializers/activity_pub/collection_serializer.rb new file mode 100644 index 00000000000..16c78eb1b7d --- /dev/null +++ b/app/serializers/activity_pub/collection_serializer.rb @@ -0,0 +1,68 @@ +# frozen_string_literal: true + +module ActivityPub + # Serializer for the `Collection` ActivityStreams model. + # Reference: https://www.w3.org/TR/activitystreams-core/#collections + class CollectionSerializer < ::BaseSerializer + include WithPagination + + NotPaginatedError = Class.new(StandardError) + + alias_method :base_represent, :represent + + def represent(resources, opts = {}) + unless respond_to?(:paginated?) && paginated? + raise NotPaginatedError, 'Pass #with_pagination to the serializer or use ActivityPub::ObjectSerializer instead' + end + + response = if paginator.params['page'].present? + represent_page(resources, paginator.params['page'].to_i, opts) + else + represent_pagination_index(resources) + end + + HashWithIndifferentAccess.new(response) + end + + private + + def represent_page(resources, page, opts) + resources = paginator.paginate(resources) + opts[:page] = page + serialized = base_represent(resources, opts) + + { + :@context => 'https://www.w3.org/ns/activitystreams', + type: 'OrderedCollectionPage', + id: collection_url(page), + prev: page > 1 ? collection_url(page - 1) : nil, + next: page < resources.total_pages ? collection_url(page + 1) : nil, + partOf: collection_url, + orderedItems: serialized + } + end + + def represent_pagination_index(resources) + paginator.params['page'] = 1 + resources = paginator.paginate(resources) + + { + :@context => 'https://www.w3.org/ns/activitystreams', + type: 'OrderedCollection', + id: collection_url, + totalItems: resources.total_count, + first: collection_url(1), + last: collection_url(resources.total_pages) + } + end + + def collection_url(page = nil) + uri = URI.parse(paginator.request.url) + uri.query ||= "" + parts = uri.query.split('&').reject { |part| part =~ /^page=/ } + parts << "page=#{page}" if page + uri.query = parts.join('&') + uri.to_s.sub(/\?$/, '') + end + end +end diff --git a/app/serializers/activity_pub/object_serializer.rb b/app/serializers/activity_pub/object_serializer.rb new file mode 100644 index 00000000000..cdcef59cc41 --- /dev/null +++ b/app/serializers/activity_pub/object_serializer.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +module ActivityPub + # Serializer for the `Object` ActivityStreams model. + # Reference: https://www.w3.org/TR/activitystreams-core/#object + class ObjectSerializer < ::BaseSerializer + MissingIdentifierError = Class.new(StandardError) + MissingTypeError = Class.new(StandardError) + + def represent(resource, opts = {}, entity_class = nil) + serialized = super(resource, opts, entity_class) + response = wrap(serialized, opts) + + validate_response(HashWithIndifferentAccess.new(response), opts) + end + + private + + def wrap(serialized, _opts) + { :@context => "https://www.w3.org/ns/activitystreams" }.merge(serialized) + end + + def validate_response(response, _opts) + unless response[:id].present? + raise MissingIdentifierError, "The serializer does not provide the mandatory 'id' field." + end + + unless response[:type].present? + raise MissingTypeError, "The serializer does not provide the mandatory 'type' field." + end + + response + end + end +end diff --git a/app/serializers/activity_pub/publish_release_activity_serializer.rb b/app/serializers/activity_pub/publish_release_activity_serializer.rb new file mode 100644 index 00000000000..b70ff470af5 --- /dev/null +++ b/app/serializers/activity_pub/publish_release_activity_serializer.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +module ActivityPub + class PublishReleaseActivitySerializer < ActivitySerializer + entity ReleaseEntity + end +end diff --git a/app/serializers/activity_pub/releases_actor_serializer.rb b/app/serializers/activity_pub/releases_actor_serializer.rb index 5bae83f2dc7..f4b33e25393 100644 --- a/app/serializers/activity_pub/releases_actor_serializer.rb +++ b/app/serializers/activity_pub/releases_actor_serializer.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module ActivityPub - class ReleasesActorSerializer < ActivityStreamsSerializer + class ReleasesActorSerializer < ActorSerializer entity ReleasesActorEntity end end diff --git a/app/serializers/activity_pub/releases_outbox_serializer.rb b/app/serializers/activity_pub/releases_outbox_serializer.rb index b6d4e633fb0..6087e713e64 100644 --- a/app/serializers/activity_pub/releases_outbox_serializer.rb +++ b/app/serializers/activity_pub/releases_outbox_serializer.rb @@ -1,9 +1,7 @@ # frozen_string_literal: true module ActivityPub - class ReleasesOutboxSerializer < ActivityStreamsSerializer - include WithPagination - + class ReleasesOutboxSerializer < CollectionSerializer entity ReleaseEntity end end diff --git a/app/services/merge_requests/request_review_service.rb b/app/services/merge_requests/request_review_service.rb index ebbae98352b..87b00aa088c 100644 --- a/app/services/merge_requests/request_review_service.rb +++ b/app/services/merge_requests/request_review_service.rb @@ -12,6 +12,7 @@ module MergeRequests notify_reviewer(merge_request, user) trigger_merge_request_reviewers_updated(merge_request) + create_system_note(merge_request, user) success else @@ -25,5 +26,9 @@ module MergeRequests notification_service.async.review_requested_of_merge_request(merge_request, current_user, reviewer) todo_service.create_request_review_todo(merge_request, current_user, reviewer) end + + def create_system_note(merge_request, user) + ::SystemNoteService.request_review(merge_request, merge_request.project, current_user, user) + end end end diff --git a/app/services/packages/npm/create_package_service.rb b/app/services/packages/npm/create_package_service.rb index 0f0dc297e9a..a27f059036c 100644 --- a/app/services/packages/npm/create_package_service.rb +++ b/app/services/packages/npm/create_package_service.rb @@ -8,24 +8,35 @@ module Packages PACKAGE_JSON_NOT_ALLOWED_FIELDS = %w[readme readmeFilename licenseText contributors exports].freeze DEFAULT_LEASE_TIMEOUT = 1.hour.to_i + ERROR_REASON_INVALID_PARAMETER = :invalid_parameter + ERROR_REASON_PACKAGE_EXISTS = :package_already_exists + ERROR_REASON_PACKAGE_LEASE_TAKEN = :package_lease_taken + ERROR_REASON_PACKAGE_PROTECTED = :package_attachment_data_empty + def execute - return error('Version is empty.', 400) if version.blank? - return error('Attachment data is empty.', 400) if attachment['data'].blank? - return error('Package already exists.', 403) if current_package_exists? - return error('Package protected.', 403) if current_package_protected? - return error('File is too large.', 400) if file_size_exceeded? + return error('Version is empty.', ERROR_REASON_INVALID_PARAMETER) if version.blank? + return error('Attachment data is empty.', ERROR_REASON_INVALID_PARAMETER) if attachment['data'].blank? + return error('Package already exists.', ERROR_REASON_PACKAGE_EXISTS) if current_package_exists? + return error('Package protected.', ERROR_REASON_PACKAGE_PROTECTED) if current_package_protected? + return error('File is too large.', ERROR_REASON_INVALID_PARAMETER) if file_size_exceeded? package = try_obtain_lease do ApplicationRecord.transaction { create_npm_package! } end - return error('Could not obtain package lease. Please try again.', 400) unless package + unless package + return error('Could not obtain package lease. Please try again.', ERROR_REASON_PACKAGE_LEASE_TAKEN) + end - package + ServiceResponse.success(payload: { package: package }) end private + def error(message, reason) + ServiceResponse.error(message: message, reason: reason) + end + def create_npm_package! package = create_package!(:npm, name: name, version: version) diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb index 9175d91119e..fc27303792b 100644 --- a/app/services/system_note_service.rb +++ b/app/services/system_note_service.rb @@ -45,6 +45,10 @@ module SystemNoteService ::SystemNotes::IssuablesService.new(noteable: issuable, project: project, author: author).change_issuable_reviewers(old_reviewers) end + def request_review(issuable, project, author, user) + ::SystemNotes::IssuablesService.new(noteable: issuable, project: project, author: author).request_review(user) + end + def change_issuable_contacts(issuable, project, author, added_count, removed_count) ::SystemNotes::IssuablesService.new(noteable: issuable, project: project, author: author).change_issuable_contacts(added_count, removed_count) end diff --git a/app/services/system_notes/issuables_service.rb b/app/services/system_notes/issuables_service.rb index 7857bf20c8f..3f96ca9cefb 100644 --- a/app/services/system_notes/issuables_service.rb +++ b/app/services/system_notes/issuables_service.rb @@ -133,6 +133,12 @@ module SystemNotes create_note(NoteSummary.new(noteable, project, author, body, action: 'reviewer')) end + def request_review(user) + body = "#{self.class.issuable_events[:review_requested]} #{user.to_reference}" + + create_note(NoteSummary.new(noteable, project, author, body, action: 'reviewer')) + end + # Called when the contacts of an issuable are changed or removed # We intend to reference the contacts but for security we are just # going to state how many were added/removed for now. See discussion: |