relative_link_filter.rb 3.5 KB
Newer Older
1 2
require 'uri'

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

17 18
        @uri_types = {}

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

23
        doc.css('img, video').each do |el|
24
          process_link_attr el.attribute('src')
25
          process_link_attr el.attribute('data-src')
26 27 28 29 30 31 32
        end

        doc
      end

      protected

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

37 38
      def process_link_attr(html_attr)
        return if html_attr.blank?
39
        return if html_attr.value.start_with?('//')
40 41 42 43 44

        uri = URI(html_attr.value)
        if uri.relative? && uri.path.present?
          html_attr.value = rebuild_relative_uri(uri).to_s
        end
45 46
      rescue URI::Error
        # noop
47 48 49
      end

      def rebuild_relative_uri(uri)
50
        file_path = relative_file_path(uri)
51 52 53

        uri.path = [
          relative_url_root,
54
          context[:project].path_with_namespace,
55
          uri_type(file_path),
56 57
          Addressable::URI.escape(ref),
          Addressable::URI.escape(file_path)
58 59 60 61 62
        ].compact.join('/').squeeze('/').chomp('/')

        uri
      end

63 64 65 66
      def relative_file_path(uri)
        path = Addressable::URI.unescape(uri.path)
        request_path = Addressable::URI.unescape(context[:requested_path])
        nested_path = build_relative_path(path, request_path)
67 68 69
        file_exists?(nested_path) ? nested_path : path
      end

70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91
      # 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)
92 93
        return request_path if path.empty?
        return path unless request_path
94
        return path[1..-1] if path.start_with?('/')
95 96

        parts = request_path.split('/')
97
        parts.pop if uri_type(request_path) != :tree
98

99
        path.sub!(%r{\A\./}, '')
100

101
        while path.start_with?('../')
102 103 104 105
          parts.pop
          path.sub!('../', '')
        end

106 107 108 109
        parts.push(path).join('/')
      end

      def file_exists?(path)
110
        path.present? && !!uri_type(path)
111 112
      end

113
      def uri_type(path)
114
        @uri_types[path] ||= current_commit.uri_type(path)
115 116
      end

117
      def current_commit
118
        @current_commit ||= context[:commit] || repository.commit(ref)
119 120 121 122 123 124
      end

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

125
      def ref
126
        context[:ref] || context[:project].default_branch
127 128 129
      end

      def repository
130
        @repository ||= context[:project].try(:repository)
131 132 133 134
      end
    end
  end
end