diff --git a/lib/hledger/journal.rb b/lib/hledger/journal.rb index 1d154bc..0920084 100644 --- a/lib/hledger/journal.rb +++ b/lib/hledger/journal.rb @@ -6,7 +6,7 @@ module Hledger # control. class Journal FENCE = /^[ \t]*`{3,}hledger[ \t]*\r?\n(?.*?)^[ \t]*`{3,}[ \t]*$/m - INCLUDE = /^[ \t]*!?include\b/i + INCLUDE = /^[ \t]*!?[ \t]*include\b/i MAX_LINES = 5_000 Result = @@ -29,6 +29,8 @@ module Hledger end def extract + return Result.new(source: nil, error: :invalid_encoding) unless @raw.valid_encoding? + matches = @raw.scan(FENCE).flatten return Result.new(source: nil, error: :none) if matches.empty? return Result.new(source: nil, error: :ambiguous) if matches.length > 1 diff --git a/spec/lib/hledger/journal_spec.rb b/spec/lib/hledger/journal_spec.rb index 804ab39..4238522 100644 --- a/spec/lib/hledger/journal_spec.rb +++ b/spec/lib/hledger/journal_spec.rb @@ -39,6 +39,32 @@ RSpec.describe Hledger::Journal do expect(result.error).to eq(:include_not_allowed) end + [ + "include /etc/passwd", + "include ../foo", + "include ~/foo", + "include *.journal", + "include **/*", + " include foo", + "\tinclude foo", + "INCLUDE foo", + "!include foo", + "! include foo", + "! include foo", + ].each do |directive| + it "rejects the include directive #{directive.inspect}" do + result = described_class.extract(raw_with(directive)) + + expect(result.error).to eq(:include_not_allowed) + end + end + + it "allows the word include inside a transaction" do + result = described_class.extract(raw_with("2024-01-01 include the foo\n a 1\n b -1")) + + expect(result).to be_ok + end + it "rejects oversized journals" do SiteSetting.hledger_max_journal_bytes = 10 @@ -48,4 +74,21 @@ RSpec.describe Hledger::Journal do ensure SiteSetting.hledger_max_journal_bytes = 262_144 end + + it "extracts a fence with trailing whitespace and a longer fence" do + raw = "Intro\n\n````hledger \n2024-01-01 x\n a 1\n b -1\n````\n\nOutro\n" + + result = described_class.extract(raw) + + expect(result).to be_ok + expect(result.source).to include("2024-01-01 x") + end + + it "rejects invalid UTF-8" do + raw = "```hledger\n2024-01-01 \xFF\n```\n" + + result = described_class.extract(raw) + + expect(result.error).to eq(:invalid_encoding) + end end diff --git a/spec/requests/hledger/reports_spec.rb b/spec/requests/hledger/reports_spec.rb index 89ea54b..268a603 100644 --- a/spec/requests/hledger/reports_spec.rb +++ b/spec/requests/hledger/reports_spec.rb @@ -129,6 +129,15 @@ RSpec.describe Hledger::ReportsController do expect(response.status).to eq(422) end + it "rejects journals with include directives" do + post.update!(raw: "```hledger\ninclude /etc/passwd\n```") + + get_report("accounts") + + expect(response.status).to eq(422) + expect(response.body).not_to include("/etc/passwd") + end + it "returns service unavailable when hledger is missing" do allow(Hledger::Runner).to receive(:new).and_return(StubHledgerRunner.new(version: nil)) @@ -168,6 +177,123 @@ RSpec.describe Hledger::ReportsController do expect(response.status).to eq(404) end + describe "authorization" do + def journal_raw + <<~RAW + ```hledger + 2024-01-01 Opening balances + assets:bank:checking 1000.00 EUR + equity:alice -600.00 EUR + equity:bob -400.00 EUR + ``` + RAW + end + + def report_for(topic) + get "/hledger/topics/#{topic.id}/reports/accounts.json" + end + + it "allows anonymous access to a public topic" do + report_for(post.topic) + + expect(response.status).to eq(200) + end + + it "allows access to a locked and archived topic the user can see" do + post.topic.update!(closed: true, archived: true) + + report_for(post.topic) + + expect(response.status).to eq(200) + end + + it "returns not found for a deleted topic" do + topic = Fabricate(:post, raw: journal_raw).topic + topic.trash! + + report_for(topic) + + expect(response.status).to eq(404) + expect(response.body).not_to include("assets:bank:checking") + end + + it "returns not found when the first post is deleted" do + topic = Fabricate(:post, raw: journal_raw).topic + topic.first_post.trash! + + report_for(topic) + + expect(response.status).to eq(404) + end + + context "with a group-restricted category" do + fab!(:group) + fab!(:restricted_category) { Fabricate(:private_category, group: group) } + fab!(:restricted_topic) do + Fabricate( + :post, + raw: journal_raw, + topic: Fabricate(:topic, category: restricted_category), + ).topic + end + + it "hides it from anonymous users" do + report_for(restricted_topic) + + expect(response.status).to eq(404) + expect(response.body).not_to include("assets:bank:checking") + end + + it "hides it from logged in non-members" do + sign_in(Fabricate(:user)) + + report_for(restricted_topic) + + expect(response.status).to eq(404) + end + + it "allows members" do + user = Fabricate(:user) + group.add(user) + sign_in(user) + + report_for(restricted_topic) + + expect(response.status).to eq(200) + end + end + + context "with a private message" do + fab!(:member, :user) + fab!(:private_message_post) do + Fabricate(:private_message_post, recipient: member, raw: journal_raw) + end + + it "hides it from anonymous users" do + report_for(private_message_post.topic) + + expect(response.status).to eq(404) + end + + it "hides it from non-members" do + sign_in(Fabricate(:user)) + + report_for(private_message_post.topic) + + expect(response.status).to eq(404) + expect(response.body).not_to include("assets:bank:checking") + end + + it "allows members" do + sign_in(member) + + report_for(private_message_post.topic) + + expect(response.status).to eq(200) + end + end + end + it "is not found when the plugin is disabled" do SiteSetting.hledger_enabled = false