1# frozen_string_literal: true
2
3require 'asciidoctor/include_ext/include_processor'
4
5module Gitlab
6  module Asciidoc
7    # Asciidoctor extension for processing includes (macro include::[]) within
8    # documents inside the same repository.
9    class IncludeProcessor < Asciidoctor::IncludeExt::IncludeProcessor
10      extend ::Gitlab::Utils::Override
11
12      def initialize(context)
13        super(logger: Gitlab::AppLogger)
14
15        @context = context
16        @repository = context[:repository] || context[:project].try(:repository)
17        @max_includes = context[:max_includes].to_i
18        @included = []
19
20        # Note: Asciidoctor calls #freeze on extensions, so we can't set new
21        # instance variables after initialization.
22        @cache = {
23            uri_types: {}
24        }
25      end
26
27      protected
28
29      override :include_allowed?
30      def include_allowed?(target, reader)
31        doc = reader.document
32
33        max_include_depth = doc.attributes.fetch('max-include-depth').to_i
34
35        return false if max_include_depth < 1
36        return false if target_uri?(target)
37        return false if included.size >= max_includes
38
39        true
40      end
41
42      override :resolve_target_path
43      def resolve_target_path(target, reader)
44        return unless repository.try(:exists?)
45
46        base_path = reader.include_stack.empty? ? requested_path : reader.file
47        path = resolve_relative_path(target, base_path)
48
49        path if Gitlab::Git::Blob.find(repository, ref, path)
50      end
51
52      override :read_lines
53      def read_lines(filename, selector)
54        blob = read_blob(ref, filename)
55
56        if selector
57          blob.data.each_line.select.with_index(1, &selector)
58        else
59          blob.data
60        end
61      end
62
63      override :unresolved_include!
64      def unresolved_include!(target, reader)
65        reader.unshift_line("*[ERROR: include::#{target}[] - unresolved directive]*")
66      end
67
68      private
69
70      attr_reader :context, :repository, :cache, :max_includes, :included
71
72      # Gets a Blob at a path for a specific revision.
73      # This method will check that the Blob exists and contains readable text.
74      #
75      # revision - The String SHA1.
76      # path     - The String file path.
77      #
78      # Returns a Blob
79      def read_blob(ref, filename)
80        blob = repository&.blob_at(ref, filename)
81
82        raise 'Blob not found' unless blob
83        raise 'File is not readable' unless blob.readable_text?
84
85        included << filename
86
87        blob
88      end
89
90      # Resolves the given relative path of file in repository into canonical
91      # path based on the specified base_path.
92      #
93      # Examples:
94      #
95      #   # File in the same directory as the current path
96      #   resolve_relative_path("users.adoc", "doc/api/README.adoc")
97      #   # => "doc/api/users.adoc"
98      #
99      #   # File in the same directory, which is also the current path
100      #   resolve_relative_path("users.adoc", "doc/api")
101      #   # => "doc/api/users.adoc"
102      #
103      #   # Going up one level to a different directory
104      #   resolve_relative_path("../update/7.14-to-8.0.adoc", "doc/api/README.adoc")
105      #   # => "doc/update/7.14-to-8.0.adoc"
106      #
107      # Returns a String
108      def resolve_relative_path(path, base_path)
109        p = Pathname(base_path)
110        p = p.dirname unless p.extname.empty?
111        p += path
112
113        p.cleanpath.to_s
114      end
115
116      def current_commit
117        cache[:current_commit] ||= context[:commit] || repository&.commit(ref)
118      end
119
120      def ref
121        context[:ref] || repository&.root_ref
122      end
123
124      def requested_path
125        cache[:requested_path] ||= Addressable::URI.unescape(context[:requested_path])
126      end
127
128      def uri_type(path)
129        cache[:uri_types][path] ||= current_commit&.uri_type(path)
130      end
131    end
132  end
133end
134