relative_link_filter.rb 4.0 KB
Newer Older
1
require 'banzai'
2
require 'html/pipeline/filter'
3 4
require 'uri'

5 6
module Banzai
  module Filter
7 8 9 10 11 12 13
    # HTML filter that "fixes" relative links to files in a repository.
    #
    # Context options:
    #   :commit
    #   :project
    #   :project_wiki
    #   :ref
14
    #   :requested_path
15 16
    class RelativeLinkFilter < HTML::Pipeline::Filter
      def call
17 18
        return doc unless linkable_files?

19
        doc.search('a:not(.gfm)').each do |el|
20 21 22 23 24
          process_link_attr el.attribute('href')
        end

        doc.search('img').each do |el|
          process_link_attr el.attribute('src')
25 26 27 28 29 30 31
        end

        doc
      end

      protected

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

36 37 38 39 40 41 42
      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
43 44
      rescue URI::Error
        # noop
45 46 47 48 49 50 51
      end

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

        uri.path = [
          relative_url_root,
52
          context[:project].path_with_namespace,
53
          path_type(file_path),
54
          ref || context[:project].default_branch,  # if no ref exists, point to the default branch
55 56 57 58 59 60 61
          file_path
        ].compact.join('/').squeeze('/').chomp('/')

        uri
      end

      def relative_file_path(path)
62
        nested_path = build_relative_path(path, context[:requested_path])
63 64 65
        file_exists?(nested_path) ? nested_path : path
      end

66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87
      # Convert a relative path into its correct location based on the currently
      # requested path
      #
      # path         - Relative path String
      # request_path - Currently-requested path String
      #
      # Examples:
      #
      #   # File in the same directory as the current path
      #   build_relative_path("users.md", "doc/api/README.md")
      #   # => "doc/api/users.md"
      #
      #   # File in the same directory, which is also the current path
      #   build_relative_path("users.md", "doc/api")
      #   # => "doc/api/users.md"
      #
      #   # Going up one level to a different directory
      #   build_relative_path("../update/7.14-to-8.0.md", "doc/api/README.md")
      #   # => "doc/update/7.14-to-8.0.md"
      #
      # Returns a String
      def build_relative_path(path, request_path)
88 89 90 91 92
        return request_path if path.empty?
        return path unless request_path

        parts = request_path.split('/')
        parts.pop if path_type(request_path) != 'tree'
93

94
        while parts.length > 1 && path.start_with?('../')
95 96 97 98
          parts.pop
          path.sub!('../', '')
        end

99 100 101 102 103 104 105 106 107
        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

108 109 110 111 112 113 114 115 116 117 118
      # Get the type of the given path
      #
      # path - String path to check
      #
      # Examples:
      #
      #   path_type('doc/README.md') # => 'blob'
      #   path_type('doc/logo.png')  # => 'raw'
      #   path_type('doc/api')       # => 'tree'
      #
      # Returns a String
119
      def path_type(path)
120
        unescaped_path = Addressable::URI.unescape(path)
121 122

        if tree?(unescaped_path)
123
          'tree'
124
        elsif image?(unescaped_path)
125 126 127 128
          'raw'
        else
          'blob'
        end
129 130
      end

131 132 133 134 135 136 137 138
      def tree?(path)
        repository.tree(current_sha, path).entries.any?
      end

      def image?(path)
        repository.blob_at(current_sha, path).try(:image?)
      end

139
      def current_sha
140 141
        context[:commit].try(:id) ||
          ref ? repository.commit(ref).try(:sha) : repository.head_commit.sha
142 143 144 145 146 147
      end

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

148 149
      def ref
        context[:ref]
150 151 152
      end

      def repository
153
        context[:project].try(:repository)
154 155 156 157
      end
    end
  end
end