summaryrefslogtreecommitdiff
path: root/lib/gitlab/asciidoc/include_processor.rb
blob: 6c4ecc04cdcbebc72ac584be0464e747cfca0e36 (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
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
# frozen_string_literal: true

require 'asciidoctor/include_ext/include_processor'

module Gitlab
  module Asciidoc
    # Asciidoctor extension for processing includes (macro include::[]) within
    # documents inside the same repository.
    class IncludeProcessor < Asciidoctor::IncludeExt::IncludeProcessor
      extend ::Gitlab::Utils::Override

      def initialize(context)
        super(logger: Gitlab::AppLogger)

        @context = context
        @repository = context[:repository] || context[:project].try(:repository)
        @max_includes = context[:max_includes].to_i
        @included = []

        # Note: Asciidoctor calls #freeze on extensions, so we can't set new
        # instance variables after initialization.
        @cache = {
            uri_types: {}
        }
      end

      protected

      override :include_allowed?
      def include_allowed?(target, reader)
        doc = reader.document

        max_include_depth = doc.attributes.fetch('max-include-depth').to_i

        return false if max_include_depth < 1
        return false if target_http?(target)
        return false if included.size >= max_includes

        true
      end

      override :resolve_target_path
      def resolve_target_path(target, reader)
        return unless repository.try(:exists?)

        base_path = reader.include_stack.empty? ? requested_path : reader.file
        path = resolve_relative_path(target, base_path)

        path if Gitlab::Git::Blob.find(repository, ref, path)
      end

      override :read_lines
      def read_lines(filename, selector)
        blob = read_blob(ref, filename)

        if selector
          blob.data.each_line.select.with_index(1, &selector)
        else
          blob.data
        end
      end

      override :unresolved_include!
      def unresolved_include!(target, reader)
        reader.unshift_line("*[ERROR: include::#{target}[] - unresolved directive]*")
      end

      private

      attr_reader :context, :repository, :cache, :max_includes, :included

      # Gets a Blob at a path for a specific revision.
      # This method will check that the Blob exists and contains readable text.
      #
      # revision - The String SHA1.
      # path     - The String file path.
      #
      # Returns a Blob
      def read_blob(ref, filename)
        blob = repository&.blob_at(ref, filename)

        raise 'Blob not found' unless blob
        raise 'File is not readable' unless blob.readable_text?

        included << filename

        blob
      end

      # Resolves the given relative path of file in repository into canonical
      # path based on the specified base_path.
      #
      # Examples:
      #
      #   # File in the same directory as the current path
      #   resolve_relative_path("users.adoc", "doc/api/README.adoc")
      #   # => "doc/api/users.adoc"
      #
      #   # File in the same directory, which is also the current path
      #   resolve_relative_path("users.adoc", "doc/api")
      #   # => "doc/api/users.adoc"
      #
      #   # Going up one level to a different directory
      #   resolve_relative_path("../update/7.14-to-8.0.adoc", "doc/api/README.adoc")
      #   # => "doc/update/7.14-to-8.0.adoc"
      #
      # Returns a String
      def resolve_relative_path(path, base_path)
        p = Pathname(base_path)
        p = p.dirname unless p.extname.empty?
        p += path

        p.cleanpath.to_s
      end

      def current_commit
        cache[:current_commit] ||= context[:commit] || repository&.commit(ref)
      end

      def ref
        context[:ref] || repository&.root_ref
      end

      def requested_path
        cache[:requested_path] ||= Addressable::URI.unescape(context[:requested_path])
      end

      def uri_type(path)
        cache[:uri_types][path] ||= current_commit&.uri_type(path)
      end
    end
  end
end