Commit 4d41bbd7 authored by Douwe Maan's avatar Douwe Maan

Merge branch 'fj-33475-files-inside-wiki-repo' into 'master'

Uploads to wiki stored inside the wiki git repository

Closes #33475

See merge request gitlab-org/gitlab-ce!21362
parents 0689900c f9475e29
module WikiHelper module WikiHelper
include API::Helpers::RelatedResourcesHelpers
# Produces a pure text breadcrumb for a given page. # Produces a pure text breadcrumb for a given page.
# #
# page_slug - The slug of a WikiPage object. # page_slug - The slug of a WikiPage object.
...@@ -39,4 +41,8 @@ module WikiHelper ...@@ -39,4 +41,8 @@ module WikiHelper
end end
end end
end end
def wiki_attachment_upload_url
expose_url(api_v4_projects_wikis_attachments_path(id: @project.id))
end
end end
...@@ -7,8 +7,8 @@ module Files ...@@ -7,8 +7,8 @@ module Files
def initialize(*args) def initialize(*args)
super super
@author_email = params[:author_email] @author_email = params[:author_email] || current_user&.email
@author_name = params[:author_name] @author_name = params[:author_name] || current_user&.name
@commit_message = params[:commit_message] @commit_message = params[:commit_message]
@last_commit_sha = params[:last_commit_sha] @last_commit_sha = params[:last_commit_sha]
......
# 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
...@@ -122,12 +122,6 @@ class FileUploader < GitlabUploader ...@@ -122,12 +122,6 @@ class FileUploader < GitlabUploader
} }
end end
def markdown_link
markdown = +"[#{markdown_name}](#{secure_url})"
markdown.prepend("!") if image_or_video? || dangerous?
markdown
end
def to_h def to_h
{ {
alt: markdown_name, alt: markdown_name,
...@@ -192,10 +186,6 @@ class FileUploader < GitlabUploader ...@@ -192,10 +186,6 @@ class FileUploader < GitlabUploader
storage.delete_dir!(store_dir) # only remove when empty storage.delete_dir!(store_dir) # only remove when empty
end end
def markdown_name
(image_or_video? ? File.basename(filename, File.extname(filename)) : filename).gsub("]", "\\]")
end
def identifier def identifier
@identifier ||= filename @identifier ||= filename
end end
......
...@@ -2,32 +2,7 @@ ...@@ -2,32 +2,7 @@
# Extra methods for uploader # Extra methods for uploader
module UploaderHelper module UploaderHelper
IMAGE_EXT = %w[png jpg jpeg gif bmp tiff ico].freeze include Gitlab::FileMarkdownLinkBuilder
# 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 private
......
...@@ -41,3 +41,8 @@ ...@@ -41,3 +41,8 @@
= render 'sidebar' = render 'sidebar'
#delete-wiki-modal.modal.fade #delete-wiki-modal.modal.fade
- content_for :scripts_body do
-# haml-lint:disable InlineJavaScript
:javascript
window.uploads_path = "#{wiki_attachment_upload_url}";
---
title: Store wiki uploads inside git repository
merge_request: 21362
author:
type: added
...@@ -97,12 +97,12 @@ curl --data "format=rdoc&title=Hello&content=Hello world" --header "PRIVATE-TOKE ...@@ -97,12 +97,12 @@ curl --data "format=rdoc&title=Hello&content=Hello world" --header "PRIVATE-TOKE
Example response: Example response:
```json ```json
{ {
"content" : "Hello world", "content" : "Hello world",
"format" : "markdown", "format" : "markdown",
"slug" : "Hello", "slug" : "Hello",
"title" : "Hello" "title" : "Hello"
} }
``` ```
## Edit an existing wiki page ## Edit an existing wiki page
...@@ -154,6 +154,44 @@ DELETE /projects/:id/wikis/:slug ...@@ -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" 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 [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)"
}
}
```
...@@ -10,6 +10,28 @@ module API ...@@ -10,6 +10,28 @@ module API
expose :content expose :content
end 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 class UserSafe < Grape::Entity
expose :id, :name, :username expose :id, :name, :username
end end
......
module API module API
class Wikis < Grape::API class Wikis < Grape::API
helpers do 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 params :wiki_page_params do
requires :content, type: String, desc: 'Content of a wiki page' requires :content, type: String, desc: 'Content of a wiki page'
requires :title, type: String, desc: 'Title of a wiki page' requires :title, type: String, desc: 'Title of a wiki page'
...@@ -84,6 +92,29 @@ module API ...@@ -84,6 +92,29 @@ module API
status 204 status 204
WikiPages::DestroyService.new(user_project, current_user).execute(wiki_page) WikiPages::DestroyService.new(user_project, current_user).execute(wiki_page)
end 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 end
end end
# frozen_string_literal: true # frozen_string_literal: true
require 'uri'
module Banzai module Banzai
module Filter module Filter
# HTML filter that "fixes" links to pages/files in a wiki. # HTML filter that "fixes" links to pages/files in a wiki.
...@@ -13,8 +11,12 @@ module Banzai ...@@ -13,8 +11,12 @@ module Banzai
def call def call
return doc unless project_wiki? return doc unless project_wiki?
doc.search('a:not(.gfm)').each do |el| doc.search('a:not(.gfm)').each { |el| process_link_attr(el.attribute('href')) }
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 end
doc doc
......
...@@ -10,11 +10,16 @@ module Banzai ...@@ -10,11 +10,16 @@ module Banzai
def apply_rules def apply_rules
# Special case: relative URLs beginning with `/uploads/` refer to # Special case: relative URLs beginning with `/uploads/` refer to
# user-uploaded files and will be handled elsewhere. # user-uploaded files will be handled elsewhere.
return @uri.to_s if @uri.relative? && @uri.path.starts_with?('/uploads/') 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! apply_relative_link_rules!
@uri.to_s @uri.to_s
end end
...@@ -39,6 +44,14 @@ module Banzai ...@@ -39,6 +44,14 @@ module Banzai
@uri = Addressable::URI.parse(link) @uri = Addressable::URI.parse(link)
end end
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 end
end end
......
# 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
# 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
...@@ -146,6 +146,8 @@ describe "User creates wiki page" do ...@@ -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") expect(page).to have_selector(".katex", count: 3).and have_content("2+2 is 4")
end end
end end
it_behaves_like 'wiki file attachments'
end end
context "in a group namespace", :js do context "in a group namespace", :js do
......
...@@ -3,6 +3,7 @@ require 'spec_helper' ...@@ -3,6 +3,7 @@ require 'spec_helper'
describe 'User updates wiki page' do describe 'User updates wiki page' do
shared_examples 'wiki page user update' do shared_examples 'wiki page user update' do
let(:user) { create(:user) } let(:user) { create(:user) }
before do before do
project.add_maintainer(user) project.add_maintainer(user)
sign_in(user) sign_in(user)
...@@ -55,6 +56,8 @@ describe 'User updates wiki page' do ...@@ -55,6 +56,8 @@ describe 'User updates wiki page' do
expect(page).to have_content('Updated Wiki Content') expect(page).to have_content('Updated Wiki Content')
end end
it_behaves_like 'wiki file attachments'
end end
end end
...@@ -64,14 +67,14 @@ describe 'User updates wiki page' do ...@@ -64,14 +67,14 @@ describe 'User updates wiki page' do
before do before do
visit(project_wikis_path(project)) visit(project_wikis_path(project))
click_link('Edit')
end end
context 'in a user namespace' do context 'in a user namespace' do
let(:project) { create(:project, :wiki_repo, namespace: user.namespace) } let(:project) { create(:project, :wiki_repo, namespace: user.namespace) }
it 'updates a page' do it 'updates a page' do
click_link('Edit')
# Commit message field should have correct value. # Commit message field should have correct value.
expect(page).to have_field('wiki[message]', with: 'Update home') expect(page).to have_field('wiki[message]', with: 'Update home')
...@@ -84,8 +87,6 @@ describe 'User updates wiki page' do ...@@ -84,8 +87,6 @@ describe 'User updates wiki page' do
end end
it 'shows a validation error message' do it 'shows a validation error message' do
click_link('Edit')
fill_in(:wiki_content, with: '') fill_in(:wiki_content, with: '')
click_button('Save changes') click_button('Save changes')
...@@ -97,8 +98,6 @@ describe 'User updates wiki page' do ...@@ -97,8 +98,6 @@ describe 'User updates wiki page' do
end end
it 'shows the emoji autocompletion dropdown', :js do it 'shows the emoji autocompletion dropdown', :js do
click_link('Edit')
find('#wiki_content').native.send_keys('') find('#wiki_content').native.send_keys('')
fill_in(:wiki_content, with: ':') fill_in(:wiki_content, with: ':')
...@@ -106,8 +105,6 @@ describe 'User updates wiki page' do ...@@ -106,8 +105,6 @@ describe 'User updates wiki page' do
end end
it 'shows the error message' do it 'shows the error message' do
click_link('Edit')
wiki_page.update(content: 'Update') wiki_page.update(content: 'Update')
click_button('Save changes') click_button('Save changes')
...@@ -116,30 +113,27 @@ describe 'User updates wiki page' do ...@@ -116,30 +113,27 @@ describe 'User updates wiki page' do
end end
it 'updates a page' do it 'updates a page' do
click_on('Edit')
fill_in('Content', with: 'Updated Wiki Content') fill_in('Content', with: 'Updated Wiki Content')
click_on('Save changes') click_on('Save changes')
expect(page).to have_content('Updated Wiki Content') expect(page).to have_content('Updated Wiki Content')
end end
it 'cancels edititng of a page' do it 'cancels editing of a page' do
click_on('Edit')
page.within(:css, '.wiki-form .form-actions') do page.within(:css, '.wiki-form .form-actions') do
click_on('Cancel') click_on('Cancel')
end end
expect(current_path).to eq(project_wiki_path(project, wiki_page)) expect(current_path).to eq(project_wiki_path(project, wiki_page))
end end
it_behaves_like 'wiki file attachments'
end end
context 'in a group namespace' do context 'in a group namespace' do
let(:project) { create(:project, :wiki_repo, namespace: create(:group, :public)) } let(:project) { create(:project, :wiki_repo, namespace: create(:group, :public)) }
it 'updates a page' do it 'updates a page' do
click_link('Edit')
# Commit message field should have correct value. # Commit message field should have correct value.
expect(page).to have_field('wiki[message]', with: 'Update home') expect(page).to have_field('wiki[message]', with: 'Update home')
...@@ -151,6 +145,8 @@ describe 'User updates wiki page' do ...@@ -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("Last edited by #{user.name}")
expect(page).to have_content('My awesome wiki!') expect(page).to have_content('My awesome wiki!')
end end
it_behaves_like 'wiki file attachments'
end end
end end
...@@ -222,6 +218,8 @@ describe 'User updates wiki page' do ...@@ -222,6 +218,8 @@ describe 'User updates wiki page' do
expect(current_path).to eq(project_wiki_path(project, "foo1/bar1/#{page_name}")) expect(current_path).to eq(project_wiki_path(project, "foo1/bar1/#{page_name}"))
end end
it_behaves_like 'wiki file attachments'
end end
end end
......
...@@ -93,7 +93,7 @@ describe 'User views a wiki page' do ...@@ -93,7 +93,7 @@ describe 'User views a wiki page' do
allow(wiki_file).to receive(:mime_type).and_return('image/jpeg') 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) 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") expect(page).to have_link('image', href: "#{project.wiki.wiki_base_path}/image.jpg")
click_on('image') click_on('image')
......
...@@ -7,6 +7,7 @@ describe Banzai::Filter::WikiLinkFilter do ...@@ -7,6 +7,7 @@ describe Banzai::Filter::WikiLinkFilter do
let(:project) { build_stubbed(:project, :public, name: "wiki_link_project", namespace: namespace) } let(:project) { build_stubbed(:project, :public, name: "wiki_link_project", namespace: namespace) }
let(:user) { double } let(:user) { double }
let(:wiki) { ProjectWiki.new(project, user) } let(:wiki) { ProjectWiki.new(project, user) }
let(:repository_upload_folder) { Wikis::CreateAttachmentService::ATTACHMENT_PATH }
it "doesn't rewrite absolute links" do it "doesn't rewrite absolute links" do
filtered_link = filter("<a href='http://example.com:8000/'>Link</a>", project_wiki: wiki).children[0] 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 ...@@ -20,6 +21,45 @@ describe Banzai::Filter::WikiLinkFilter do
expect(filtered_link.attribute('href').value).to eq('/uploads/a.test') expect(filtered_link.attribute('href').value).to eq('/uploads/a.test')
end 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 describe "invalid links" do
invalid_links = ["http://:8080", "http://", "http://:8080/path"] invalid_links = ["http://:8080", "http://", "http://:8080/path"]
......
# 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
# 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
...@@ -139,6 +139,27 @@ describe API::Wikis do ...@@ -139,6 +139,27 @@ describe API::Wikis do
end end
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 describe 'GET /projects/:id/wikis' do
let(:url) { "/projects/#{project.id}/wikis" } let(:url) { "/projects/#{project.id}/wikis" }
...@@ -698,4 +719,107 @@ describe API::Wikis do ...@@ -698,4 +719,107 @@ describe API::Wikis do
include_examples '204 No Content' include_examples '204 No Content'
end end
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 end
# 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
# 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
...@@ -11,27 +11,10 @@ describe UploaderHelper do ...@@ -11,27 +11,10 @@ describe UploaderHelper do
example_uploader.new example_uploader.new
end end
def upload_fixture(filename) describe '#extension_match?' do
fixture_file_upload(File.join('spec', 'fixtures', filename)) it 'returns false if file does not exists' do
end expect(uploader.file).to be_nil
expect(uploader.send(:extension_match?, 'jpg')).to eq false
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
end end
end end
end end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment