From c0dfaf98ac60fc928a2df5f7f355b262f0fcaf91 Mon Sep 17 00:00:00 2001 From: Reuben Pereira Date: Sat, 30 Jun 2018 14:47:03 +0530 Subject: A working implementation of a project reference filter which links project references to project profile. --- lib/banzai/filter/project_reference_filter.rb | 114 ++++++++++++++++++++++++++ lib/banzai/pipeline/gfm_pipeline.rb | 1 + lib/banzai/reference_parser/project_parser.rb | 17 ++++ 3 files changed, 132 insertions(+) create mode 100644 lib/banzai/filter/project_reference_filter.rb create mode 100644 lib/banzai/reference_parser/project_parser.rb (limited to 'lib/banzai') diff --git a/lib/banzai/filter/project_reference_filter.rb b/lib/banzai/filter/project_reference_filter.rb new file mode 100644 index 00000000000..ed69f7eae92 --- /dev/null +++ b/lib/banzai/filter/project_reference_filter.rb @@ -0,0 +1,114 @@ +module Banzai + module Filter + # HTML filter that replaces project references with links. + class ProjectReferenceFilter < ReferenceFilter + self.reference_type = :project + + # Public: Find `project>` project references in text + # + # ProjectReferenceFilter.references_in(text) do |match, project| + # "#{project}>" + # end + # + # text - String text to search. + # + # Yields the String match, and the String project name. + # + # Returns a String replaced with the return of the block. + def self.references_in(text) + text.gsub(Project.markdown_reference_pattern) do |match| + yield match, "#{$~[:namespace]}/#{$~[:project]}" + end + end + + def call + ref_pattern = Project.markdown_reference_pattern + ref_pattern_start = /\A#{ref_pattern}\z/ + + nodes.each do |node| + if text_node?(node) + replace_text_when_pattern_matches(node, ref_pattern) do |content| + project_link_filter(content) + end + elsif element_node?(node) + yield_valid_link(node) do |link, inner_html| + if link =~ ref_pattern_start + replace_link_node_with_href(node, link) do + project_link_filter(link, link_content: inner_html) + end + end + end + end + end + + doc + end + + # Replace `project>` project references in text with links to the referenced + # project page. + # + # text - String text to replace references in. + # link_content - Original content of the link being replaced. + # + # Returns a String with `project>` references replaced with links. All links + # have `gfm` and `gfm-project` class names attached for styling. + def project_link_filter(text, link_content: nil) + self.class.references_in(text) do |match, project_name| + cached_call(:banzai_url_for_object, match, path: [Project, project_name.downcase]) do + if project = projects_hash[project_name.downcase] + link_to_project(project, link_content: link_content) || match + else + match + end + end + end + end + + # Returns a Hash containing all Namespace objects for the project + # references in the current document. + # + # The keys of this Hash are the namespace paths, the values the + # corresponding Namespace objects. + def projects_hash + @projects ||= Project.where_full_path_in(projects) + .index_by(&:full_path) + .transform_keys(&:downcase) + end + + # Returns all projects referenced in the current document. + def projects + refs = Set.new + + nodes.each do |node| + node.to_html.scan(Project.markdown_reference_pattern) do + refs << "#{$~[:namespace]}/#{$~[:project]}" + end + end + + refs.to_a + end + + private + + def urls + Gitlab::Routing.url_helpers + end + + def link_class + reference_class(:project) + end + + def link_to_project(project, link_content: nil) + url = urls.project_url(project, only_path: context[:only_path]) + data = data_attribute(project: project.id) + content = link_content || project.full_path + Project.reference_postfix + + link_tag(url, data, content, project.name) + end + + def link_tag(url, data, link_content, title) + %(#{link_content}) + end + end + end +end diff --git a/lib/banzai/pipeline/gfm_pipeline.rb b/lib/banzai/pipeline/gfm_pipeline.rb index 0d9b874ef85..9295ca9efcc 100644 --- a/lib/banzai/pipeline/gfm_pipeline.rb +++ b/lib/banzai/pipeline/gfm_pipeline.rb @@ -25,6 +25,7 @@ module Banzai Filter::ExternalLinkFilter, Filter::UserReferenceFilter, + Filter::ProjectReferenceFilter, Filter::IssueReferenceFilter, Filter::ExternalIssueReferenceFilter, Filter::MergeRequestReferenceFilter, diff --git a/lib/banzai/reference_parser/project_parser.rb b/lib/banzai/reference_parser/project_parser.rb new file mode 100644 index 00000000000..54fd3c38a85 --- /dev/null +++ b/lib/banzai/reference_parser/project_parser.rb @@ -0,0 +1,17 @@ +module Banzai + module ReferenceParser + class ProjectParser < BaseParser + self.reference_type = :project + + def references_relation + Project + end + + private + + def can_read_reference?(user, ref_project, node) + can?(user, :read_project, ref_project) + end + end + end +end -- cgit v1.2.3 From 28cf16a772d359cac3eb589553b579cdc2de6ca9 Mon Sep 17 00:00:00 2001 From: Reuben Pereira Date: Sat, 30 Jun 2018 18:49:07 +0530 Subject: Correct the comment above the projects_hash function in the project_reference_filter --- lib/banzai/filter/project_reference_filter.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'lib/banzai') diff --git a/lib/banzai/filter/project_reference_filter.rb b/lib/banzai/filter/project_reference_filter.rb index ed69f7eae92..4a162733438 100644 --- a/lib/banzai/filter/project_reference_filter.rb +++ b/lib/banzai/filter/project_reference_filter.rb @@ -64,11 +64,11 @@ module Banzai end end - # Returns a Hash containing all Namespace objects for the project + # Returns a Hash containing all Project objects for the project # references in the current document. # - # The keys of this Hash are the namespace paths, the values the - # corresponding Namespace objects. + # The keys of this Hash are the project paths, the values the + # corresponding Project objects. def projects_hash @projects ||= Project.where_full_path_in(projects) .index_by(&:full_path) -- cgit v1.2.3 From 07ac9ee2f0861135e6cd514e956e6eb7019e7096 Mon Sep 17 00:00:00 2001 From: Reuben Pereira Date: Tue, 3 Jul 2018 01:09:39 +0530 Subject: Correct the misleading name of a variable in ProjectReferenceFilter.project_link_filter --- lib/banzai/filter/project_reference_filter.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'lib/banzai') diff --git a/lib/banzai/filter/project_reference_filter.rb b/lib/banzai/filter/project_reference_filter.rb index 4a162733438..8bc4ceb92cd 100644 --- a/lib/banzai/filter/project_reference_filter.rb +++ b/lib/banzai/filter/project_reference_filter.rb @@ -53,9 +53,9 @@ module Banzai # Returns a String with `project>` references replaced with links. All links # have `gfm` and `gfm-project` class names attached for styling. def project_link_filter(text, link_content: nil) - self.class.references_in(text) do |match, project_name| - cached_call(:banzai_url_for_object, match, path: [Project, project_name.downcase]) do - if project = projects_hash[project_name.downcase] + self.class.references_in(text) do |match, project_path| + cached_call(:banzai_url_for_object, match, path: [Project, project_path.downcase]) do + if project = projects_hash[project_path.downcase] link_to_project(project, link_content: link_content) || match else match -- cgit v1.2.3 From c8302b2f50efad5c159fb9e01598259c80887dac Mon Sep 17 00:00:00 2001 From: Reuben Pereira Date: Tue, 3 Jul 2018 10:15:56 +0530 Subject: Correct the comments above the ProjectReferenceFilter#project_link_filter function to make it clear that the full project path is required in a reference. --- lib/banzai/filter/project_reference_filter.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'lib/banzai') diff --git a/lib/banzai/filter/project_reference_filter.rb b/lib/banzai/filter/project_reference_filter.rb index 8bc4ceb92cd..6f257a2d70d 100644 --- a/lib/banzai/filter/project_reference_filter.rb +++ b/lib/banzai/filter/project_reference_filter.rb @@ -44,13 +44,13 @@ module Banzai doc end - # Replace `project>` project references in text with links to the referenced + # Replace `namespace/project>` project references in text with links to the referenced # project page. # # text - String text to replace references in. # link_content - Original content of the link being replaced. # - # Returns a String with `project>` references replaced with links. All links + # Returns a String with `namespace/project>` references replaced with links. All links # have `gfm` and `gfm-project` class names attached for styling. def project_link_filter(text, link_content: nil) self.class.references_in(text) do |match, project_path| -- cgit v1.2.3 From 972f12218e6954e183d9654775c4cbedcc97d2eb Mon Sep 17 00:00:00 2001 From: Reuben Pereira Date: Tue, 3 Jul 2018 10:18:49 +0530 Subject: Eager load the namespace and route of a project in project_reference_filter.rb --- lib/banzai/filter/project_reference_filter.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'lib/banzai') diff --git a/lib/banzai/filter/project_reference_filter.rb b/lib/banzai/filter/project_reference_filter.rb index 6f257a2d70d..6eead2ab1c3 100644 --- a/lib/banzai/filter/project_reference_filter.rb +++ b/lib/banzai/filter/project_reference_filter.rb @@ -70,7 +70,8 @@ module Banzai # The keys of this Hash are the project paths, the values the # corresponding Project objects. def projects_hash - @projects ||= Project.where_full_path_in(projects) + @projects ||= Project.eager_load(:namespace, :route) + .where_full_path_in(projects) .index_by(&:full_path) .transform_keys(&:downcase) end -- cgit v1.2.3 From 7429eb3b48515d05d3840b6ba8ca68581eeb5ebe Mon Sep 17 00:00:00 2001 From: Reuben Pereira Date: Tue, 3 Jul 2018 21:26:28 +0530 Subject: Eager load a project's route and its namespace with route in project_reference_filter --- lib/banzai/filter/project_reference_filter.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'lib/banzai') diff --git a/lib/banzai/filter/project_reference_filter.rb b/lib/banzai/filter/project_reference_filter.rb index 6eead2ab1c3..9e680d25b50 100644 --- a/lib/banzai/filter/project_reference_filter.rb +++ b/lib/banzai/filter/project_reference_filter.rb @@ -70,7 +70,7 @@ module Banzai # The keys of this Hash are the project paths, the values the # corresponding Project objects. def projects_hash - @projects ||= Project.eager_load(:namespace, :route) + @projects ||= Project.eager_load(:route, namespace: [:route]) .where_full_path_in(projects) .index_by(&:full_path) .transform_keys(&:downcase) -- cgit v1.2.3 From d6f4810ea154d2f2303591ac37d616c8249fca9d Mon Sep 17 00:00:00 2001 From: Reuben Pereira Date: Tue, 3 Jul 2018 22:24:59 +0530 Subject: Correct the comment above the ProjectReferenceFilter.references_in method. --- lib/banzai/filter/project_reference_filter.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'lib/banzai') diff --git a/lib/banzai/filter/project_reference_filter.rb b/lib/banzai/filter/project_reference_filter.rb index 9e680d25b50..9764cfa4ef5 100644 --- a/lib/banzai/filter/project_reference_filter.rb +++ b/lib/banzai/filter/project_reference_filter.rb @@ -4,7 +4,7 @@ module Banzai class ProjectReferenceFilter < ReferenceFilter self.reference_type = :project - # Public: Find `project>` project references in text + # Public: Find `namespace/project>` project references in text # # ProjectReferenceFilter.references_in(text) do |match, project| # "#{project}>" -- cgit v1.2.3 From 2730ae1d869af4ddd48dc312d230c1bcafec19b5 Mon Sep 17 00:00:00 2001 From: Reuben Pereira Date: Thu, 5 Jul 2018 01:26:09 +0530 Subject: Use a custom ProjectParser#nodes_visible_to_user function so that the user permissions for all project references can be checked together --- lib/banzai/reference_parser/project_parser.rb | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) (limited to 'lib/banzai') diff --git a/lib/banzai/reference_parser/project_parser.rb b/lib/banzai/reference_parser/project_parser.rb index 54fd3c38a85..787cb671b2d 100644 --- a/lib/banzai/reference_parser/project_parser.rb +++ b/lib/banzai/reference_parser/project_parser.rb @@ -7,10 +7,29 @@ module Banzai Project end + def nodes_visible_to_user(user, nodes) + nodes_projects_hash = lazy { projects_for_nodes(nodes) } + project_attr = 'data-project' + + readable_project_ids = projects_readable_by_user(nodes_projects_hash.values, user) + + nodes.select do |node| + if node.has_attribute?(project_attr) + readable_project_ids.include?(nodes_projects_hash[node].try(:id)) + else + true + end + end + end + private - def can_read_reference?(user, ref_project, node) - can?(user, :read_project, ref_project) + # Returns an Array of Project ids that can be read by the given user. + # + # projects - The projects to reduce down to those readable by the user. + # user - The User for which to check the projects + def projects_readable_by_user(projects, user) + Project.public_or_visible_to_user(user).where("projects.id IN (?)", projects.collect(&:id)).pluck(:id) end end end -- cgit v1.2.3 From 74c81cc50e7d4deff11e9777b9522f8adf8b3c96 Mon Sep 17 00:00:00 2001 From: Reuben Pereira Date: Thu, 5 Jul 2018 01:55:55 +0530 Subject: Add back the Project#to_reference_with_postfix function since it can be used in the ProjectReferenceFilter#link_to_project function --- lib/banzai/filter/project_reference_filter.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'lib/banzai') diff --git a/lib/banzai/filter/project_reference_filter.rb b/lib/banzai/filter/project_reference_filter.rb index 9764cfa4ef5..fd2a86a6d45 100644 --- a/lib/banzai/filter/project_reference_filter.rb +++ b/lib/banzai/filter/project_reference_filter.rb @@ -102,7 +102,7 @@ module Banzai def link_to_project(project, link_content: nil) url = urls.project_url(project, only_path: context[:only_path]) data = data_attribute(project: project.id) - content = link_content || project.full_path + Project.reference_postfix + content = link_content || project.to_reference_with_postfix link_tag(url, data, content, project.name) end -- cgit v1.2.3 From a08b5144fe64cf7afde116334c91e95f54c9f4fb Mon Sep 17 00:00:00 2001 From: Reuben Pereira Date: Thu, 5 Jul 2018 21:13:46 +0530 Subject: Use map instead of collect in the ProjectParser#projects_readable_by_user function --- lib/banzai/reference_parser/project_parser.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'lib/banzai') diff --git a/lib/banzai/reference_parser/project_parser.rb b/lib/banzai/reference_parser/project_parser.rb index 787cb671b2d..2bf02e3df53 100644 --- a/lib/banzai/reference_parser/project_parser.rb +++ b/lib/banzai/reference_parser/project_parser.rb @@ -29,7 +29,7 @@ module Banzai # projects - The projects to reduce down to those readable by the user. # user - The User for which to check the projects def projects_readable_by_user(projects, user) - Project.public_or_visible_to_user(user).where("projects.id IN (?)", projects.collect(&:id)).pluck(:id) + Project.public_or_visible_to_user(user).where("projects.id IN (?)", projects.map(&:id)).pluck(:id) end end end -- cgit v1.2.3 From 8cc646fa0ec05820c83a163df74605f04c8877ea Mon Sep 17 00:00:00 2001 From: Reuben Pereira Date: Sun, 15 Jul 2018 17:25:04 +0530 Subject: Remove nodes_visible_to_user from ProjectParser and memoize readable_project_ids --- lib/banzai/reference_parser/project_parser.rb | 27 ++++++++++----------------- 1 file changed, 10 insertions(+), 17 deletions(-) (limited to 'lib/banzai') diff --git a/lib/banzai/reference_parser/project_parser.rb b/lib/banzai/reference_parser/project_parser.rb index 2bf02e3df53..a329af42681 100644 --- a/lib/banzai/reference_parser/project_parser.rb +++ b/lib/banzai/reference_parser/project_parser.rb @@ -1,35 +1,28 @@ module Banzai module ReferenceParser class ProjectParser < BaseParser + include Gitlab::Utils::StrongMemoize + self.reference_type = :project def references_relation Project end - def nodes_visible_to_user(user, nodes) - nodes_projects_hash = lazy { projects_for_nodes(nodes) } - project_attr = 'data-project' - - readable_project_ids = projects_readable_by_user(nodes_projects_hash.values, user) - - nodes.select do |node| - if node.has_attribute?(project_attr) - readable_project_ids.include?(nodes_projects_hash[node].try(:id)) - else - true - end - end - end - private # Returns an Array of Project ids that can be read by the given user. # # projects - The projects to reduce down to those readable by the user. # user - The User for which to check the projects - def projects_readable_by_user(projects, user) - Project.public_or_visible_to_user(user).where("projects.id IN (?)", projects.map(&:id)).pluck(:id) + def readable_project_ids_for(projects, user) + strong_memoize(:readable_project_ids_for) do + Project.public_or_visible_to_user(user).where("projects.id IN (?)", projects.map(&:id)).pluck(:id) + end + end + + def can_read_reference?(user, ref_project, node) + readable_project_ids_for(@projects_for_nodes.values, user).include?(ref_project.try(:id)) end end end -- cgit v1.2.3 From 34e912b538b54619920b714b5177798597758808 Mon Sep 17 00:00:00 2001 From: Reuben Pereira Date: Tue, 31 Jul 2018 00:48:59 +0530 Subject: Use a hash to memoize readable_project_ids with user objects as keys --- lib/banzai/reference_parser/project_parser.rb | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) (limited to 'lib/banzai') diff --git a/lib/banzai/reference_parser/project_parser.rb b/lib/banzai/reference_parser/project_parser.rb index a329af42681..2a33b00ddbd 100644 --- a/lib/banzai/reference_parser/project_parser.rb +++ b/lib/banzai/reference_parser/project_parser.rb @@ -13,16 +13,15 @@ module Banzai # Returns an Array of Project ids that can be read by the given user. # - # projects - The projects to reduce down to those readable by the user. # user - The User for which to check the projects - def readable_project_ids_for(projects, user) - strong_memoize(:readable_project_ids_for) do - Project.public_or_visible_to_user(user).where("projects.id IN (?)", projects.map(&:id)).pluck(:id) - end + def readable_project_ids_for(user) + @project_ids_by_user ||= {} + @project_ids_by_user[user] ||= + Project.public_or_visible_to_user(user).where("projects.id IN (?)", @projects_for_nodes.values.map(&:id)).pluck(:id) end def can_read_reference?(user, ref_project, node) - readable_project_ids_for(@projects_for_nodes.values, user).include?(ref_project.try(:id)) + readable_project_ids_for(user).include?(ref_project.try(:id)) end end end -- cgit v1.2.3