Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions app/controllers/folder_share_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ def show
@folder = fetch_folder_by_token
authorize share_data, policy_class: SharePolicy

search_results = FolderSignService.new(search:, relation: policy_scope(Sign), folder: @folder).process
@signs = search_results.data
@page = search_results.support

render "folders/show"
end

Expand Down Expand Up @@ -51,4 +55,8 @@ def folder_id
def share_token
params[:token]
end

def search
@search ||= Search.new(params.permit(:page, :sort))
end
end
8 changes: 8 additions & 0 deletions app/controllers/folders_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ def index

def show
@folder = policy_scope(Folder).find(id)
search_results = FolderSignService.new(search:, relation: policy_scope(Sign), folder: @folder).process
@signs = search_results.data
@page = search_results.support

authorize @folder
render :show
end
Expand Down Expand Up @@ -69,4 +73,8 @@ def redirect_after_save
def id
params[:id]
end

def search
@search ||= Search.new(params.permit(:page, :sort))
end
end
19 changes: 17 additions & 2 deletions app/controllers/topics_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,30 @@ def index

def show
@topic = policy_scope(Topic).find(params[:id])
@signs = policy_scope(@topic.signs)

search_results = TopicSignService.new(search:, relation: policy_scope(Sign), topic: @topic).process
@signs = search_results.data
@page = search_results.support

authorize @topic
end

def uncategorised
# "uncategorised" isn't a topic
# it references signs that LACK a topic
@topic = Topic.new(name: Topic::NO_TOPIC_DESCRIPTION)
search_results ||= TopicSignService.new(search:, relation: policy_scope(Sign), topic: @topic).process
@signs = search_results.data
@page = search_results.support

authorize @topic, :show?
@signs = policy_scope(Sign).uncategorised

render :show
end

private

def search
@search ||= Search.new(params.permit(:page, :sort))
end
end
46 changes: 46 additions & 0 deletions app/services/folder_sign_service.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# frozen_string_literal: true

require "./lib/sql/status"

class FolderSignService
def initialize(search:, relation:, folder:)
@search = search
@relation = relation
@folder = folder
end

def process
results = SearchResults.new
results.data = build_results
results.support = @search.page_with_total
results
end

private

def build_results
sql_arr = [SQL::Status.all_signs(@search.order_clause)]
result_ids = parse_results(exec_query(sql_arr))

result_relation = @relation.joins(:folder_memberships)
.where(folder_memberships: { folder_id: [@folder.id] })
.where(@relation.primary_key => result_ids)
@search.total = result_relation.count
fetch_results(result_relation, result_ids)
end

def fetch_results(result_relation, result_ids)
result_relation
.limit(@search.page[:limit])
.order(Arel.sql("array_position(array[#{result_ids.join(",")}]::integer[],
\"#{@relation.table_name}\".\"#{@relation.primary_key}\")"))
end

def parse_results(results)
results.field_values(@relation.primary_key)
end

def exec_query(sql_arr)
ApplicationRecord.connection.execute(ApplicationRecord.send(:sanitize_sql_array, sql_arr))
end
end
12 changes: 5 additions & 7 deletions app/services/public_sign_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,33 +3,31 @@
require "./lib/sql/status"

class PublicSignService
attr_reader :search, :results

def initialize(search:, relation:)
@search = search
@relation = relation
@results = SearchResults.new
end

def process
results = SearchResults.new
results.data = build_results
results.support = search.page_with_total
results.support = @search.page_with_total
results
end

private

def build_results
sql_arr = [SQL::Status.public_signs(search.order_clause)]
sql_arr = [SQL::Status.public_signs(@search.order_clause)]
result_ids = parse_results(exec_query(sql_arr))
result_relation = @relation.where(@relation.primary_key => result_ids)
search.total = result_relation.count
@search.total = result_relation.count
fetch_results(result_relation, result_ids)
end

def fetch_results(result_relation, result_ids)
result_relation
.limit(search.page[:limit])
.limit(@search.page[:limit])
.order(Arel.sql("array_position(array[#{result_ids.join(",")}]::integer[],
\"#{@relation.table_name}\".\"#{@relation.primary_key}\")"))
end
Expand Down
57 changes: 57 additions & 0 deletions app/services/topic_sign_service.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# frozen_string_literal: true

