diff options
24 files changed, 906 insertions, 85 deletions
diff --git a/app/helpers/wiki_helper.rb b/app/helpers/wiki_helper.rb index 41f9eedd4bd..17940aeb900 100644 --- a/app/helpers/wiki_helper.rb +++ b/app/helpers/wiki_helper.rb @@ -1,4 +1,6 @@ module WikiHelper + include API::Helpers::RelatedResourcesHelpers + # Produces a pure text breadcrumb for a given page. # # page_slug - The slug of a WikiPage object. @@ -39,4 +41,8 @@ module WikiHelper end end end + + def wiki_attachment_upload_url + expose_url(api_v4_projects_wikis_attachments_path(id: @project.id)) + end end diff --git a/app/services/files/base_service.rb b/app/services/files/base_service.rb index 025f093a428..fc7b236f7da 100644 --- a/app/services/files/base_service.rb +++ b/app/services/files/base_service.rb @@ -7,8 +7,8 @@ module Files def initialize(*args) super - @author_email = params[:author_email] - @author_name = params[:author_name] + @author_email = params[:author_email] || current_user&.email + @author_name = params[:author_name] || current_user&.name @commit_message = params[:commit_message] @last_commit_sha = params[:last_commit_sha] diff --git a/app/services/wikis/create_attachment_service.rb b/app/services/wikis/create_attachment_service.rb new file mode 100644 index 00000000000..30fe0e371a6 --- /dev/null +++ b/app/services/wikis/create_attachment_service.rb @@ -0,0 +1,71 @@ +# frozen_string_literal: true + +module Wikis + class CreateAttachmentService < Files::CreateService + ATTACHMENT_PATH = 'uploads'.freeze + MAX_FILENAME_LENGTH = 255 + + delegate :wiki, to: :project + delegate :repository, to: :wiki + + def initialize(*args) + super + + @file_name = truncate_file_name(params[:file_name]) + @file_path = File.join(ATTACHMENT_PATH, SecureRandom.hex, @file_name) if @file_name + @commit_message ||= "Upload attachment #{@file_name}" + @branch_name ||= wiki.default_branch + end + + def create_commit! + commit_result(create_transformed_commit(@file_content)) + end + + private + + def truncate_file_name(file_name) + return unless file_name.present? + return file_name if file_name.length <= MAX_FILENAME_LENGTH + + extension = File.extname(file_name) + truncate_at = MAX_FILENAME_LENGTH - extension.length - 1 + base_name = File.basename(file_name, extension)[0..truncate_at] + base_name + extension + end + + def validate! + validate_file_name! + validate_permissions! + end + + def validate_file_name! + raise_error('The file name cannot be empty') unless @file_name + end + + def validate_permissions! + unless can?(current_user, :create_wiki, project) + raise_error('You are not allowed to push to the wiki') + end + end + + def create_transformed_commit(content) + repository.create_file( + current_user, + @file_path, + content, + message: @commit_message, + branch_name: @branch_name, + author_email: @author_email, + author_name: @author_name) + end + + def commit_result(commit_id) + { + file_name: @file_name, + file_path: @file_path, + branch: @branch_name, + commit: commit_id + } + end + end +end diff --git a/app/uploaders/file_uploader.rb b/app/uploaders/file_uploader.rb index b1365659834..ffc1e5f75ca 100644 --- a/app/uploaders/file_uploader.rb +++ b/app/uploaders/file_uploader.rb @@ -122,12 +122,6 @@ class FileUploader < GitlabUploader } end - def markdown_link - markdown = +"[#{markdown_name}](#{secure_url})" - markdown.prepend("!") if image_or_video? || dangerous? - markdown - end - def to_h { alt: markdown_name, @@ -192,10 +186,6 @@ class FileUploader < GitlabUploader storage.delete_dir!(store_dir) # only remove when empty end - def markdown_name - (image_or_video? ? File.basename(filename, File.extname(filename)) : filename).gsub("]", "\\]") - end - def identifier @identifier ||= filename end diff --git a/app/uploaders/uploader_helper.rb b/app/uploaders/uploader_helper.rb index 2a2b54a9270..e8a2dce7755 100644 --- a/app/uploaders/uploader_helper.rb +++ b/app/uploaders/uploader_helper.rb @@ -2,32 +2,7 @@ # Extra methods for uploader module UploaderHelper - IMAGE_EXT = %w[png jpg jpeg gif bmp tiff ico].freeze - # We recommend using the .mp4 format over .mov. Videos in .mov format can - # still be used but you really need to make sure they are served with the - # proper MIME type video/mp4 and not video/quicktime or your videos won't play - # on IE >= 9. - # http://archive.sublimevideo.info/20150912/docs.sublimevideo.net/troubleshooting.html - VIDEO_EXT = %w[mp4 m4v mov webm ogv].freeze - # These extension types can contain dangerous code and should only be embedded inline with - # proper filtering. They should always be tagged as "Content-Disposition: attachment", not "inline". - DANGEROUS_EXT = %w[svg].freeze - - def image? - extension_match?(IMAGE_EXT) - end - - def video? - extension_match?(VIDEO_EXT) - end - - def image_or_video? - image? || video? - end - - def dangerous? - extension_match?(DANGEROUS_EXT) - end + include Gitlab::FileMarkdownLinkBuilder private diff --git a/app/views/projects/wikis/edit.html.haml b/app/views/projects/wikis/edit.html.haml index d80d2957466..71359708022 100644 --- a/app/views/projects/wikis/edit.html.haml +++ b/app/views/projects/wikis/edit.html.haml @@ -41,3 +41,8 @@ = render 'sidebar' #delete-wiki-modal.modal.fade + +- content_for :scripts_body do + -# haml-lint:disable InlineJavaScript + :javascript + window.uploads_path = "#{wiki_attachment_upload_url}"; diff --git a/changelogs/unreleased/fj-33475-files-inside-wiki-repo.yml b/changelogs/unreleased/fj-33475-files-inside-wiki-repo.yml new file mode 100644 index 00000000000..8c1f0e3dbf2 --- /dev/null +++ b/changelogs/unreleased/fj-33475-files-inside-wiki-repo.yml @@ -0,0 +1,5 @@ +--- +title: Store wiki uploads inside git repository +merge_request: 21362 +author: +type: added diff --git a/doc/api/wikis.md b/doc/api/wikis.md index 15ce5f96b60..fb0ec773da5 100644 --- a/doc/api/wikis.md +++ b/doc/api/wikis.md @@ -97,12 +97,12 @@ curl --data "format=rdoc&title=Hello&content=Hello world" --header "PRIVATE-TOKE Example response: ```json -{ +{ "content" : "Hello world", "format" : "markdown", "slug" : "Hello", "title" : "Hello" -} +} ``` ## Edit an existing wiki page @@ -154,6 +154,44 @@ DELETE /projects/:id/wikis/:slug curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/wikis/foo" ``` -On success the HTTP status code is `204` and no JSON response is expected. +On success the HTTP status code is `204` and no JSON response is expected. [ce-13372]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/13372 + +## Upload an attachment to the wiki repository + +Uploads a file to the attachment folder inside the wiki's repository. The + attachment folder is the `uploads` folder. + +``` +POST /projects/:id/wikis/attachments +``` + +| Attribute | Type | Required | Description | +| ------------- | ------- | -------- | ---------------------------- | +| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) | +| `file` | string | yes | The attachment to be uploaded | +| `branch` | string | no | The name of the branch. Defaults to the wiki repository default branch | + +To upload a file from your filesystem, use the `--form` argument. This causes +cURL to post data using the header `Content-Type: multipart/form-data`. +The `file=` parameter must point to a file on your filesystem and be preceded +by `@`. For example: + +```bash +curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --form "file=@dk.png" https://gitlab.example.com/api/v4/projects/1/wikis/attachments +``` + +Example response: + +```json +{ + "file_name" : "dk.png", + "file_path" : "uploads/6a061c4cf9f1c28cb22c384b4b8d4e3c/dk.png", + "branch" : "master", + "link" : { + "url" : "uploads/6a061c4cf9f1c28cb22c384b4b8d4e3c/dk.png", + "markdown" : "![dk](uploads/6a061c4cf9f1c28cb22c384b4b8d4e3c/dk.png)" + } +} +``` diff --git a/lib/api/entities.rb b/lib/api/entities.rb index 59042d2b568..624eda3f5dd 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -10,6 +10,28 @@ module API expose :content end + class WikiAttachment < Grape::Entity + include Gitlab::FileMarkdownLinkBuilder + + expose :file_name + expose :file_path + expose :branch + expose :link do + expose :file_path, as: :url + expose :markdown do |_entity| + self.markdown_link + end + end + + def filename + object.file_name + end + + def secure_url + object.file_path + end + end + class UserSafe < Grape::Entity expose :id, :name, :username end diff --git a/lib/api/wikis.rb b/lib/api/wikis.rb index b3fc4e876ad..e86ebc573f2 100644 --- a/lib/api/wikis.rb +++ b/lib/api/wikis.rb @@ -1,6 +1,14 @@ module API class Wikis < Grape::API helpers do + def commit_params(attrs) + { + file_name: attrs[:file][:filename], + file_content: File.read(attrs[:file][:tempfile]), + branch_name: attrs[:branch] + } + end + params :wiki_page_params do requires :content, type: String, desc: 'Content of a wiki page' requires :title, type: String, desc: 'Title of a wiki page' @@ -84,6 +92,29 @@ module API status 204 WikiPages::DestroyService.new(user_project, current_user).execute(wiki_page) end + + desc 'Upload an attachment to the wiki repository' do + detail 'This feature was introduced in GitLab 11.3.' + success Entities::WikiAttachment + end + params do + requires :file, type: File, desc: 'The attachment file to be uploaded' + optional :branch, type: String, desc: 'The name of the branch' + end + post ":id/wikis/attachments", requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do + authorize! :create_wiki, user_project + + result = ::Wikis::CreateAttachmentService.new(user_project, + current_user, + commit_params(declared_params(include_missing: false))).execute + + if result[:status] == :success + status(201) + present OpenStruct.new(result[:result]), with: Entities::WikiAttachment + else + render_api_error!(result[:message], 400) + end + end end end end diff --git a/lib/banzai/filter/wiki_link_filter.rb b/lib/banzai/filter/wiki_link_filter.rb index 870721f895d..1728a442533 100644 --- a/lib/banzai/filter/wiki_link_filter.rb +++ b/lib/banzai/filter/wiki_link_filter.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'uri' - module Banzai module Filter # HTML filter that "fixes" links to pages/files in a wiki. @@ -13,8 +11,12 @@ module Banzai def call return doc unless project_wiki? - doc.search('a:not(.gfm)').each do |el| - process_link_attr el.attribute('href') + doc.search('a:not(.gfm)').each { |el| process_link_attr(el.attribute('href')) } + doc.search('video').each { |el| process_link_attr(el.attribute('src')) } + doc.search('img').each do |el| + attr = el.attribute('data-src') || el.attribute('src') + + process_link_attr(attr) end doc diff --git a/lib/banzai/filter/wiki_link_filter/rewriter.rb b/lib/banzai/filter/wiki_link_filter/rewriter.rb index 072d24e5a11..4bf80aff418 100644 --- a/lib/banzai/filter/wiki_link_filter/rewriter.rb +++ b/lib/banzai/filter/wiki_link_filter/rewriter.rb @@ -10,11 +10,16 @@ module Banzai def apply_rules # Special case: relative URLs beginning with `/uploads/` refer to - # user-uploaded files and will be handled elsewhere. - return @uri.to_s if @uri.relative? && @uri.path.starts_with?('/uploads/') + # user-uploaded files will be handled elsewhere. + return @uri.to_s if public_upload? + + # Special case: relative URLs beginning with Wikis::CreateAttachmentService::ATTACHMENT_PATH + # refer to user-uploaded files to the wiki repository. + unless repository_upload? + apply_file_link_rules! + apply_hierarchical_link_rules! + end - apply_file_link_rules! - apply_hierarchical_link_rules! apply_relative_link_rules! @uri.to_s end @@ -39,6 +44,14 @@ module Banzai @uri = Addressable::URI.parse(link) end end + + def public_upload? + @uri.relative? && @uri.path.starts_with?('/uploads/') + end + + def repository_upload? + @uri.relative? && @uri.path.starts_with?(Wikis::CreateAttachmentService::ATTACHMENT_PATH) + end end end end diff --git a/lib/gitlab/file_markdown_link_builder.rb b/lib/gitlab/file_markdown_link_builder.rb new file mode 100644 index 00000000000..5386656efe7 --- /dev/null +++ b/lib/gitlab/file_markdown_link_builder.rb @@ -0,0 +1,21 @@ +# Builds the markdown link of a file +# It needs the methods filename and secure_url (final destination url) to be defined. +module Gitlab + module FileMarkdownLinkBuilder + include FileTypeDetection + + def markdown_link + return unless name = markdown_name + + markdown = "[#{name.gsub(']', '\\]')}](#{secure_url})" + markdown.prepend("!") if image_or_video? || dangerous? + markdown + end + + def markdown_name + return unless filename.present? + + image_or_video? ? File.basename(filename, File.extname(filename)) : filename + end + end +end diff --git a/lib/gitlab/file_type_detection.rb b/lib/gitlab/file_type_detection.rb new file mode 100644 index 00000000000..25ee07cf940 --- /dev/null +++ b/lib/gitlab/file_type_detection.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +# File helpers methods. +# It needs the method filename to be defined. +module Gitlab + module FileTypeDetection + IMAGE_EXT = %w[png jpg jpeg gif bmp tiff ico].freeze + # We recommend using the .mp4 format over .mov. Videos in .mov format can + # still be used but you really need to make sure they are served with the + # proper MIME type video/mp4 and not video/quicktime or your videos won't play + # on IE >= 9. + # http://archive.sublimevideo.info/20150912/docs.sublimevideo.net/troubleshooting.html + VIDEO_EXT = %w[mp4 m4v mov webm ogv].freeze + # These extension types can contain dangerous code and should only be embedded inline with + # proper filtering. They should always be tagged as "Content-Disposition: attachment", not "inline". + DANGEROUS_EXT = %w[svg].freeze + + def image? + extension_match?(IMAGE_EXT) + end + + def video? + extension_match?(VIDEO_EXT) + end + + def image_or_video? + image? || video? + end + + def dangerous? + extension_match?(DANGEROUS_EXT) + end + + private + + def extension_match?(extensions) + return false unless filename + + extension = File.extname(filename).delete('.') + extensions.include?(extension.downcase) + end + end +end diff --git a/spec/features/projects/wiki/user_creates_wiki_page_spec.rb b/spec/features/projects/wiki/user_creates_wiki_page_spec.rb index 149eeb4f9ba..b30286e4446 100644 --- a/spec/features/projects/wiki/user_creates_wiki_page_spec.rb +++ b/spec/features/projects/wiki/user_creates_wiki_page_spec.rb @@ -146,6 +146,8 @@ describe "User creates wiki page" do expect(page).to have_selector(".katex", count: 3).and have_content("2+2 is 4") end end + + it_behaves_like 'wiki file attachments' end context "in a group namespace", :js do diff --git a/spec/features/projects/wiki/user_updates_wiki_page_spec.rb b/spec/features/projects/wiki/user_updates_wiki_page_spec.rb index 2840d28cf30..2ce5ee0e87d 100644 --- a/spec/features/projects/wiki/user_updates_wiki_page_spec.rb +++ b/spec/features/projects/wiki/user_updates_wiki_page_spec.rb @@ -3,6 +3,7 @@ require 'spec_helper' describe 'User updates wiki page' do shared_examples 'wiki page user update' do let(:user) { create(:user) } + before do project.add_maintainer(user) sign_in(user) @@ -55,6 +56,8 @@ describe 'User updates wiki page' do expect(page).to have_content('Updated Wiki Content') end + + it_behaves_like 'wiki file attachments' end end @@ -64,14 +67,14 @@ describe 'User updates wiki page' do before do visit(project_wikis_path(project)) + + click_link('Edit') end context 'in a user namespace' do let(:project) { create(:project, :wiki_repo, namespace: user.namespace) } it 'updates a page' do - click_link('Edit') - # Commit message field should have correct value. expect(page).to have_field('wiki[message]', with: 'Update home') @@ -84,8 +87,6 @@ describe 'User updates wiki page' do end it 'shows a validation error message' do - click_link('Edit') - fill_in(:wiki_content, with: '') click_button('Save changes') @@ -97,8 +98,6 @@ describe 'User updates wiki page' do end it 'shows the emoji autocompletion dropdown', :js do - click_link('Edit') - find('#wiki_content').native.send_keys('') fill_in(:wiki_content, with: ':') @@ -106,8 +105,6 @@ describe 'User updates wiki page' do end it 'shows the error message' do - click_link('Edit') - wiki_page.update(content: 'Update') click_button('Save changes') @@ -116,30 +113,27 @@ describe 'User updates wiki page' do end it 'updates a page' do - click_on('Edit') fill_in('Content', with: 'Updated Wiki Content') click_on('Save changes') expect(page).to have_content('Updated Wiki Content') end - it 'cancels edititng of a page' do - click_on('Edit') - + it 'cancels editing of a page' do page.within(:css, '.wiki-form .form-actions') do click_on('Cancel') end expect(current_path).to eq(project_wiki_path(project, wiki_page)) end + + it_behaves_like 'wiki file attachments' end context 'in a group namespace' do let(:project) { create(:project, :wiki_repo, namespace: create(:group, :public)) } it 'updates a page' do - click_link('Edit') - # Commit message field should have correct value. expect(page).to have_field('wiki[message]', with: 'Update home') @@ -151,6 +145,8 @@ describe 'User updates wiki page' do expect(page).to have_content("Last edited by #{user.name}") expect(page).to have_content('My awesome wiki!') end + + it_behaves_like 'wiki file attachments' end end @@ -222,6 +218,8 @@ describe 'User updates wiki page' do expect(current_path).to eq(project_wiki_path(project, "foo1/bar1/#{page_name}")) end + + it_behaves_like 'wiki file attachments' end end diff --git a/spec/features/projects/wiki/user_views_wiki_page_spec.rb b/spec/features/projects/wiki/user_views_wiki_page_spec.rb index 760324adacc..747406efc8b 100644 --- a/spec/features/projects/wiki/user_views_wiki_page_spec.rb +++ b/spec/features/projects/wiki/user_views_wiki_page_spec.rb @@ -93,7 +93,7 @@ describe 'User views a wiki page' do allow(wiki_file).to receive(:mime_type).and_return('image/jpeg') allow_any_instance_of(ProjectWiki).to receive(:find_file).with('image.jpg', nil).and_return(wiki_file) - expect(page).to have_xpath('//img[@data-src="image.jpg"]') + expect(page).to have_xpath("//img[@data-src='#{project.wiki.wiki_base_path}/image.jpg']") expect(page).to have_link('image', href: "#{project.wiki.wiki_base_path}/image.jpg") click_on('image') diff --git a/spec/lib/banzai/filter/wiki_link_filter_spec.rb b/spec/lib/banzai/filter/wiki_link_filter_spec.rb index 50d053011b3..b9059b85fdc 100644 --- a/spec/lib/banzai/filter/wiki_link_filter_spec.rb +++ b/spec/lib/banzai/filter/wiki_link_filter_spec.rb @@ -7,6 +7,7 @@ describe Banzai::Filter::WikiLinkFilter do let(:project) { build_stubbed(:project, :public, name: "wiki_link_project", namespace: namespace) } let(:user) { double } let(:wiki) { ProjectWiki.new(project, user) } + let(:repository_upload_folder) { Wikis::CreateAttachmentService::ATTACHMENT_PATH } it "doesn't rewrite absolute links" do filtered_link = filter("<a href='http://example.com:8000/'>Link</a>", project_wiki: wiki).children[0] @@ -20,6 +21,45 @@ describe Banzai::Filter::WikiLinkFilter do expect(filtered_link.attribute('href').value).to eq('/uploads/a.test') end + describe "when links point to the #{Wikis::CreateAttachmentService::ATTACHMENT_PATH} folder" do + context 'with an "a" html tag' do + it 'rewrites links' do + filtered_link = filter("<a href='#{repository_upload_folder}/a.test'>Link</a>", project_wiki: wiki).children[0] + + expect(filtered_link.attribute('href').value).to eq("#{wiki.wiki_base_path}/#{repository_upload_folder}/a.test") + end + end + + context 'with "img" html tag' do + let(:path) { "#{wiki.wiki_base_path}/#{repository_upload_folder}/a.jpg" } + + context 'inside an "a" html tag' do + it 'rewrites links' do + filtered_elements = filter("<a href='#{repository_upload_folder}/a.jpg'><img src='#{repository_upload_folder}/a.jpg'>example</img></a>", project_wiki: wiki) + + expect(filtered_elements.search('img').first.attribute('src').value).to eq(path) + expect(filtered_elements.search('a').first.attribute('href').value).to eq(path) + end + end + + context 'outside an "a" html tag' do + it 'rewrites links' do + filtered_link = filter("<img src='#{repository_upload_folder}/a.jpg'>example</img>", project_wiki: wiki).children[0] + + expect(filtered_link.attribute('src').value).to eq(path) + end + end + end + + context 'with "video" html tag' do + it 'rewrites links' do + filtered_link = filter("<video src='#{repository_upload_folder}/a.mp4'></video>", project_wiki: wiki).children[0] + + expect(filtered_link.attribute('src').value).to eq("#{wiki.wiki_base_path}/#{repository_upload_folder}/a.mp4") + end + end + end + describe "invalid links" do invalid_links = ["http://:8080", "http://", "http://:8080/path"] diff --git a/spec/lib/gitlab/file_markdown_link_builder_spec.rb b/spec/lib/gitlab/file_markdown_link_builder_spec.rb new file mode 100644 index 00000000000..feb2776c5d0 --- /dev/null +++ b/spec/lib/gitlab/file_markdown_link_builder_spec.rb @@ -0,0 +1,80 @@ +# frozen_string_literal: true +require 'rails_helper' + +describe Gitlab::FileMarkdownLinkBuilder do + let(:custom_class) do + Class.new do + include Gitlab::FileMarkdownLinkBuilder + end.new + end + + before do + allow(custom_class).to receive(:filename).and_return(filename) + end + + describe 'markdown_link' do + let(:url) { "/uploads/#{filename}"} + + before do + allow(custom_class).to receive(:secure_url).and_return(url) + end + + context 'when file name has the character ]' do + let(:filename) { 'd]k.png' } + + it 'escapes the character' do + expect(custom_class.markdown_link).to eq '![d\\]k](/uploads/d]k.png)' + end + end + + context 'when file is an image or video' do + let(:filename) { 'dk.png' } + + it 'returns preview markdown link' do + expect(custom_class.markdown_link).to eq '![dk](/uploads/dk.png)' + end + end + + context 'when file is not an image or video' do + let(:filename) { 'dk.zip' } + + it 'returns markdown link' do + expect(custom_class.markdown_link).to eq '[dk.zip](/uploads/dk.zip)' + end + end + + context 'when file name is blank' do + let(:filename) { nil } + + it 'returns nil' do + expect(custom_class.markdown_link).to eq nil + end + end + end + + describe 'mardown_name' do + context 'when file is an image or video' do + let(:filename) { 'dk.png' } + + it 'retrieves the name without the extension' do + expect(custom_class.markdown_name).to eq 'dk' + end + end + + context 'when file is not an image or video' do + let(:filename) { 'dk.zip' } + + it 'retrieves the name with the extesion' do + expect(custom_class.markdown_name).to eq 'dk.zip' + end + end + + context 'when file name is blank' do + let(:filename) { nil } + + it 'returns nil' do + expect(custom_class.markdown_name).to eq nil + end + end + end +end diff --git a/spec/lib/gitlab/file_type_detection_spec.rb b/spec/lib/gitlab/file_type_detection_spec.rb new file mode 100644 index 00000000000..5e9b8988cc8 --- /dev/null +++ b/spec/lib/gitlab/file_type_detection_spec.rb @@ -0,0 +1,82 @@ +# frozen_string_literal: true +require 'rails_helper' + +describe Gitlab::FileTypeDetection do + def upload_fixture(filename) + fixture_file_upload(File.join('spec', 'fixtures', filename)) + end + + describe '#image_or_video?' do + context 'when class is an uploader' do + let(:uploader) do + example_uploader = Class.new(CarrierWave::Uploader::Base) do + include Gitlab::FileTypeDetection + + storage :file + end + + example_uploader.new + end + + it 'returns true for an image file' do + uploader.store!(upload_fixture('dk.png')) + + expect(uploader).to be_image_or_video + end + + it 'returns true for a video file' do + uploader.store!(upload_fixture('video_sample.mp4')) + + expect(uploader).to be_image_or_video + end + + it 'returns false for other extensions' do + uploader.store!(upload_fixture('doc_sample.txt')) + + expect(uploader).not_to be_image_or_video + end + + it 'returns false if filename is blank' do + uploader.store!(upload_fixture('dk.png')) + + allow(uploader).to receive(:filename).and_return(nil) + + expect(uploader).not_to be_image_or_video + end + end + + context 'when class is a regular class' do + let(:custom_class) do + custom_class = Class.new do + include Gitlab::FileTypeDetection + end + + custom_class.new + end + + it 'returns true for an image file' do + allow(custom_class).to receive(:filename).and_return('dk.png') + + expect(custom_class).to be_image_or_video + end + + it 'returns true for a video file' do + allow(custom_class).to receive(:filename).and_return('video_sample.mp4') + + expect(custom_class).to be_image_or_video + end + + it 'returns false for other extensions' do + allow(custom_class).to receive(:filename).and_return('doc_sample.txt') + + expect(custom_class).not_to be_image_or_video + end + + it 'returns false if filename is blank' do + allow(custom_class).to receive(:filename).and_return(nil) + + expect(custom_class).not_to be_image_or_video + end + end + end +end diff --git a/spec/requests/api/wikis_spec.rb b/spec/requests/api/wikis_spec.rb index 489cb001b82..c40d01e1a14 100644 --- a/spec/requests/api/wikis_spec.rb +++ b/spec/requests/api/wikis_spec.rb @@ -139,6 +139,27 @@ describe API::Wikis do end end + shared_examples_for 'uploads wiki attachment' do + it 'pushes attachment to the wiki repository' do + allow(SecureRandom).to receive(:hex).and_return('fixed_hex') + + post(api(url, user), payload) + + expect(response).to have_gitlab_http_status(201) + expect(json_response).to eq result_hash.deep_stringify_keys + end + + it 'responds with validation error on empty file' do + payload.delete(:file) + + post(api(url, user), payload) + + expect(response).to have_gitlab_http_status(400) + expect(json_response.size).to eq(1) + expect(json_response['error']).to eq('file is missing') + end + end + describe 'GET /projects/:id/wikis' do let(:url) { "/projects/#{project.id}/wikis" } @@ -698,4 +719,107 @@ describe API::Wikis do include_examples '204 No Content' end end + + describe 'POST /projects/:id/wikis/attachments' do + let(:payload) { { file: fixture_file_upload('spec/fixtures/dk.png') } } + let(:url) { "/projects/#{project.id}/wikis/attachments" } + let(:file_path) { "#{Wikis::CreateAttachmentService::ATTACHMENT_PATH}/fixed_hex/dk.png" } + let(:result_hash) do + { + file_name: 'dk.png', + file_path: file_path, + branch: 'master', + link: { + url: file_path, + markdown: "![dk](#{file_path})" + } + } + end + + context 'when wiki is disabled' do + let(:project) { create(:project, :wiki_disabled, :wiki_repo) } + + context 'when user is guest' do + before do + post(api(url), payload) + end + + include_examples '404 Project Not Found' + end + + context 'when user is developer' do + before do + project.add_developer(user) + post(api(url, user), payload) + end + + include_examples '403 Forbidden' + end + + context 'when user is maintainer' do + before do + project.add_maintainer(user) + post(api(url, user), payload) + end + + include_examples '403 Forbidden' + end + end + + context 'when wiki is available only for team members' do + let(:project) { create(:project, :wiki_private, :wiki_repo) } + + context 'when user is guest' do + before do + post(api(url), payload) + end + + include_examples '404 Project Not Found' + end + + context 'when user is developer' do + before do + project.add_developer(user) + end + + include_examples 'uploads wiki attachment' + end + + context 'when user is maintainer' do + before do + project.add_maintainer(user) + end + + include_examples 'uploads wiki attachment' + end + end + + context 'when wiki is available for everyone with access' do + let(:project) { create(:project, :wiki_repo) } + + context 'when user is guest' do + before do + post(api(url), payload) + end + + include_examples '404 Project Not Found' + end + + context 'when user is developer' do + before do + project.add_developer(user) + end + + include_examples 'uploads wiki attachment' + end + + context 'when user is maintainer' do + before do + project.add_maintainer(user) + end + + include_examples 'uploads wiki attachment' + end + end + end end diff --git a/spec/services/wikis/create_attachment_service_spec.rb b/spec/services/wikis/create_attachment_service_spec.rb new file mode 100644 index 00000000000..3f4da873ce4 --- /dev/null +++ b/spec/services/wikis/create_attachment_service_spec.rb @@ -0,0 +1,202 @@ +# frozen_string_literal: true +require 'spec_helper' + +describe Wikis::CreateAttachmentService do + let(:project) { create(:project, :wiki_repo) } + let(:user) { create(:user) } + let(:file_name) { 'filename.txt' } + let(:file_path_regex) { %r{#{described_class::ATTACHMENT_PATH}/\h{32}/#{file_name}} } + + let(:file_opts) do + { + file_name: file_name, + file_content: 'Content of attachment' + } + end + let(:opts) { file_opts } + + subject(:service) { described_class.new(project, user, opts) } + + before do + project.add_developer(user) + end + + describe 'initialization' do + context 'author commit info' do + it 'does not raise error if user is nil' do + service = described_class.new(project, nil, opts) + + expect(service.instance_variable_get(:@author_email)).to be_nil + expect(service.instance_variable_get(:@author_name)).to be_nil + end + + it 'fills file_path from the repository uploads folder' do + expect(service.instance_variable_get(:@file_path)).to match(file_path_regex) + end + + context 'when no author info provided' do + it 'fills author_email and author_name from current_user info' do + expect(service.instance_variable_get(:@author_email)).to eq user.email + expect(service.instance_variable_get(:@author_name)).to eq user.name + end + end + + context 'when author info provided' do + let(:author_email) { 'author_email' } + let(:author_name) { 'author_name' } + let(:opts) { file_opts.merge(author_email: author_email, author_name: author_name) } + + it 'fills author_email and author_name from params' do + expect(service.instance_variable_get(:@author_email)).to eq author_email + expect(service.instance_variable_get(:@author_name)).to eq author_name + end + end + end + + context 'commit message' do + context 'when no commit message provided' do + it 'sets a default commit message' do + expect(service.instance_variable_get(:@commit_message)).to eq "Upload attachment #{opts[:file_name]}" + end + end + + context 'when commit message provided' do + let(:commit_message) { 'whatever' } + let(:opts) { file_opts.merge(commit_message: commit_message) } + + it 'use the commit message from params' do + expect(service.instance_variable_get(:@commit_message)).to eq commit_message + end + end + end + + context 'branch name' do + context 'when no branch provided' do + it 'sets the branch from the wiki default_branch' do + expect(service.instance_variable_get(:@branch_name)).to eq project.wiki.default_branch + end + end + + context 'when branch provided' do + let(:branch_name) { 'whatever' } + let(:opts) { file_opts.merge(branch_name: branch_name) } + + it 'use the commit message from params' do + expect(service.instance_variable_get(:@branch_name)).to eq branch_name + end + end + end + end + + describe 'validations' do + context 'when file_name' do + context 'is not present' do + let(:file_name) { nil } + + it 'returns error' do + result = service.execute + + expect(result[:status]).to eq :error + expect(result[:message]).to eq 'The file name cannot be empty' + end + end + + context 'length' do + context 'is bigger than 255' do + let(:file_name) { "#{'0' * 256}.jpg" } + + it 'truncates file name' do + result = service.execute + + expect(result[:status]).to eq :success + expect(result[:result][:file_name].length).to eq 255 + expect(result[:result][:file_name]).to match(/0{251}\.jpg/) + end + end + + context 'is less or equal to 255 does not return error' do + let(:file_name) { '0' * 255 } + + it 'does not return error' do + result = service.execute + + expect(result[:status]).to eq :success + end + end + end + end + + context 'when user' do + shared_examples 'wiki attachment user validations' do + it 'returns error' do + result = described_class.new(project, user2, opts).execute + + expect(result[:status]).to eq :error + expect(result[:message]).to eq 'You are not allowed to push to the wiki' + end + end + + context 'does not have permission' do + let(:user2) { create(:user) } + + it_behaves_like 'wiki attachment user validations' + end + + context 'is nil' do + let(:user2) { nil } + + it_behaves_like 'wiki attachment user validations' + end + end + end + + describe '#execute' do + let(:wiki) { project.wiki } + subject(:service_execute) { service.execute[:result] } + + context 'creates branch if it does not exists' do + let(:branch_name) { 'new_branch' } + let(:opts) { file_opts.merge(branch_name: branch_name) } + + it do + expect(wiki.repository.branches).to be_empty + expect { service.execute }.to change { wiki.repository.branches.count }.by(1) + expect(wiki.repository.branches.first.name).to eq branch_name + end + end + + it 'adds file to the repository' do + expect(wiki.repository.ls_files('HEAD')).to be_empty + + service.execute + + files = wiki.repository.ls_files('HEAD') + expect(files.count).to eq 1 + expect(files.first).to match(file_path_regex) + end + + context 'returns' do + before do + allow(SecureRandom).to receive(:hex).and_return('fixed_hex') + + service_execute + end + + it 'returns the file name' do + expect(service_execute[:file_name]).to eq file_name + end + + it 'returns the path where file was stored' do + expect(service_execute[:file_path]).to eq 'uploads/fixed_hex/filename.txt' + end + + it 'returns the branch where the file was pushed' do + expect(service_execute[:branch]).to eq wiki.default_branch + end + + it 'returns the commit id' do + expect(service_execute[:commit]).not_to be_empty + end + end + end +end diff --git a/spec/support/shared_examples/wiki_file_attachments_examples.rb b/spec/support/shared_examples/wiki_file_attachments_examples.rb new file mode 100644 index 00000000000..b6fb2a66b0e --- /dev/null +++ b/spec/support/shared_examples/wiki_file_attachments_examples.rb @@ -0,0 +1,88 @@ +# frozen_string_literal: true + +# Requires a context containing: +# project + +shared_examples 'wiki file attachments' do + include DropzoneHelper + + context 'uploading attachments', :js do + let(:wiki) { project.wiki } + + def attach_with_dropzone(wait = false) + dropzone_file([Rails.root.join('spec', 'fixtures', 'dk.png')], 0, wait) + end + + context 'before uploading' do + it 'shows "Attach a file" button' do + expect(page).to have_button('Attach a file') + expect(page).not_to have_selector('.uploading-progress-container', visible: true) + end + end + + context 'uploading is in progress' do + it 'cancels uploading on clicking to "Cancel" button' do + slow_requests do + attach_with_dropzone + + click_button 'Cancel' + end + + expect(page).to have_button('Attach a file') + expect(page).not_to have_button('Cancel') + expect(page).not_to have_selector('.uploading-progress-container', visible: true) + end + + it 'shows "Attaching a file" message on uploading 1 file' do + slow_requests do + attach_with_dropzone + + expect(page).to have_selector('.attaching-file-message', visible: true, text: 'Attaching a file -') + end + end + end + + context 'uploading is complete' do + it 'shows "Attach a file" button on uploading complete' do + attach_with_dropzone + wait_for_requests + + expect(page).to have_button('Attach a file') + expect(page).not_to have_selector('.uploading-progress-container', visible: true) + end + + it 'the markdown link is added to the page' do + fill_in(:wiki_content, with: '') + attach_with_dropzone(true) + wait_for_requests + + expect(page.find('#wiki_content').value) + .to match(%r{\!\[dk\]\(uploads/\h{32}/dk\.png\)$}) + end + + it 'the links point to the wiki root url' do + attach_with_dropzone(true) + wait_for_requests + + find('.js-md-preview-button').click + file_path = page.find('input[name="files[]"]', visible: :hidden).value + link = page.find('a.no-attachment-icon')['href'] + img_link = page.find('a.no-attachment-icon img')['src'] + + expect(link).to eq img_link + expect(URI.parse(link).path).to eq File.join(wiki.wiki_base_path, file_path) + end + + it 'the file has been added to the wiki repository' do + expect do + attach_with_dropzone(true) + wait_for_requests + end.to change { wiki.repository.ls_files('HEAD').count }.by(1) + + file_path = page.find('input[name="files[]"]', visible: :hidden).value + + expect(wiki.find_file(file_path, 'HEAD').path).not_to be_nil + end + end + end +end diff --git a/spec/uploaders/uploader_helper_spec.rb b/spec/uploaders/uploader_helper_spec.rb index 33da93cc9d0..fd6712d4645 100644 --- a/spec/uploaders/uploader_helper_spec.rb +++ b/spec/uploaders/uploader_helper_spec.rb @@ -11,27 +11,10 @@ describe UploaderHelper do example_uploader.new end - def upload_fixture(filename) - fixture_file_upload(File.join('spec', 'fixtures', filename)) - end - - describe '#image_or_video?' do - it 'returns true for an image file' do - uploader.store!(upload_fixture('dk.png')) - - expect(uploader).to be_image_or_video - end - - it 'it returns true for a video file' do - uploader.store!(upload_fixture('video_sample.mp4')) - - expect(uploader).to be_image_or_video - end - - it 'returns false for other extensions' do - uploader.store!(upload_fixture('doc_sample.txt')) - - expect(uploader).not_to be_image_or_video + describe '#extension_match?' do + it 'returns false if file does not exists' do + expect(uploader.file).to be_nil + expect(uploader.send(:extension_match?, 'jpg')).to eq false end end end |