markdownr 0.8.0 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/bin/markdownr +64 -0
- data/bin/start-claude +2 -0
- data/lib/markdown_server/app.rb +253 -40
- data/lib/markdown_server/assets/editor-loader.js +362 -0
- data/lib/markdown_server/helpers/admin_helpers.rb +10 -0
- data/lib/markdown_server/helpers/formatting_helpers.rb +3 -1
- data/lib/markdown_server/helpers/markdown_helpers.rb +132 -5
- data/lib/markdown_server/helpers/path_helpers.rb +56 -7
- data/lib/markdown_server/helpers/search_helpers.rb +31 -3
- data/lib/markdown_server/permitted_bases.rb +13 -0
- data/lib/markdown_server/plugins/bible_citations/citations.rb +4 -4
- data/lib/markdown_server/unhide.rb +114 -0
- data/lib/markdown_server/version.rb +1 -1
- data/views/browser.erb +1436 -50
- data/views/layout.erb +122 -5
- data/views/popup_assets.erb +52 -26
- metadata +6 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e25bef09e610cf90ddade4977a1ab04e69c5eff89e1904e7f25edebeacd11d05
|
|
4
|
+
data.tar.gz: ef7183813e31254cc73a8be5776e445dc67830c3387629724c3110707205ed50
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e9528e7bf4f77ebf68486cf0ae3ac72042084365a6e8853a0ea638b6af1bc7edb2986c46ce0612c42880e38c34e9f169ce5ec93759af79253ad93a0f7f5d38ed
|
|
7
|
+
data.tar.gz: 4ccd4fac98ce2a4ce78d04bc1dec843be9648baa949cc5510f8da559aeb2b59d8db5a980b5a4da50cd60873f5c15662e066e793b0a151da14723e894056bc8e7
|
data/bin/markdownr
CHANGED
|
@@ -32,6 +32,14 @@ OptionParser.new do |opts|
|
|
|
32
32
|
options[:behind_proxy] = true
|
|
33
33
|
end
|
|
34
34
|
|
|
35
|
+
opts.on("--admin", "Grant admin access to every visitor (use only on trusted local networks)") do
|
|
36
|
+
options[:admin_all] = true
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
opts.on("--admin-only", "Block all routes except /admin/login until logged in as admin") do
|
|
40
|
+
options[:admin_only] = true
|
|
41
|
+
end
|
|
42
|
+
|
|
35
43
|
opts.on("-i", "--index-file FILENAME", "Render this file instead of the directory listing when present (e.g. index.md, moc.md)") do |f|
|
|
36
44
|
options[:index_file] = f
|
|
37
45
|
end
|
|
@@ -48,6 +56,18 @@ OptionParser.new do |opts|
|
|
|
48
56
|
options[:verbose] = true
|
|
49
57
|
end
|
|
50
58
|
|
|
59
|
+
opts.on("--default-vim", "Open the in-browser editor in vim mode by default (per-user toggle still wins via localStorage)") do
|
|
60
|
+
options[:default_vim] = true
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
opts.on("--allow-editor", "Enable the in-browser file editor (markdown / source). Without this flag, the Edit button is hidden and all editor write/preview/source routes return errors, even for admins.") do
|
|
64
|
+
options[:allow_editor] = true
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
opts.on("--allow-csv-editor", "Enable CSV editing (row update, duplicate, delete, add-on actions). Without this flag, CSV tables are forced read-only on the server and in the UI, even for admins.") do
|
|
68
|
+
options[:allow_csv_editor] = true
|
|
69
|
+
end
|
|
70
|
+
|
|
51
71
|
opts.on("--plugin NAME", "Enable a plugin by name (can be specified multiple times)") do |name|
|
|
52
72
|
options[:plugin_overrides] = (options[:plugin_overrides] || {}).merge(
|
|
53
73
|
name => (options.dig(:plugin_overrides, name) || {}).merge("enabled" => true)
|
|
@@ -58,6 +78,14 @@ OptionParser.new do |opts|
|
|
|
58
78
|
(options[:plugin_dirs] ||= []) << d
|
|
59
79
|
end
|
|
60
80
|
|
|
81
|
+
opts.on("--follow-link PATH", Array, "Allow this symlink (inside the served dir) to escape to its target. Repeatable; comma-separated values also accepted.") do |paths|
|
|
82
|
+
(options[:follow_links] ||= []).concat(paths)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
opts.on("--unhide ENTRY", "Show a normally-hidden entry (repeatable). Bare name matches anywhere; @/path is project-root-anchored; multi-segment narrows the listing.") do |v|
|
|
86
|
+
(options[:unhide] ||= []) << v
|
|
87
|
+
end
|
|
88
|
+
|
|
61
89
|
opts.on("-v", "--version", "Show version") do
|
|
62
90
|
puts "markdownr #{MarkdownServer::VERSION}"
|
|
63
91
|
exit
|
|
@@ -74,18 +102,39 @@ end
|
|
|
74
102
|
|
|
75
103
|
MarkdownServer::App.set :root_dir, dir
|
|
76
104
|
MarkdownServer::App.set :behind_proxy, options[:behind_proxy] || false
|
|
105
|
+
MarkdownServer::App.set :admin_all, options[:admin_all] || false
|
|
106
|
+
MarkdownServer::App.set :admin_only, options[:admin_only] || false
|
|
77
107
|
MarkdownServer::App.set :custom_title, options[:title]
|
|
78
108
|
MarkdownServer::App.set :allow_robots, options[:allow_robots] || false
|
|
79
109
|
MarkdownServer::App.set :index_file, options[:index_file]
|
|
80
110
|
MarkdownServer::App.set :link_tooltips, options.fetch(:link_tooltips, true)
|
|
81
111
|
MarkdownServer::App.set :hard_wrap, options.fetch(:hard_wrap, true)
|
|
82
112
|
MarkdownServer::App.set :verbose, options[:verbose] || false
|
|
113
|
+
MarkdownServer::App.set :default_vim, options[:default_vim] || false
|
|
114
|
+
MarkdownServer::App.set :allow_editor, options[:allow_editor] || false
|
|
115
|
+
MarkdownServer::App.set :allow_csv_editor, options[:allow_csv_editor] || false
|
|
83
116
|
MarkdownServer::App.set :port, options[:port]
|
|
84
117
|
MarkdownServer::App.set :bind, options[:bind]
|
|
85
118
|
MarkdownServer::App.set :server_settings, { max_threads: options[:threads], min_threads: 1 }
|
|
86
119
|
MarkdownServer::App.set :plugin_overrides, options[:plugin_overrides] || {}
|
|
87
120
|
MarkdownServer::App.set :plugin_dirs, options[:plugin_dirs] || []
|
|
88
121
|
|
|
122
|
+
root_real = File.realpath(dir)
|
|
123
|
+
followed_links = (options[:follow_links] || []).filter_map do |p|
|
|
124
|
+
expanded = File.expand_path(p, dir)
|
|
125
|
+
unless File.exist?(expanded)
|
|
126
|
+
$stderr.puts "Warning: --follow-link path does not exist, skipping: #{p}"
|
|
127
|
+
next nil
|
|
128
|
+
end
|
|
129
|
+
link_real = File.realpath(expanded)
|
|
130
|
+
unless expanded == root_real || expanded.start_with?("#{root_real}/")
|
|
131
|
+
$stderr.puts "Warning: --follow-link path is outside the served dir, skipping: #{p}"
|
|
132
|
+
next nil
|
|
133
|
+
end
|
|
134
|
+
link_real
|
|
135
|
+
end.uniq
|
|
136
|
+
MarkdownServer::App.set :followed_links, followed_links
|
|
137
|
+
|
|
89
138
|
# Load .markdownr.yml popup settings
|
|
90
139
|
config_path = File.join(dir, ".markdownr.yml")
|
|
91
140
|
if File.exist?(config_path)
|
|
@@ -102,6 +151,10 @@ if File.exist?(config_path)
|
|
|
102
151
|
MarkdownServer::App.set :plugin_dirs, (yaml["plugin_dirs"] + existing).uniq
|
|
103
152
|
end
|
|
104
153
|
|
|
154
|
+
if yaml && yaml.key?("admin_only") && !options[:admin_only]
|
|
155
|
+
MarkdownServer::App.set :admin_only, yaml["admin_only"] == true
|
|
156
|
+
end
|
|
157
|
+
|
|
105
158
|
# Load CSV database config (top-level key or legacy plugin key)
|
|
106
159
|
csv_db_paths = yaml&.dig("csv_databases") || yaml&.dig("plugins", "csv_browser", "databases")
|
|
107
160
|
if csv_db_paths.is_a?(Array) && !csv_db_paths.empty?
|
|
@@ -118,6 +171,17 @@ if File.exist?(config_path)
|
|
|
118
171
|
end
|
|
119
172
|
end
|
|
120
173
|
|
|
174
|
+
# Compile and apply unhide rules (YAML + CLI, additive merge).
|
|
175
|
+
yaml_unhide = (defined?(yaml) && yaml.is_a?(Hash) && yaml["unhide"].is_a?(Array)) ? yaml["unhide"] : []
|
|
176
|
+
combined_unhide = (yaml_unhide + (options[:unhide] || [])).uniq
|
|
177
|
+
unless combined_unhide.empty?
|
|
178
|
+
begin
|
|
179
|
+
MarkdownServer::App.set :unhide_rules, MarkdownServer::Unhide.compile(combined_unhide)
|
|
180
|
+
rescue MarkdownServer::Unhide::CompileError => e
|
|
181
|
+
abort "Invalid unhide config: #{e.message}"
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
|
|
121
185
|
MarkdownServer::App.load_plugins!
|
|
122
186
|
|
|
123
187
|
puts "Serving #{dir} on http://#{options[:bind]}:#{options[:port]}/"
|
data/bin/start-claude
ADDED
data/lib/markdown_server/app.rb
CHANGED
|
@@ -10,11 +10,14 @@ require "pathname"
|
|
|
10
10
|
require "set"
|
|
11
11
|
require "net/http"
|
|
12
12
|
require "base64"
|
|
13
|
+
require "digest"
|
|
13
14
|
require_relative "plugin"
|
|
14
15
|
require_relative "csv_browser/config_loader"
|
|
15
16
|
require_relative "csv_browser/table_reader"
|
|
16
17
|
require_relative "csv_browser/addon_registry"
|
|
17
18
|
require_relative "csv_browser/row_context"
|
|
19
|
+
require_relative "permitted_bases"
|
|
20
|
+
require_relative "unhide"
|
|
18
21
|
require_relative "helpers/path_helpers"
|
|
19
22
|
require_relative "helpers/formatting_helpers"
|
|
20
23
|
require_relative "helpers/markdown_helpers"
|
|
@@ -41,6 +44,8 @@ module MarkdownServer
|
|
|
41
44
|
set :protection, false
|
|
42
45
|
set :host_authorization, { permitted_hosts: [] }
|
|
43
46
|
set :behind_proxy, false
|
|
47
|
+
set :admin_all, false
|
|
48
|
+
set :admin_only, false
|
|
44
49
|
set :verbose, false
|
|
45
50
|
set :session_secret, ENV.fetch("MARKDOWNR_SESSION_SECRET", SecureRandom.hex(64))
|
|
46
51
|
set :sessions, key: "markdownr_session", same_site: :strict, httponly: true
|
|
@@ -53,6 +58,11 @@ module MarkdownServer
|
|
|
53
58
|
set :dictionary_url, nil
|
|
54
59
|
set :plugin_dirs, []
|
|
55
60
|
set :csv_browser_config, nil
|
|
61
|
+
set :followed_links, []
|
|
62
|
+
set :unhide_rules, []
|
|
63
|
+
set :default_vim, false
|
|
64
|
+
set :allow_editor, false
|
|
65
|
+
set :allow_csv_editor, false
|
|
56
66
|
end
|
|
57
67
|
|
|
58
68
|
def self.load_plugins!
|
|
@@ -83,16 +93,21 @@ module MarkdownServer
|
|
|
83
93
|
end
|
|
84
94
|
|
|
85
95
|
before do
|
|
86
|
-
if request.path_info.start_with?("/browser", "/csv-browser")
|
|
87
|
-
cache_control :no_cache, :no_store, :must_revalidate
|
|
88
|
-
else
|
|
89
|
-
cache_control :public, max_age: 14400
|
|
90
|
-
end
|
|
91
|
-
|
|
92
96
|
if settings.verbose
|
|
93
97
|
$stdout.puts "#{Time.now.strftime("%Y-%m-%d %H:%M:%S")} #{client_ip} #{request.request_method} #{request.fullpath}"
|
|
94
98
|
$stdout.flush
|
|
95
99
|
end
|
|
100
|
+
|
|
101
|
+
if admin_only_mode? && !admin_only_public_route?(request.path_info) && !admin?
|
|
102
|
+
cache_control :no_cache, :no_store, :must_revalidate
|
|
103
|
+
redirect "/admin/login?return_to=#{CGI.escape(request.fullpath)}"
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
if request.path_info.start_with?("/browser", "/csv-browser", "/__markdownr/api")
|
|
107
|
+
cache_control :no_cache, :no_store, :must_revalidate
|
|
108
|
+
else
|
|
109
|
+
cache_control :public, max_age: 14400
|
|
110
|
+
end
|
|
96
111
|
end
|
|
97
112
|
|
|
98
113
|
get "/" do
|
|
@@ -110,6 +125,175 @@ module MarkdownServer
|
|
|
110
125
|
{ ok: true }.to_json
|
|
111
126
|
end
|
|
112
127
|
|
|
128
|
+
EDITOR_FILE_MAX_BYTES = 5 * 1024 * 1024
|
|
129
|
+
|
|
130
|
+
def editor_loader_path
|
|
131
|
+
@@editor_loader_path ||= File.expand_path("assets/editor-loader.js", __dir__)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def editor_loader_version
|
|
135
|
+
path = editor_loader_path
|
|
136
|
+
content = File.read(path) + editor_import_map_json
|
|
137
|
+
Digest::SHA256.hexdigest(content)[0, 10]
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
EDITOR_PKGS = {
|
|
141
|
+
"@codemirror/state" => "6.6.0",
|
|
142
|
+
"@codemirror/view" => "6.41.1",
|
|
143
|
+
"@codemirror/commands" => "6.10.3",
|
|
144
|
+
"@codemirror/language" => "6.12.3",
|
|
145
|
+
"@codemirror/search" => "6.7.0",
|
|
146
|
+
"@codemirror/autocomplete" => "6.20.1",
|
|
147
|
+
"@codemirror/theme-one-dark" => "6.1.3",
|
|
148
|
+
"@codemirror/lang-markdown" => "6.5.0",
|
|
149
|
+
"@codemirror/lang-javascript" => "6.2.5",
|
|
150
|
+
"@codemirror/lang-json" => "6.0.2",
|
|
151
|
+
"@codemirror/lang-yaml" => "6.1.3",
|
|
152
|
+
"@codemirror/lang-html" => "6.4.11",
|
|
153
|
+
"@codemirror/lang-css" => "6.3.1",
|
|
154
|
+
"@codemirror/lang-python" => "6.2.1",
|
|
155
|
+
"@codemirror/legacy-modes" => "6.5.2",
|
|
156
|
+
"@replit/codemirror-vim" => "6.3.0"
|
|
157
|
+
}.freeze
|
|
158
|
+
|
|
159
|
+
EDITOR_SHARED_DEPS = %w[
|
|
160
|
+
@codemirror/state
|
|
161
|
+
@codemirror/view
|
|
162
|
+
@codemirror/language
|
|
163
|
+
@codemirror/commands
|
|
164
|
+
].freeze
|
|
165
|
+
|
|
166
|
+
def editor_import_map_json
|
|
167
|
+
@@editor_import_map_json ||= begin
|
|
168
|
+
imports = {}
|
|
169
|
+
EDITOR_PKGS.each do |pkg, ver|
|
|
170
|
+
deps = EDITOR_SHARED_DEPS.reject { |d| d == pkg }
|
|
171
|
+
imports[pkg] = "https://esm.sh/#{pkg}@#{ver}?external=#{deps.join(",")}"
|
|
172
|
+
end
|
|
173
|
+
EDITOR_PKGS.each do |pkg, ver|
|
|
174
|
+
next unless pkg == "@codemirror/legacy-modes"
|
|
175
|
+
%w[ruby shell].each do |mode|
|
|
176
|
+
deps = EDITOR_SHARED_DEPS.reject { |d| d == pkg }
|
|
177
|
+
imports["#{pkg}/mode/#{mode}"] =
|
|
178
|
+
"https://esm.sh/#{pkg}@#{ver}/mode/#{mode}.js?external=#{deps.join(",")}"
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
JSON.dump({ imports: imports })
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
get "/__markdownr/editor-loader.js" do
|
|
186
|
+
halt 404, "editor disabled" unless settings.allow_editor
|
|
187
|
+
cache_control :public, max_age: 14400
|
|
188
|
+
send_file File.expand_path("assets/editor-loader.js", __dir__),
|
|
189
|
+
type: "application/javascript"
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
get "/__markdownr/api/file/source/?*" do
|
|
193
|
+
halt 404, '{"error":"editor disabled"}' unless settings.allow_editor
|
|
194
|
+
requested = params["splat"].first.to_s.chomp("/")
|
|
195
|
+
halt 400, '{"error":"missing path"}' if requested.empty?
|
|
196
|
+
|
|
197
|
+
real_path = safe_path(requested)
|
|
198
|
+
halt 404, '{"error":"not a file"}' unless File.file?(real_path)
|
|
199
|
+
|
|
200
|
+
size = File.size(real_path)
|
|
201
|
+
halt 413, { error: "file too large", max: EDITOR_FILE_MAX_BYTES, size: size }.to_json if size > EDITOR_FILE_MAX_BYTES
|
|
202
|
+
|
|
203
|
+
content = File.read(real_path, encoding: "utf-8")
|
|
204
|
+
etag = Digest::SHA256.hexdigest(content)
|
|
205
|
+
headers "ETag" => %("#{etag}")
|
|
206
|
+
headers "Last-Modified" => File.mtime(real_path).httpdate
|
|
207
|
+
content_type "text/plain; charset=utf-8"
|
|
208
|
+
cache_control :no_cache, :no_store, :must_revalidate
|
|
209
|
+
content
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
put "/__markdownr/api/file/?*" do
|
|
213
|
+
content_type :json
|
|
214
|
+
cache_control :no_cache, :no_store, :must_revalidate
|
|
215
|
+
unless settings.allow_editor
|
|
216
|
+
halt 403, { error: "File editor disabled on this server. Restart with --allow-editor to enable." }.to_json
|
|
217
|
+
end
|
|
218
|
+
unless admin?
|
|
219
|
+
halt 403, { error: "Admin login required to save changes.",
|
|
220
|
+
admin_url: "https://github.com/brianmd/markdown-server#admin-access",
|
|
221
|
+
client_ip: client_ip }.to_json
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
requested = params["splat"].first.to_s.chomp("/")
|
|
225
|
+
halt 400, { error: "missing path" }.to_json if requested.empty?
|
|
226
|
+
|
|
227
|
+
real_path = safe_path(requested)
|
|
228
|
+
halt 404, { error: "not a file" }.to_json unless File.file?(real_path)
|
|
229
|
+
|
|
230
|
+
body = request.body.read
|
|
231
|
+
halt 413, { error: "file too large", max: EDITOR_FILE_MAX_BYTES, size: body.bytesize }.to_json if body.bytesize > EDITOR_FILE_MAX_BYTES
|
|
232
|
+
|
|
233
|
+
body = body.force_encoding("utf-8")
|
|
234
|
+
halt 400, { error: "body is not valid UTF-8" }.to_json unless body.valid_encoding?
|
|
235
|
+
|
|
236
|
+
if_match = request.env["HTTP_IF_MATCH"].to_s.strip
|
|
237
|
+
force = if_match.empty? || if_match == "*"
|
|
238
|
+
|
|
239
|
+
unless force
|
|
240
|
+
current = File.read(real_path, encoding: "utf-8")
|
|
241
|
+
current_etag = Digest::SHA256.hexdigest(current)
|
|
242
|
+
provided = if_match.gsub(/\A"|"\z/, "")
|
|
243
|
+
if provided != current_etag
|
|
244
|
+
halt 409, { error: "stale", current_etag: current_etag }.to_json
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
tmp_path = "#{real_path}.tmp.#{Process.pid}.#{Thread.current.object_id}"
|
|
249
|
+
File.write(tmp_path, body)
|
|
250
|
+
File.rename(tmp_path, real_path)
|
|
251
|
+
|
|
252
|
+
logger.info("file edit: #{client_ip} #{requested} #{body.bytesize}b") if respond_to?(:logger) && logger
|
|
253
|
+
new_etag = Digest::SHA256.hexdigest(body)
|
|
254
|
+
{ etag: new_etag, mtime: File.mtime(real_path).iso8601, bytes: body.bytesize }.to_json
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
post "/__markdownr/api/render/preview" do
|
|
258
|
+
content_type :json
|
|
259
|
+
cache_control :no_cache, :no_store, :must_revalidate
|
|
260
|
+
unless settings.allow_editor
|
|
261
|
+
halt 403, { error: "File editor disabled on this server. Restart with --allow-editor to enable." }.to_json
|
|
262
|
+
end
|
|
263
|
+
unless admin?
|
|
264
|
+
halt 403, { error: "Admin login required to preview edits.",
|
|
265
|
+
admin_url: "https://github.com/brianmd/markdown-server#admin-access",
|
|
266
|
+
client_ip: client_ip }.to_json
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
payload = JSON.parse(request.body.read) rescue {}
|
|
270
|
+
content = payload["content"].to_s
|
|
271
|
+
halt 413, { error: "content too large" }.to_json if content.bytesize > EDITOR_FILE_MAX_BYTES
|
|
272
|
+
|
|
273
|
+
wiki_dir = payload["current_wiki_dir"].to_s
|
|
274
|
+
base_real = File.realpath(root_dir)
|
|
275
|
+
if wiki_dir.empty?
|
|
276
|
+
@current_wiki_dir = base_real
|
|
277
|
+
else
|
|
278
|
+
candidate = File.expand_path(wiki_dir, base_real)
|
|
279
|
+
@current_wiki_dir = (candidate == base_real || candidate.start_with?("#{base_real}/")) ? candidate : base_real
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
meta, body = parse_frontmatter(content)
|
|
283
|
+
html = render_markdown(body)
|
|
284
|
+
settings.plugins.each { |p| html = p.post_render(html, meta, self) }
|
|
285
|
+
|
|
286
|
+
frontmatter_html = ""
|
|
287
|
+
if meta && !meta.empty?
|
|
288
|
+
rows = meta.map { |key, value|
|
|
289
|
+
"<tr><th>#{h(key)}</th><td>#{render_frontmatter_value(value)}</td></tr>"
|
|
290
|
+
}.join
|
|
291
|
+
frontmatter_html = %(<div class="frontmatter"><div class="frontmatter-heading">Frontmatter</div><table class="meta-table">#{rows}</table></div>)
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
{ html: html, frontmatter_html: frontmatter_html }.to_json
|
|
295
|
+
end
|
|
296
|
+
|
|
113
297
|
get "/setup-info" do
|
|
114
298
|
@title = "Setup Info"
|
|
115
299
|
@client_ip = client_ip
|
|
@@ -150,6 +334,11 @@ module MarkdownServer
|
|
|
150
334
|
@root_title = dir_title
|
|
151
335
|
@start_mode = "directory"
|
|
152
336
|
@csv_databases = csv_databases_json
|
|
337
|
+
@initial_path = ""
|
|
338
|
+
@is_admin = admin?
|
|
339
|
+
@default_vim = settings.default_vim
|
|
340
|
+
@allow_editor = settings.allow_editor
|
|
341
|
+
@allow_csv_editor = settings.allow_csv_editor
|
|
153
342
|
erb :browser, layout: false
|
|
154
343
|
end
|
|
155
344
|
|
|
@@ -158,6 +347,11 @@ module MarkdownServer
|
|
|
158
347
|
@root_title = dir_title
|
|
159
348
|
@start_mode = "csv"
|
|
160
349
|
@csv_databases = csv_databases_json
|
|
350
|
+
@initial_path = ""
|
|
351
|
+
@is_admin = admin?
|
|
352
|
+
@default_vim = settings.default_vim
|
|
353
|
+
@allow_editor = settings.allow_editor
|
|
354
|
+
@allow_csv_editor = settings.allow_csv_editor
|
|
161
355
|
erb :browser, layout: false
|
|
162
356
|
end
|
|
163
357
|
|
|
@@ -351,6 +545,9 @@ module MarkdownServer
|
|
|
351
545
|
|
|
352
546
|
put "/browser/api/csv/databases/:db/tables/:table/rows/:row" do
|
|
353
547
|
content_type :json
|
|
548
|
+
unless settings.allow_csv_editor
|
|
549
|
+
halt 403, { error: "CSV editor disabled on this server. Restart with --allow-csv-editor to enable." }.to_json
|
|
550
|
+
end
|
|
354
551
|
unless admin?
|
|
355
552
|
halt 403, { error: "Admin login required to save changes.",
|
|
356
553
|
admin_url: "https://github.com/brianmd/markdown-server#admin-access",
|
|
@@ -385,6 +582,9 @@ module MarkdownServer
|
|
|
385
582
|
|
|
386
583
|
post "/browser/api/csv/databases/:db/tables/:table/rows/:row/duplicate" do
|
|
387
584
|
content_type :json
|
|
585
|
+
unless settings.allow_csv_editor
|
|
586
|
+
halt 403, { error: "CSV editor disabled on this server. Restart with --allow-csv-editor to enable." }.to_json
|
|
587
|
+
end
|
|
388
588
|
unless admin?
|
|
389
589
|
halt 403, { error: "Admin login required to duplicate rows.",
|
|
390
590
|
admin_url: "https://github.com/brianmd/markdown-server#admin-access",
|
|
@@ -415,6 +615,9 @@ module MarkdownServer
|
|
|
415
615
|
|
|
416
616
|
delete "/browser/api/csv/databases/:db/tables/:table/rows/:row" do
|
|
417
617
|
content_type :json
|
|
618
|
+
unless settings.allow_csv_editor
|
|
619
|
+
halt 403, { error: "CSV editor disabled on this server. Restart with --allow-csv-editor to enable." }.to_json
|
|
620
|
+
end
|
|
418
621
|
unless admin?
|
|
419
622
|
halt 403, { error: "Admin login required to delete rows.",
|
|
420
623
|
admin_url: "https://github.com/brianmd/markdown-server#admin-access",
|
|
@@ -482,6 +685,9 @@ module MarkdownServer
|
|
|
482
685
|
# Invoke an add-on action (initial call or prompt continuation).
|
|
483
686
|
post "/browser/api/csv/databases/:db/tables/:table/addons/:addon/:action" do
|
|
484
687
|
content_type :json
|
|
688
|
+
unless settings.allow_csv_editor
|
|
689
|
+
halt 403, { error: "CSV editor disabled on this server. Restart with --allow-csv-editor to enable." }.to_json
|
|
690
|
+
end
|
|
485
691
|
unless admin?
|
|
486
692
|
halt 403, { error: "Admin login required to run add-on actions.",
|
|
487
693
|
admin_url: "https://github.com/brianmd/markdown-server#admin-access",
|
|
@@ -550,6 +756,34 @@ module MarkdownServer
|
|
|
550
756
|
}.to_json
|
|
551
757
|
end
|
|
552
758
|
|
|
759
|
+
get "/browser/*" do
|
|
760
|
+
splat = params["splat"].first.to_s
|
|
761
|
+
pass if splat.start_with?("api/")
|
|
762
|
+
@title = dir_title
|
|
763
|
+
@root_title = dir_title
|
|
764
|
+
@start_mode = "directory"
|
|
765
|
+
@csv_databases = csv_databases_json
|
|
766
|
+
@initial_path = splat.chomp("/")
|
|
767
|
+
@is_admin = admin?
|
|
768
|
+
@default_vim = settings.default_vim
|
|
769
|
+
@allow_editor = settings.allow_editor
|
|
770
|
+
@allow_csv_editor = settings.allow_csv_editor
|
|
771
|
+
erb :browser, layout: false
|
|
772
|
+
end
|
|
773
|
+
|
|
774
|
+
get "/csv-browser/*" do
|
|
775
|
+
@title = dir_title
|
|
776
|
+
@root_title = dir_title
|
|
777
|
+
@start_mode = "csv"
|
|
778
|
+
@csv_databases = csv_databases_json
|
|
779
|
+
@initial_path = params["splat"].first.to_s.chomp("/")
|
|
780
|
+
@is_admin = admin?
|
|
781
|
+
@default_vim = settings.default_vim
|
|
782
|
+
@allow_editor = settings.allow_editor
|
|
783
|
+
@allow_csv_editor = settings.allow_csv_editor
|
|
784
|
+
erb :browser, layout: false
|
|
785
|
+
end
|
|
786
|
+
|
|
553
787
|
get "/browse/?*" do
|
|
554
788
|
requested = params["splat"].first.to_s
|
|
555
789
|
requested = requested.chomp("/")
|
|
@@ -606,11 +840,7 @@ module MarkdownServer
|
|
|
606
840
|
halt 404, '{"error":"not found"}'
|
|
607
841
|
end
|
|
608
842
|
|
|
609
|
-
halt 403, '{"error":"forbidden"}' unless
|
|
610
|
-
|
|
611
|
-
relative = real.sub("#{base}/", "")
|
|
612
|
-
first_segment = relative.split("/").first
|
|
613
|
-
halt 403, '{"error":"forbidden"}' if EXCLUDED.include?(first_segment) || first_segment&.start_with?(".")
|
|
843
|
+
halt 403, '{"error":"forbidden"}' unless permitted_path?(real)
|
|
614
844
|
|
|
615
845
|
halt 404, '{"error":"not found"}' unless File.file?(real) && File.extname(real).downcase == ".md"
|
|
616
846
|
|
|
@@ -756,8 +986,8 @@ module MarkdownServer
|
|
|
756
986
|
end
|
|
757
987
|
|
|
758
988
|
def render_directory(real_path, relative_path)
|
|
759
|
-
entries = Dir.entries(real_path).
|
|
760
|
-
e
|
|
989
|
+
entries = Dir.entries(real_path).select do |e|
|
|
990
|
+
e != "." && e != ".." && entry_admitted?(real_path, relative_path, e)
|
|
761
991
|
end
|
|
762
992
|
|
|
763
993
|
browse_prefix = relative_path.empty? ? "/browse/" : "/browse/#{relative_path}/"
|
|
@@ -899,8 +1129,8 @@ module MarkdownServer
|
|
|
899
1129
|
end
|
|
900
1130
|
|
|
901
1131
|
def browser_render_directory(real_path, requested)
|
|
902
|
-
entries = Dir.entries(real_path).
|
|
903
|
-
e
|
|
1132
|
+
entries = Dir.entries(real_path).select do |e|
|
|
1133
|
+
e != "." && e != ".." && entry_admitted?(real_path, requested, e)
|
|
904
1134
|
end
|
|
905
1135
|
|
|
906
1136
|
browse_prefix = requested.empty? ? "" : "#{requested}/"
|
|
@@ -927,7 +1157,12 @@ module MarkdownServer
|
|
|
927
1157
|
item
|
|
928
1158
|
end.compact
|
|
929
1159
|
|
|
930
|
-
title = requested.empty?
|
|
1160
|
+
title = if requested.empty?
|
|
1161
|
+
dir_title
|
|
1162
|
+
else
|
|
1163
|
+
parent = File.dirname(requested)
|
|
1164
|
+
parent == "." ? File.basename(requested) : "#{File.basename(requested)} (#{parent})"
|
|
1165
|
+
end
|
|
931
1166
|
{ type: "directory", path: requested, title: title, items: items }
|
|
932
1167
|
end
|
|
933
1168
|
|
|
@@ -1046,17 +1281,7 @@ module MarkdownServer
|
|
|
1046
1281
|
size = File.size(real_path) rescue 0
|
|
1047
1282
|
{ type: "download", title: title, href: download_href, size: format_size(size) }
|
|
1048
1283
|
else
|
|
1049
|
-
lang =
|
|
1050
|
-
when ".py" then "python"
|
|
1051
|
-
when ".rb" then "ruby"
|
|
1052
|
-
when ".csv" then "text"
|
|
1053
|
-
when ".sh" then "bash"
|
|
1054
|
-
when ".yaml", ".yml" then "yaml"
|
|
1055
|
-
when ".erb" then "html"
|
|
1056
|
-
when ".css" then "css"
|
|
1057
|
-
when ".js" then "javascript"
|
|
1058
|
-
else "text"
|
|
1059
|
-
end
|
|
1284
|
+
lang = detect_source_language(real_path, content)
|
|
1060
1285
|
{ type: "code", title: title, language: lang, html: syntax_highlight(content, lang) }
|
|
1061
1286
|
end
|
|
1062
1287
|
end
|
|
@@ -1142,19 +1367,7 @@ module MarkdownServer
|
|
|
1142
1367
|
if content.nil? || content.encoding == Encoding::BINARY || !content.valid_encoding?
|
|
1143
1368
|
send_file real_path, disposition: "inline"
|
|
1144
1369
|
else
|
|
1145
|
-
lang =
|
|
1146
|
-
when ".py" then "python"
|
|
1147
|
-
when ".rb" then "ruby"
|
|
1148
|
-
when ".csv" then "text"
|
|
1149
|
-
when ".sh" then "bash"
|
|
1150
|
-
when ".yaml", ".yml" then "yaml"
|
|
1151
|
-
when ".erb" then "html"
|
|
1152
|
-
# .html and .js are handled by dedicated branches above;
|
|
1153
|
-
# kept here for completeness if those branches are ever removed
|
|
1154
|
-
when ".html" then "html"
|
|
1155
|
-
when ".js" then "javascript"
|
|
1156
|
-
else "text"
|
|
1157
|
-
end
|
|
1370
|
+
lang = detect_source_language(real_path, content)
|
|
1158
1371
|
@code = syntax_highlight(content, lang)
|
|
1159
1372
|
@language = lang
|
|
1160
1373
|
erb :raw
|