Module: CookedProcessorMixin

Included in:
CookedPostProcessor
Defined in:
lib/cooked_processor_mixin.rb

Instance Method Summary collapse

Instance Method Details

#add_blocked_hotlinked_image_placeholder!(el) ⇒ Object



270
271
272
273
274
275
276
277
278
279
# File 'lib/cooked_processor_mixin.rb', line 270

def add_blocked_hotlinked_image_placeholder!(el)
  el.name = "a"
  el.set_attribute("href", el[PrettyText::BLOCKED_HOTLINKED_SRC_ATTR])
  el.set_attribute("class", "blocked-hotlinked-placeholder")
  el.set_attribute("title", I18n.t("post.image_placeholder.blocked_hotlinked_title"))
  el << "<svg class=\"fa d-icon d-icon-link svg-icon\" aria-hidden=\"true\"><use href=\"#link\"></use></svg>"
  el << "<span class=\"notice\">#{CGI.escapeHTML(I18n.t("post.image_placeholder.blocked_hotlinked"))}</span>"

  true
end

#add_blocked_hotlinked_media_placeholder!(el, src) ⇒ Object



281
282
283
284
285
286
287
288
289
290
291
292
293
# File 'lib/cooked_processor_mixin.rb', line 281

def add_blocked_hotlinked_media_placeholder!(el, src)
  placeholder = Nokogiri::XML::Node.new("a", el.document)
  placeholder.name = "a"
  placeholder.set_attribute("href", src)
  placeholder.set_attribute("class", "blocked-hotlinked-placeholder")
  placeholder.set_attribute("title", I18n.t("post.media_placeholder.blocked_hotlinked_title"))
  placeholder << "<svg class=\"fa d-icon d-icon-link svg-icon\" aria-hidden=\"true\"><use href=\"#link\"></use></svg>"
  placeholder << "<span class=\"notice\">#{CGI.escapeHTML(I18n.t("post.media_placeholder.blocked_hotlinked"))}</span>"

  el.replace(placeholder)

  true
end

#add_broken_image_placeholder!(img) ⇒ Object



259
260
261
262
263
264
265
266
267
268
# File 'lib/cooked_processor_mixin.rb', line 259

def add_broken_image_placeholder!(img)
  img.name = "span"
  img.set_attribute("class", "broken-image")
  img.set_attribute("title", I18n.t("post.image_placeholder.broken"))
  img << "<svg class=\"fa d-icon d-icon-unlink svg-icon\" aria-hidden=\"true\"><use href=\"#unlink\"></use></svg>"
  img.remove_attribute("src")
  img.remove_attribute("width")
  img.remove_attribute("height")
  true
end

#add_large_image_placeholder!(img) ⇒ Object



208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
# File 'lib/cooked_processor_mixin.rb', line 208

def add_large_image_placeholder!(img)
  url = img["src"]

  is_hyperlinked = is_a_hyperlink?(img)

  placeholder = create_node("div", "large-image-placeholder")
  img.add_next_sibling(placeholder)
  placeholder.add_child(img)

  a = create_link_node(nil, url, true)
  img.add_next_sibling(a)

  span = create_span_node("url", url)
  a.add_child(span)
  span.add_previous_sibling(create_icon_node("far-image"))
  span.add_next_sibling(
    create_span_node(
      "help",
      I18n.t(
        "upload.placeholders.too_large_humanized",
        max_size:
          ActiveSupport::NumberHelper.number_to_human_size(
            SiteSetting.max_image_size_kb.kilobytes,
          ),
      ),
    ),
  )

  # Only if the image is already linked
  if is_hyperlinked
    parent = placeholder.parent
    parent.add_next_sibling(placeholder)

    if parent.name == "a" && parent["href"].present?
      if url == parent["href"]
        parent.remove
      else
        parent["class"] = "link"
        a.add_previous_sibling(parent)

        lspan = create_span_node("url", parent["href"])
        parent.add_child(lspan)
        lspan.add_previous_sibling(create_icon_node("link"))
      end
    end
  end

  img.remove
  true
end

#add_to_size_cache(url, w, h) ⇒ Object



169
170
171
# File 'lib/cooked_processor_mixin.rb', line 169

def add_to_size_cache(url, w, h)
  @size_cache[url] = [w, h]
end

#create_icon_node(klass) ⇒ Object



347
348
349
350
351
# File 'lib/cooked_processor_mixin.rb', line 347

def create_icon_node(klass)
  icon = create_node("svg", "fa d-icon d-icon-#{klass} svg-icon")
  icon.set_attribute("aria-hidden", "true")
  icon << "<use href=\"##{klass}\"></use>"
end


337
338
339
340
341
342
343
344
345
# File 'lib/cooked_processor_mixin.rb', line 337

