summaryrefslogtreecommitdiff
path: root/lib/gitlab/markdown/relative_link_filter.rb
blob: deb302c88e13425dac1edfe1832a01c1fdac790f (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
require 'html/pipeline/filter'
require 'uri'

module Gitlab
  module Markdown
    # HTML filter that "fixes" relative links to files in a repository.
    #
    # Context options:
    #   :commit
    #   :project
    #   :project_wiki
    #   :requested_path
    #   :ref
    class RelativeLinkFilter < HTML::Pipeline::Filter

      def call
        if linkable_files?
          doc.search('a').each do |el|
            process_link_attr el.attribute('href')
          end

          doc.search('img').each do |el|
            process_link_attr el.attribute('src')
          end
        end

        doc
      end

      protected

      def linkable_files?
        context[:project_wiki].nil? && repository.try(:exists?) && !repository.empty?
      end

      def process_link_attr(html_attr)
        return if html_attr.blank?

        uri = URI(html_attr.value)
        if uri.relative? && uri.path.present?
          html_attr.value = rebuild_relative_uri(uri).to_s
        end
      end

      def rebuild_relative_uri(uri)
        file_path = relative_file_path(uri.path)

        uri.path = [
          relative_url_root,
          context[:project].path_with_namespace,
          path_type(file_path),
          ref || 'master',  # assume that if no ref exists we can point to master
          file_path
        ].compact.join('/').squeeze('/').chomp('/')

        uri
      end

      def relative_file_path(path)
        nested_path = build_nested_path(path, context[:requested_path])
        file_exists?(nested_path) ? nested_path : path
      end

      # Covering a special case, when the link is referencing file in the same
      # directory.
      # If we are at doc/api/README.md and the README.md contains relative
      # links like [Users](users.md), this takes the request
      # path(doc/api/README.md) and replaces the README.md with users.md so the
      # path looks like doc/api/users.md.
      # If we are at doc/api and the README.md shown in below the tree view
      # this takes the request path(doc/api) and adds users.md so the path
      # looks like doc/api/users.md
      def build_nested_path(path, request_path)
        return request_path if path.empty?
        return path unless request_path

        parts = request_path.split('/')
        parts.pop if path_type(request_path) != 'tree'
        parts.push(path).join('/')
      end

      def file_exists?(path)
        return false if path.nil?
        repository.blob_at(current_sha, path).present? ||
          repository.tree(current_sha, path).entries.any?
      end

      # Check if the path is pointing to a directory(tree) or a file(blob)
      # eg. doc/api is directory and doc/README.md is file.
      def path_type(path)
        return 'tree' if repository.tree(current_sha, path).entries.any?
        return 'raw' if repository.blob_at(current_sha, path).try(:image?)
        'blob'
      end

      def current_sha
        context[:commit].try(:id) ||
          ref ? repository.commit(ref).try(:sha) : repository.head_commit.sha
      end

      def relative_url_root
        Gitlab.config.gitlab.relative_url_root.presence || '/'
      end

      def ref
        context[:ref]
      end

      def repository
        context[:project].try(:repository)
      end
    end
  end
end