# frozen_string_literal: true require 'uri' module Banzai module Filter # HTML filter that "fixes" relative links to files in a repository. # # Context options: # :commit # :current_user # :project # :project_wiki # :ref # :requested_path # :system_note class RepositoryLinkFilter < BaseRelativeLinkFilter def call return doc if context[:system_note] clear_memoization(:linkable_files) clear_memoization(:linkable_attributes) load_uri_types linkable_attributes.each do |attr| if linkable_files? && repo_visible_to_user? process_link_to_repository_attr(attr) end end doc end protected def load_uri_types return unless linkable_attributes.present? return unless linkable_files? return {} unless repository @uri_types = request_path.present? ? get_uri_types([request_path]) : {} paths = linkable_attributes.flat_map do |attr| [get_uri(attr).to_s, relative_file_path(get_uri(attr))] end paths.reject!(&:blank?) paths.uniq! @uri_types.merge!(get_uri_types(paths)) end def linkable_files? strong_memoize(:linkable_files) do context[:project_wiki].nil? && repository.try(:exists?) && !repository.empty? end end def get_uri_types(paths) return {} if paths.empty? uri_types = Hash[paths.collect { |name| [name, nil] }] get_blob_types(paths).each do |name, type| if type == :blob blob = ::Blob.decorate(Gitlab::Git::Blob.new(name: name), project) uri_types[name] = blob.image? || blob.video? || blob.audio? ? :raw : :blob else uri_types[name] = type end end uri_types end def get_blob_types(paths) revision_paths = paths.collect do |path| [current_commit.sha, path.chomp("/")] end Gitlab::GitalyClient::BlobService.new(repository).get_blob_types(revision_paths, 1) rescue GRPC::Unavailable, GRPC::DeadlineExceeded => e # Handle Gitaly connection issues gracefully Gitlab::ErrorTracking.track_exception(e, project_id: project.id) # Return all links as blob types paths.collect do |path| [path, :blob] end end def get_uri(html_attr) uri = URI(html_attr.value) uri if uri.relative? && uri.path.present? rescue URI::Error, Addressable::URI::InvalidURIError end def process_link_to_repository_attr(html_attr) uri = URI(html_attr.value) if uri.relative? && uri.path.present? html_attr.value = rebuild_relative_uri(uri).to_s end rescue URI::Error, Addressable::URI::InvalidURIError # noop end def rebuild_relative_uri(uri) file_path = nested_file_path_if_exists(uri) resource_type = uri_type(file_path) # Repository routes are under /-/ scope now. # Since we craft a path without using route helpers we must # ensure - is added here. prefix = '-' if %w(tree blob raw commits).include?(resource_type.to_s) uri.path = [ relative_url_root, project.full_path, prefix, resource_type, Addressable::URI.escape(ref).gsub('#', '%23'), Addressable::URI.escape(file_path) ].compact.join('/').squeeze('/').chomp('/') uri end def nested_file_path_if_exists(uri) path = cleaned_file_path(uri) nested_path = relative_file_path(uri) file_exists?(nested_path) ? nested_path : path end def cleaned_file_path(uri) unescape_and_scrub_uri(uri.path).delete("\0").chomp("/") end def relative_file_path(uri) return if uri.nil? build_relative_path(cleaned_file_path(uri), request_path) end def request_path return unless context[:requested_path] unescape_and_scrub_uri(context[:requested_path]).chomp("/") end # Convert a relative path into its correct location based on the currently # requested path # # path - Relative path String # request_path - Currently-requested path String # # Examples: # # # File in the same directory as the current path # build_relative_path("users.md", "doc/api/README.md") # # => "doc/api/users.md" # # # File in the same directory, which is also the current path # build_relative_path("users.md", "doc/api") # # => "doc/api/users.md" # # # Going up one level to a different directory # build_relative_path("../update/7.14-to-8.0.md", "doc/api/README.md") # # => "doc/update/7.14-to-8.0.md" # # Returns a String def build_relative_path(path, request_path) return request_path if path.empty? return path unless request_path return path[1..-1] if path.start_with?('/') parts = request_path.split('/') parts.pop if uri_type(request_path) != :tree path.sub!(%r{\A\./}, '') while path.start_with?('../') parts.pop path.sub!('../', '') end parts.push(path).join('/') end def file_exists?(path) path.present? && uri_type(path).present? end def uri_type(path) @uri_types[path] == :unknown ? "" : @uri_types[path] end def current_commit @current_commit ||= context[:commit] || repository.commit(ref) end def repo_visible_to_user? project && Ability.allowed?(current_user, :download_code, project) end def ref context[:ref] || project.default_branch end def current_user context[:current_user] end def repository @repository ||= project&.repository end end end end