def create_link_node(klass, url, external = false)
  a = create_node("a", klass)
  a["href"] = url
  if external
    a["target"] = "_blank"
    a["rel"] = "nofollow noopener"
  end
  a
end

#create_node(tag_name, klass) ⇒ Object



353
354
355
356
357
358
# File 'lib/cooked_processor_mixin.rb', line 353

def create_node(tag_name, klass)
  node = @doc.document.create_element(tag_name)
  node["class"] = klass if klass.present?
  @doc.add_child(node)
  node
end

#create_span_node(klass, content = nil) ⇒ Object



360
361
362
363
364
# File 'lib/cooked_processor_mixin.rb', line 360

def create_span_node(klass, content = nil)
  span = create_node("span", klass)
  span.content = content if content
  span
end

#dirty?Boolean

Returns:

  • (Boolean)


329
330
331
# File 'lib/cooked_processor_mixin.rb', line 329

def dirty?
  @previous_cooked != html
end

#get_size(url) ⇒ Object



173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
# File 'lib/cooked_processor_mixin.rb', line 173

def get_size(url)
  return @size_cache[url] if @size_cache.has_key?(url)

  absolute_url = url
  absolute_url = Discourse.base_url_no_prefix + absolute_url if absolute_url =~ %r{\A/[^/]}

  return unless absolute_url

  # FastImage fails when there's no scheme
  absolute_url = SiteSetting.scheme + ":" + absolute_url if absolute_url.start_with?("//")

  # we can't direct FastImage to our secure-uploads url because it bounces
  # anonymous requests with a 404 error
  if url && Upload.secure_uploads_url?(url)
    absolute_url = Upload.signed_url_from_secure_uploads_url(absolute_url)
  end

  return unless is_valid_image_url?(absolute_url)

  upload = Upload.get_from_url(absolute_url)
  if upload && upload.width && upload.width > 0
    @size_cache[url] = [upload.width, upload.height]
  else
    @size_cache[url] = FinalDestination::FastImage.size(absolute_url)
  end
rescue Zlib::BufError, URI::Error, OpenSSL::SSL::SSLError
  # FastImage.size raises BufError for some gifs, leave it.
end

#get_size_from_attributes(img) ⇒ Object



136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
# File 'lib/cooked_processor_mixin.rb', line 136

def get_size_from_attributes(img)
  w, h = img["width"].to_i, img["height"].to_i
  return w, h if w > 0 && h > 0
  # if only width or height are specified attempt to scale image
  if w > 0 || h > 0
    w = w.to_f
    h = h.to_f

    return unless original_image_size = get_size(img["src"])
    original_width, original_height = original_image_size.map(&:to_f)

    if w > 0
      ratio = w / original_width
      [w.floor, (original_height * ratio).floor]
    else
      ratio = h / original_height
      [(original_width * ratio).floor, h.floor]
    end
  end
end

#get_size_from_image_sizes(src, image_sizes) ⇒ Object



157
158
159
160
161
162
163
164
165
166
167
# File 'lib/cooked_processor_mixin.rb', line 157

def get_size_from_image_sizes(src, image_sizes)
  return unless image_sizes.present?
  image_sizes.each do |image_size|
    url, size = image_size[0], image_size[1]
    if url && src && url.include?(src) && size && size["width"].to_i > 0 &&
         size["height"].to_i > 0
      return size["width"], size["height"]
    end
  end
  nil
end

#htmlObject



333
334
335
# File 'lib/cooked_processor_mixin.rb', line 333

def html
  @doc.try(:to_html)
end

#is_a_hyperlink?(img) ⇒ Boolean

Returns:

  • (Boolean)


299
300
301
302
303
304
305
306
# File 'lib/cooked_processor_mixin.rb', line 299

def is_a_hyperlink?(img)
  parent = img.parent
  while parent
    return true if parent.name == "a"
    parent = parent.parent if parent.respond_to?(:parent)
  end
  false
end

#is_valid_image_url?(url) ⇒ Boolean

Returns:

  • (Boolean)


202
203
204
205
206
# File 'lib/cooked_processor_mixin.rb', line 202

def is_valid_image_url?(url)
  uri = URI.parse(url)
  %w[http https].include? uri.scheme
rescue URI::Error
end

#limit_size!(img) ⇒ Object



123
124
125
126
127
128
129
130
131
132
133
134
# File 'lib/cooked_processor_mixin.rb', line 123

def limit_size!(img)
  # retrieve the size from
  #  1) the width/height attributes
  #  2) the dimension from the preview (image_sizes)
  #  3) the dimension of the original image (HTTP request)
  w, h =
    get_size_from_attributes(img) || get_size_from_image_sizes(img["src"], @opts[:image_sizes]) ||
      get_size(img["src"])

  # limit the size of the thumbnail
  img["width"], img["height"] = ImageSizer.resize(w, h)