require "./lib/sql/status"

class TopicSignService
def initialize(search:, relation:, topic:)
@search = search
@relation = relation
@topic = topic
end

def process
results = SearchResults.new
results.data = build_results
results.support = @search.page_with_total
results
end

private

def build_results
sql_arr = [SQL::Status.all_signs(@search.order_clause)]
result_ids = parse_results(exec_query(sql_arr))

result_relation = choose_topic.where(@relation.primary_key => result_ids)

# use length, so we don't try and count in SQL, because when there is a group by in the query such as in the
# uncategorised scope the count returns a hash of the count of each grouped result
# but we just want to know how many resutls there are in total
@search.total = result_relation.length
fetch_results(result_relation, result_ids)
end

def choose_topic
if @topic.id.nil?
@relation.uncategorised
else
@relation.joins(:sign_topics)
.where(sign_topics: { topic_id: [@topic.id] })
end
end

def fetch_results(result_relation, result_ids)
result_relation
.limit(@search.page[:limit])
.order(Arel.sql("array_position(array[#{result_ids.join(",")}]::integer[],
\"#{@relation.table_name}\".\"#{@relation.primary_key}\")"))
end

def parse_results(results)
results.field_values(@relation.primary_key)
end

def exec_query(sql_arr)
ApplicationRecord.connection.execute(ApplicationRecord.send(:sanitize_sql_array, sql_arr))
end
end
23 changes: 22 additions & 1 deletion app/views/folders/show.html.erb
Original file line number Diff line number Diff line change
@@ -1,4 +1,16 @@
<% provide(:title, @folder.title) %>
<div class="grid-x margin-vertical-2">
<div class="cell auto">
<h2 class="normal">
<%= [@page[:limit], @page[:total]].min %> of <%= @page[:total] %> signs
</h2>
</div>

<div class="cell shrink">
<%= render "search/sort_by", page: @page unless @page[:total].zero? %>
</div>
</div>
<hr class="list__divider list__divider--dark">

<div class="grid-x grid-margin-x">
<nav class="cell shrink" aria-label="You are here:" role="navigation">
Expand All @@ -10,6 +22,7 @@
</li>
</ul>
</nav>

<div class="grid-x cell list">
<div class="list__title list__section cell grid-x align-middle">
<div class="cell grid-x align-middle">
Expand Down Expand Up @@ -43,12 +56,20 @@
<div class="list__description--metadata"><%= pluralize(@folder.collaborators.count, "team member") %></div>
</div>
</div>

<hr class="list__divider"/>
<div class="list__section list__section--sign-cards cell margin-vertical-0 sign-grid">
<% @folder.signs.each do |sign| %>
<% @signs.for_cards.each do |sign| %>
<%= render "signs/card", sign: sign %>
<% end %>
</div>

<% if @page[:total] >= @page[:limit] %>
<div class="cell auto text-center margin-2">
<%= link_to "Show More", params.to_unsafe_h.merge(page: @page[:next_page], anchor: dom_id(@signs.last, :card)), class: "button primary" %>
</div>
<% end %>

<div class="reveal modal-form align-center" id="manage-folder-modal" data-reveal>
Loading...
</div>
Expand Down
6 changes: 1 addition & 5 deletions app/views/public_signs/index.html.erb
Original file line number Diff line number Diff line change
@@ -1,11 +1,7 @@
<div class="grid-x margin-vertical-2">
<div class="cell auto">
<h2 class="normal">
<% if @page[:limit] >= @page[:total] %>
<%= @page[:total] %> of <%= @page[:total] %> signs
<% else %>
<%= @page[:limit] %> of <%= @page[:total] %> signs
<% end %>
<%= [@page[:limit], @page[:total]].min %> of <%= @page[:total] %> signs
</h2>
</div>

Expand Down
19 changes: 19 additions & 0 deletions app/views/topics/show.html.erb
Original file line number Diff line number Diff line change
@@ -1,5 +1,18 @@
<% provide(:title, @topic.name) %>

