Commit f9475e29 authored by Francisco Javier López's avatar Francisco Javier López Committed by Douwe Maan

Uploads to wiki stored inside the wiki git repository

parent 0689900c
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