discourse/lib/onebox/mixins/git_blob_onebox.rb
Martin Brennan 560e8aff75
FEATURE: Allow oneboxing private GitHub URLs (#27705)
This commit adds the ability to onebox private GitHub
commits, pull requests, issues, blobs, and actions using
a new `github_onebox_access_token` site setting. The token
must be set up in correctly to have access to the repos needed.

To do this successfully with the Oneboxer, we need to skip
redirects on the github.com host, otherwise we get a 404
on the URL before it is translated into a GitHub API URL
and has the appropriate headers added.
2024-07-10 09:39:31 +10:00

248 lines
9.7 KiB
Ruby

# frozen_string_literal: true
module Onebox
module Mixins
module GitBlobOnebox
def self.included(klass)
klass.include(Onebox::Engine)
klass.include(Onebox::LayoutSupport)
klass.matches_regexp(klass.git_regexp)
klass.always_https
klass.include(InstanceMethods)
end
EXPAND_AFTER = 0b001
EXPAND_BEFORE = 0b010
EXPAND_NONE = 0b0
DEFAULTS = {
EXPAND_ONE_LINER: EXPAND_AFTER | EXPAND_BEFORE, #set how to expand a one liner. user EXPAND_NONE to disable expand
LINES_BEFORE: 10,
LINES_AFTER: 10,
SHOW_LINE_NUMBER: true,
MAX_LINES: 20,
MAX_CHARS: 5000,
}
module InstanceMethods
def initialize(url, timeout = nil)
super url, timeout
# merge engine options from global Onebox.options interface
# self.options = Onebox.options["GithubBlobOnebox"] # self.class.name.split("::").last.to_s
# self.options = Onebox.options[self.class.name.split("::").last.to_s] #We can use this a more generic approach. extract the engine class name automatically
self.options = DEFAULTS
@selected_lines_array = nil
@selected_one_liner = 0
@model_file = nil
# Define constant after merging options set in Onebox.options
# We can define constant automatically.
options.each_pair do |constant_name, value|
constant_name_u = constant_name.to_s.upcase
if constant_name_u == constant_name.to_s
#define a constant if not already defined
unless self.class.const_defined? constant_name_u.to_sym
Onebox::Mixins::GitBlobOnebox.const_set constant_name_u.to_sym,
options[constant_name_u.to_sym]
end
end
end
end
private
def calc_range(m, contents_lines_size)
truncated = false
from = /\d+/.match(m[:from]) #get numeric should only match a positive interger
to = /\d+/.match(m[:to]) #get numeric should only match a positive interger
range_provided = !(from.nil? && to.nil?) #true if "from" or "to" provided in URL
from = from.nil? ? 1 : from[0].to_i #if from not provided default to 1st line
to = to.nil? ? -1 : to[0].to_i #if to not provided default to undefiend to be handled later in the logic
if to === -1 && range_provided #case "from" exists but no valid "to". aka ONE_LINER
one_liner = true
to = from
else
one_liner = false
end
unless range_provided #case no range provided default to 1..MAX_LINES
from = 1
to = MAX_LINES
truncated = true if contents_lines_size > MAX_LINES
#we can technically return here
end
from, to = [from, to].sort #enforce valid range. [from < to]
from = 1 if from > contents_lines_size #if "from" out of TOP bound set to 1st line
to = contents_lines_size if to > contents_lines_size #if "to" is out of TOP bound set to last line.
if one_liner
@selected_one_liner = from
if EXPAND_ONE_LINER != EXPAND_NONE
if (EXPAND_ONE_LINER & EXPAND_BEFORE != 0) # check if EXPAND_BEFORE flag is on
from = [1, from - LINES_BEFORE].max # make sure expand before does not go out of bound
end
if (EXPAND_ONE_LINER & EXPAND_AFTER != 0) # check if EXPAND_FLAG flag is on
to = [to + LINES_AFTER, contents_lines_size].min # make sure expand after does not go out of bound
end
from = contents_lines_size if from > contents_lines_size #if "from" is out of the content top bound
# to = contents_lines_size if to > contents_lines_size #if "to" is out of the content top bound
else
#no expand show the one liner solely
end
end
if to - from > MAX_LINES && !one_liner #if exceed the MAX_LINES limit correct unless range was produced by one_liner which it expand setting will allow exceeding the line limit
truncated = true
to = from + MAX_LINES - 1
end
{
from: from, #calculated from
from_minus_one: from - 1, #used for getting currect ol>li numbering with css used in template
to: to, #calculated to
one_liner: one_liner, #boolean if a one-liner
selected_one_liner: @selected_one_liner, #if a one liner is provided we create a reference for it.
range_provided: range_provided, #boolean if range provided
truncated: truncated,
}
end
#minimize/compact leading indentation while preserving overall indentation
def removeLeadingIndentation(str)
min_space = 100
a_lines = str.lines
a_lines.each do |l|
l = l.chomp("\n") # remove new line
m = l.match(/\A[ ]*/) # find leading spaces 0 or more
if m.nil? || l.size == m[0].size || l.size == 0
next # SKIP no match or line is only spaces
else # no match | only spaces in line | empty line
m_str_length = m[0].size
if m_str_length <= 1 # minimum space is 1 or nothing we can break we found our minimum
min_space = m_str_length
break #stop iteration
end
min_space = m_str_length if m_str_length < min_space
end
end
a_lines.each do |l|
re = Regexp.new "^[ ]{#{min_space}}" #match the minimum spaces of the line
l.gsub!(re, "")
end
a_lines.join
end
def line_number_helper(lines, start, selected)
lines = removeLeadingIndentation(lines.join).lines # A little ineffeicent we could modify removeLeadingIndentation to accept array and return array, but for now it is only working with a string
hash_builder = []
output_builder = []
lines.map.with_index do |line, i|
lnum = (i.to_i + start)
hash_builder.push(
line_number: lnum,
data: line.gsub("\n", ""),
selected: (selected == lnum) ? true : false,
)
output_builder.push "#{lnum}: #{line}"
end
{ output: output_builder.join(), array: hash_builder }
end
def raw
return @raw if defined?(@raw)
m = @url.match(self.raw_regexp)
if m
from = /\d+/.match(m[:from]) #get numeric should only match a positive interger
to = /\d+/.match(m[:to]) #get numeric should only match a positive interger
@file = m[:file]
@lang = Onebox::FileTypeFinder.from_file_name(m[:file])
if @lang == "stl" && link.match?(%r{\Ahttps?://(www\.)?github\.com.*/blob/})
@model_file = @lang.dup
@raw = "https://render.githubusercontent.com/view/solid?url=" + self.raw_template(m)
else
contents =
URI
.parse(self.raw_template(m))
.open({ read_timeout: timeout }.merge(self.auth_headers))
.read
if contents.encoding == Encoding::BINARY || contents.bytes.include?(0)
@raw = nil
@binary = true
return
end
contents_lines = contents.lines #get contents lines
contents_lines_size = contents_lines.size #get number of lines
cr = calc_range(m, contents_lines_size) #calculate the range of lines for output
selected_one_liner = cr[:selected_one_liner] #if url is a one-liner calc_range will return it
from = cr[:from]
to = cr[:to]
@truncated = cr[:truncated]
range_provided = cr[:range_provided]
@cr_results = cr
if range_provided #if a range provided (single line or more)
if SHOW_LINE_NUMBER
lines_result =
line_number_helper(
contents_lines[(from - 1)..(to - 1)],
from,
selected_one_liner,
) #print code with prefix line numbers in case range provided
contents = lines_result[:output]
@selected_lines_array = lines_result[:array]
else
contents = contents_lines[(from - 1)..(to - 1)].join()
end
else
contents = contents_lines[(from - 1)..(to - 1)].join()
end
if contents.length > MAX_CHARS #truncate content chars to limits
contents = contents[0..MAX_CHARS]
@truncated = true
end
@raw = contents
end
end
end
def data
@data ||= {
title: title,
link: link,
i18n: i18n,
# IMPORTANT NOTE: All of the other class variables are populated
# as *side effects* of the `raw` method! They must all appear
# AFTER the call to `raw`! Don't get bitten by this like I did!
content: raw,
binary: @binary,
lang: "lang-#{@lang}",
lines: @selected_lines_array,
has_lines: !@selected_lines_array.nil?,
selected_one_liner: @selected_one_liner,
cr_results: @cr_results,
truncated: @truncated,
model_file: @model_file,
width: 480,
height: 360,
}
end
end
end
end
end