<div class="grid-x margin-vertical-2">
<div class="cell auto">
<h2 class="normal">
<%= [@page[:limit], @page[:total]].min %> of <%= @page[:total] %> signs
</h2>
</div>

<div class="cell shrink">
<%= render "search/sort_by", page: @page unless @page[:total].zero? %>
</div>
</div>
<hr class="list__divider list__divider--dark">

<div class="grid-x grid-margin-x">
<nav class="cell shrink" aria-label="You are here:" role="navigation">
<ul class="breadcrumbs">
Expand All @@ -20,3 +33,9 @@
</div>
</div>
</div>

<% if @page[:total] >= @page[:limit] %>
<div class="cell auto text-center margin-2">
<%= link_to "Show More", params.to_unsafe_h.merge(page: @page[:next_page], anchor: dom_id(@signs.last, :card)), class: "button primary" %>
</div>
<% end %>
49 changes: 29 additions & 20 deletions config/brakeman.ignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,42 +3,48 @@
{
"warning_type": "SQL Injection",
"warning_code": 0,
"fingerprint": "1f5bce3e919d9636f7e3ad264fc6b4f0e7b10914df8cecd7617afa3b55b51a60",
"fingerprint": "03824bf3e00caacbe3bb32ff1a4c23578b4d0893f9494eed88d02ff41d3403d5",
"check_name": "SQL",
"message": "Possible SQL injection",
"file": "app/services/search_service.rb",
"line": 42,
"file": "app/services/topic_sign_service.rb",
"line": 37,
"link": "https://brakemanscanner.org/docs/warning_types/sql_injection/",
"code": "Arel.sql(\"array_position(array[#{result_ids.join(\",\")}]::integer[],\\n \\\"#{relation.table_name}\\\".\\\"#{relation.primary_key}\\\")\")",
"render_path": null,
"location": {
"type": "method",
"class": "SearchService",
"class": "TopicSignService",
"method": "fetch_results"
},
"user_input": "result_ids.join(\",\")",
"confidence": "Medium",
"cwe_id": [
89
],
"note": ""
},
{
"warning_type": "Dynamic Render Path",
"warning_code": 15,
"fingerprint": "378082454d97d60a7aa8ca6e6af6915b93a8480ce792b4cffb4cea39227c4969",
"check_name": "Render",
"message": "Render path contains parameter value",
"file": "app/controllers/static_controller.rb",
"line": 5,
"link": "https://brakemanscanner.org/docs/warning_types/dynamic_render_path/",
"code": "render(action => params[:page], {})",
"warning_type": "SQL Injection",
"warning_code": 0,
"fingerprint": "dfdbc776ac224c9d431aaa96a339f2418d07f24e9021fba8ce3212af6d891b4f",
"check_name": "SQL",
"message": "Possible SQL injection",
"file": "app/services/folder_sign_service.rb",
"line": 37,
"link": "https://brakemanscanner.org/docs/warning_types/sql_injection/",
"code": "Arel.sql(\"array_position(array[#{result_ids.join(\",\")}]::integer[],\\n \\\"#{relation.table_name}\\\".\\\"#{relation.primary_key}\\\")\")",
"render_path": null,
"location": {
"type": "method",
"class": "StaticController",
"method": "show"
"class": "FolderSignService",
"method": "fetch_results"
},
"user_input": "params[:page]",
"confidence": "High",
"note": "This is protected by the gaurd clause on the line above"
"user_input": "result_ids.join(\",\")",
"confidence": "Medium",
"cwe_id": [
89
],
"note": ""
},
{
"warning_type": "SQL Injection",
Expand All @@ -58,9 +64,12 @@
},
"user_input": "result_ids.join(\",\")",
"confidence": "Medium",
"cwe_id": [
89
],
"note": ""
}
],
"updated": "2021-06-10 10:53:40 +1200",
"brakeman_version": "5.0.4"
"updated": "2025-08-21 14:21:48 +1200",
"brakeman_version": "5.4.0"
}
Loading
Loading