BigW Consortium Gitlab

renderer.rb 6.06 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12
module Banzai
  module Renderer
    # Convert a Markdown String into an HTML-safe String of HTML
    #
    # Note that while the returned HTML will have been sanitized of dangerous
    # HTML, it may post a risk of information leakage if it's not also passed
    # through `post_process`.
    #
    # Also note that the returned String is always HTML, not XHTML. Views
    # requiring XHTML, such as Atom feeds, need to call `post_process` on the
    # result, providing the appropriate `pipeline` option.
    #
13
    # text     - Markdown String
14 15 16
    # context  - Hash of context options passed to our HTML Pipeline
    #
    # Returns an HTML-safe String
17
    def self.render(text, context = {})
18 19 20
      cache_key = context.delete(:cache_key)
      cache_key = full_cache_key(cache_key, context[:pipeline])

21
      if cache_key
22 23 24 25
        Gitlab::Metrics.measure(:banzai_cached_render) do
          Rails.cache.fetch(cache_key) do
            cacheless_render(text, context)
          end
26
        end
27 28
      else
        cacheless_render(text, context)
29 30 31
      end
    end

32 33 34 35
    # Convert a Markdown-containing field on an object into an HTML-safe String
    # of HTML. This method is analogous to calling render(object.field), but it
    # can cache the rendered HTML in the object, rather than Redis.
    #
36 37
    # The context to use is managed by the object and cannot be changed.
    # Use #render, passing it the field text, if a custom rendering is needed.
38
    def self.render_field(object, field)
39
      object.refresh_markdown_cache!(do_update: update_object?(object)) unless object.cached_html_up_to_date?(field)
40

41
      object.cached_html_for(field)
42 43 44
    end

    # Same as +render_field+, but without consulting or updating the cache field
45
    def self.cacheless_render_field(object, field, options = {})
46
      text = object.__send__(field)
47
      context = object.banzai_render_context(field).merge(options)
48 49 50 51

      cacheless_render(text, context)
    end

52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74
    # Perform multiple render from an Array of Markdown String into an
    # Array of HTML-safe String of HTML.
    #
    # As the rendered Markdown String can be already cached read all the data
    # from the cache using Rails.cache.read_multi operation. If the Markdown String
    # is not in the cache or it's not cacheable (no cache_key entry is provided in
    # the context) the Markdown String is rendered and stored in the cache so the
    # next render call gets the rendered HTML-safe String from the cache.
    #
    # For further explanation see #render method comments.
    #
    # texts_and_contexts - An Array of Hashes that contains the Markdown String (:text)
    #                      an options passed to our HTML Pipeline (:context)
    #
    # If on the :context you specify a :cache_key entry will be used to retrieve it
    # and cache the result of rendering the Markdown String.
    #
    # Returns an Array containing HTML-safe String instances.
    #
    # Example:
    #    texts_and_contexts
    #    => [{ text: '### Hello',
    #          context: { cache_key: [note, :note] } }]
75
    def self.cache_collection_render(texts_and_contexts)
76 77 78 79 80 81 82 83
      items_collection = texts_and_contexts.each_with_index do |item, index|
        context = item[:context]
        cache_key = full_cache_multi_key(context.delete(:cache_key), context[:pipeline])

        item[:cache_key] = cache_key if cache_key
      end

      cacheable_items, non_cacheable_items = items_collection.partition { |item| item.key?(:cache_key) }
84 85 86 87 88 89 90 91 92 93

      items_in_cache = []
      items_not_in_cache = []

      unless cacheable_items.empty?
        items_in_cache = Rails.cache.read_multi(*cacheable_items.map { |item| item[:cache_key] })
        items_not_in_cache = cacheable_items.reject do |item|
          item[:rendered] = items_in_cache[item[:cache_key]]
          items_in_cache.key?(item[:cache_key])
        end
94 95 96 97 98 99 100 101 102 103
      end

      (items_not_in_cache + non_cacheable_items).each do |item|
        item[:rendered] = render(item[:text], item[:context])
        Rails.cache.write(item[:cache_key], item[:rendered]) if item[:cache_key]
      end

      items_collection.map { |item| item[:rendered] }
    end

104
    def self.render_result(text, context = {})
105
      text = Pipeline[:pre_process].to_html(text, context) if text
106

107
      Pipeline[context[:pipeline]].call(text, context)
108 109
    end

110 111 112 113 114 115 116 117 118 119 120 121 122
    # Perform post-processing on an HTML String
    #
    # This method is used to perform state-dependent changes to a String of
    # HTML, such as removing references that the current user doesn't have
    # permission to make (`RedactorFilter`).
    #
    # html     - String to process
    # context  - Hash of options to customize output
    #            :pipeline  - Symbol pipeline type
    #            :project   - Project
    #            :user      - User object
    #
    # Returns an HTML-safe String
123
    def self.post_process(html, context)
124 125 126 127 128 129 130 131 132 133
      context = Pipeline[context[:pipeline]].transform_context(context)

      pipeline = Pipeline[:post_process]
      if context[:xhtml]
        pipeline.to_document(html, context).to_html(save_with: Nokogiri::XML::Node::SaveOptions::AS_XHTML)
      else
        pipeline.to_html(html, context)
      end.html_safe
    end

134
    def self.cacheless_render(text, context = {})
135 136
      Gitlab::Metrics.measure(:banzai_cacheless_render) do
        result = render_result(text, context)
137

138 139 140 141 142 143
        output = result[:output]
        if output.respond_to?(:to_html)
          output.to_html
        else
          output.to_s
        end
144 145 146
      end
    end

147
    def self.full_cache_key(cache_key, pipeline_name)
148 149 150
      return unless cache_key
      ["banzai", *cache_key, pipeline_name || :full]
    end
151 152 153 154

    # To map Rails.cache.read_multi results we need to know the Rails.cache.expanded_key.
    # Other option will be to generate stringified keys on our side and don't delegate to Rails.cache.expanded_key
    # method.
155
    def self.full_cache_multi_key(cache_key, pipeline_name)
156 157 158
      return unless cache_key
      Rails.cache.send(:expanded_key, full_cache_key(cache_key, pipeline_name))
    end
159

160 161 162
    # GitLab EE needs to disable updates on GET requests in Geo
    def self.update_object?(object)
      true
163
    end
164 165
  end
end