end

#oneboxed_imagesObject



295
296
297
# File 'lib/cooked_processor_mixin.rb', line 295

def oneboxed_images
  @doc.css(".onebox-body img, .onebox img, img.onebox")
end

#post_process_oneboxesObject



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
# File 'lib/cooked_processor_mixin.rb', line 4

def post_process_oneboxes
  limit = SiteSetting.max_oneboxes_per_post - @doc.css("aside.onebox, a.inline-onebox").size
  oneboxes = {}
  inlineOneboxes = {}

  Oneboxer.apply(@doc, extra_paths: [".inline-onebox-loading"]) do |url, element|
    is_onebox = element["class"] == Oneboxer::ONEBOX_CSS_CLASS
    map = is_onebox ? oneboxes : inlineOneboxes
    skip_onebox = limit <= 0 && !map[url]

    if skip_onebox
      if is_onebox
        element.remove_class("onebox")
      else
        remove_inline_onebox_loading_class(element)
      end

      next
    end

    limit -= 1
    map[url] = true

    if is_onebox
      onebox =
        Oneboxer.onebox(
          url,
          invalidate_oneboxes: !!@opts[:invalidate_oneboxes],
          user_id: @model&.user_id,
          category_id: @category_id,
        )

      @has_oneboxes = true if onebox.present?
      onebox
    else
      process_inline_onebox(element)
      false
    end
  end

  PrettyText.sanitize_hotlinked_media(@doc)

  oneboxed_images.each do |img|
    next if img["src"].blank?

    parent = img.parent

    if respond_to?(:process_hotlinked_image, true)
      still_an_image = process_hotlinked_image(img)
      next if !still_an_image
    end

    # make sure we grab dimensions for oneboxed images
    # and wrap in a div
    limit_size!(img)

    next if img["class"]&.include?("onebox-avatar")

    parent = parent&.parent if parent&.name == "a"
    parent_class = parent && parent["class"]
    width = img["width"].to_i
    height = img["height"].to_i

    if parent_class&.include?("onebox-body") && width > 0 && height > 0
      # special instruction for width == height, assume we are dealing with an avatar
      if (img["width"].to_i == img["height"].to_i)
        found = false
        parent = img
        while parent = parent.parent
          if parent["class"] && parent["class"].match?(/\b(allowlistedgeneric|discoursetopic)\b/)
            found = true
            break
          end
        end

        if found
          img["class"] = img["class"].to_s + " onebox-avatar"
          next
        end
      end

      if width < 64 && height < 64
        img["class"] = img["class"].to_s + " onebox-full-image"
      else
        img.delete("width")
        img.delete("height")
        new_parent =
          img.add_next_sibling(
            "<div class='aspect-image' style='--aspect-ratio:#{width}/#{height};'/>",
          )
        new_parent.first.add_child(img)
      end
    elsif (
          parent_class&.include?("instagram-images") || parent_class&.include?("tweet-images") ||
            parent_class&.include?("scale-images")
        ) && width > 0 && height > 0
      img.remove_attribute("width")
      img.remove_attribute("height")
      parent["class"] = "aspect-image-full-size"
      parent["style"] = "--aspect-ratio:#{width}/#{height};"
    end
  end

  if @omit_nofollow || !SiteSetting.add_rel_nofollow_to_user_content
    @doc
      .css(".onebox-body a[rel], .onebox a[rel]")
      .each do |a|
        rel_values = a["rel"].split(" ").map(&:downcase)
        rel_values.delete("nofollow")
        rel_values.delete("ugc")
        if rel_values.blank?
          a.remove_attribute("rel")
        else
          a["rel"] = rel_values.join(" ")
        end
      end
  end
end

#process_inline_onebox(element) ⇒ Object



308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
# File 'lib/cooked_processor_mixin.rb', line 308

def process_inline_onebox(element)
  inline_onebox =
    InlineOneboxer.lookup(
      element.attributes["href"].value,
      invalidate: !!@opts[:invalidate_oneboxes],
      user_id: @model&.user_id,
      category_id: @category_id,
    )

  if title = inline_onebox&.dig(:title)
    element.children = CGI.escapeHTML(title)
    element.add_class("inline-onebox")
  end

  remove_inline_onebox_loading_class(element)
end

#remove_inline_onebox_loading_class(element) ⇒ Object



325
326
327
# File 'lib/cooked_processor_mixin.rb', line 325

def remove_inline_onebox_loading_class(element)
  element.remove_class("inline-onebox-loading")
end