clickwrap 0.0.0 → 0.1.0
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/.rubocop.yml +90 -0
- data/CHANGELOG.md +612 -0
- data/README.md +830 -1204
- data/SECURITY.md +33 -0
- data/app/assets/stylesheets/clickwrap.css +241 -0
- data/app/controllers/clickwrap/application_controller.rb +79 -0
- data/app/controllers/clickwrap/captures_controller.rb +145 -0
- data/app/controllers/clickwrap/document_versions_controller.rb +71 -0
- data/app/controllers/clickwrap/receipts_controller.rb +115 -0
- data/app/controllers/clickwrap/withdrawals_controller.rb +60 -0
- data/app/helpers/clickwrap/engine_helper.rb +97 -0
- data/app/views/clickwrap/captures/show.html.erb +34 -0
- data/app/views/clickwrap/receipts/index.html.erb +38 -0
- data/app/views/clickwrap/receipts/show.html.erb +91 -0
- data/app/views/clickwrap/shared/_error_summary.html.erb +39 -0
- data/app/views/clickwrap/shared/_fields.html.erb +100 -0
- data/app/views/clickwrap/shared/_statement.html.erb +105 -0
- data/app/views/clickwrap/withdrawals/new.html.erb +30 -0
- data/config/locales/en.yml +160 -0
- data/config/locales/es.yml +138 -0
- data/config/routes.rb +41 -0
- data/exe/clickwrap +374 -0
- data/guides/README.md +30 -0
- data/guides/accessibility.md +249 -0
- data/guides/consent-and-lifecycle.md +299 -0
- data/guides/integrating.md +610 -0
- data/guides/integrity.md +212 -0
- data/guides/migrating.md +335 -0
- data/guides/naming.md +320 -0
- data/guides/organizations.md +320 -0
- data/guides/receipts-and-verification.md +415 -0
- data/guides/request-evidence.md +512 -0
- data/guides/retention-and-legal-holds.md +438 -0
- data/lib/clickwrap/actor_proxy.rb +147 -0
- data/lib/clickwrap/anonymous_actor.rb +47 -0
- data/lib/clickwrap/authority.rb +174 -0
- data/lib/clickwrap/canonical_json.rb +216 -0
- data/lib/clickwrap/capture/event_builder.rb +220 -0
- data/lib/clickwrap/capture/presentation_verifier.rb +521 -0
- data/lib/clickwrap/capture.rb +650 -0
- data/lib/clickwrap/configuration.rb +1129 -0
- data/lib/clickwrap/controller_helpers.rb +758 -0
- data/lib/clickwrap/current_state.rb +282 -0
- data/lib/clickwrap/digest.rb +125 -0
- data/lib/clickwrap/doctor.rb +418 -0
- data/lib/clickwrap/document_definition.rb +255 -0
- data/lib/clickwrap/document_renderer.rb +83 -0
- data/lib/clickwrap/document_renderers/markdown.rb +175 -0
- data/lib/clickwrap/document_renderers/markdown_rails.rb +126 -0
- data/lib/clickwrap/dsl/policy_builder.rb +462 -0
- data/lib/clickwrap/dsl/retention_builder.rb +89 -0
- data/lib/clickwrap/durable_commit_callback.rb +37 -0
- data/lib/clickwrap/engine.rb +184 -0
- data/lib/clickwrap/errors.rb +181 -0
- data/lib/clickwrap/form_builder_extensions.rb +341 -0
- data/lib/clickwrap/front_matter.rb +67 -0
- data/lib/clickwrap/identifier.rb +112 -0
- data/lib/clickwrap/import/external_receipt.rb +241 -0
- data/lib/clickwrap/import/fine_print.rb +290 -0
- data/lib/clickwrap/import/legacy.rb +450 -0
- data/lib/clickwrap/integrations/organizations_authority.rb +81 -0
- data/lib/clickwrap/integrity/anchor.rb +130 -0
- data/lib/clickwrap/integrity/attestation_reconciler.rb +114 -0
- data/lib/clickwrap/integrity/attestor.rb +221 -0
- data/lib/clickwrap/integrity/chain.rb +313 -0
- data/lib/clickwrap/integrity/timestamp.rb +143 -0
- data/lib/clickwrap/ip_geolocation/location.rb +112 -0
- data/lib/clickwrap/ip_geolocation/null_resolver.rb +35 -0
- data/lib/clickwrap/ip_geolocation/resolver.rb +97 -0
- data/lib/clickwrap/ip_geolocation/static_resolver.rb +107 -0
- data/lib/clickwrap/ip_geolocation/trackdown_resolver.rb +330 -0
- data/lib/clickwrap/ip_geolocation.rb +16 -0
- data/lib/clickwrap/lifecycle.rb +534 -0
- data/lib/clickwrap/linter.rb +382 -0
- data/lib/clickwrap/localized_text.rb +101 -0
- data/lib/clickwrap/macros.rb +203 -0
- data/lib/clickwrap/models/application_record.rb +20 -0
- data/lib/clickwrap/models/chain_head.rb +79 -0
- data/lib/clickwrap/models/concerns/has_clickwraps.rb +55 -0
- data/lib/clickwrap/models/disposition_plan.rb +208 -0
- data/lib/clickwrap/models/document.rb +46 -0
- data/lib/clickwrap/models/document_version.rb +163 -0
- data/lib/clickwrap/models/event.rb +743 -0
- data/lib/clickwrap/models/event_document.rb +79 -0
- data/lib/clickwrap/models/event_statement.rb +92 -0
- data/lib/clickwrap/models/external_action.rb +150 -0
- data/lib/clickwrap/models/integrity_attestation.rb +90 -0
- data/lib/clickwrap/models/legal_hold.rb +81 -0
- data/lib/clickwrap/models/policy_revision.rb +115 -0
- data/lib/clickwrap/models/presentation.rb +59 -0
- data/lib/clickwrap/models/receipt_access.rb +53 -0
- data/lib/clickwrap/models/recording_sequence.rb +21 -0
- data/lib/clickwrap/models/request_evidence.rb +378 -0
- data/lib/clickwrap/models/statement_identity_lock.rb +38 -0
- data/lib/clickwrap/models/statement_state.rb +130 -0
- data/lib/clickwrap/pending_receipt.rb +177 -0
- data/lib/clickwrap/policy.rb +283 -0
- data/lib/clickwrap/presentation_manifest.rb +210 -0
- data/lib/clickwrap/presenter.rb +716 -0
- data/lib/clickwrap/privacy.rb +419 -0
- data/lib/clickwrap/protected_outcome.rb +120 -0
- data/lib/clickwrap/receipt.rb +606 -0
- data/lib/clickwrap/receipt_html.rb +235 -0
- data/lib/clickwrap/receipt_verifier.rb +978 -0
- data/lib/clickwrap/reference.rb +44 -0
- data/lib/clickwrap/registration.rb +236 -0
- data/lib/clickwrap/registry.rb +54 -0
- data/lib/clickwrap/remediation_token.rb +155 -0
- data/lib/clickwrap/request_evidence_extractor.rb +590 -0
- data/lib/clickwrap/request_evidence_policy.rb +261 -0
- data/lib/clickwrap/retention/applier.rb +231 -0
- data/lib/clickwrap/retention/disposition.rb +221 -0
- data/lib/clickwrap/retention/planner.rb +502 -0
- data/lib/clickwrap/retention_class.rb +97 -0
- data/lib/clickwrap/reviewed_text.rb +28 -0
- data/lib/clickwrap/schema_requirements.rb +196 -0
- data/lib/clickwrap/services/authorize_external_action.rb +149 -0
- data/lib/clickwrap/services/load_policies.rb +69 -0
- data/lib/clickwrap/services/publish_documents.rb +251 -0
- data/lib/clickwrap/services/validate_policy_references.rb +166 -0
- data/lib/clickwrap/statement.rb +248 -0
- data/lib/clickwrap/subject_fingerprint.rb +28 -0
- data/lib/clickwrap/submission.rb +169 -0
- data/lib/clickwrap/system_actor.rb +31 -0
- data/lib/clickwrap/test_helpers.rb +676 -0
- data/lib/clickwrap/testing.rb +211 -0
- data/lib/clickwrap/trusted_proxy_configuration.rb +92 -0
- data/lib/clickwrap/verification.rb +504 -0
- data/lib/clickwrap/version.rb +12 -1
- data/lib/clickwrap/view_helpers.rb +190 -0
- data/lib/clickwrap/vocabulary.rb +294 -0
- data/lib/clickwrap.rb +497 -7
- data/lib/generators/clickwrap/document_generator.rb +164 -0
- data/lib/generators/clickwrap/hardening_generator.rb +177 -0
- data/lib/generators/clickwrap/install_generator.rb +1287 -0
- data/lib/generators/clickwrap/link_generator.rb +56 -0
- data/lib/generators/clickwrap/policy_generator.rb +118 -0
- data/lib/generators/clickwrap/templates/clickwrap_hardening.rb.erb +256 -0
- data/lib/generators/clickwrap/templates/clickwrap_policies.rb.erb +192 -0
- data/lib/generators/clickwrap/templates/create_clickwrap_external_action_tables.rb.erb +128 -0
- data/lib/generators/clickwrap/templates/create_clickwrap_integrity_tables.rb.erb +157 -0
- data/lib/generators/clickwrap/templates/create_clickwrap_presentation_tables.rb.erb +160 -0
- data/lib/generators/clickwrap/templates/create_clickwrap_request_evidence_tables.rb.erb +180 -0
- data/lib/generators/clickwrap/templates/create_clickwrap_retention_tables.rb.erb +174 -0
- data/lib/generators/clickwrap/templates/create_clickwrap_tables.rb.erb +568 -0
- data/lib/generators/clickwrap/templates/initializer.rb.erb +439 -0
- data/lib/generators/clickwrap/templates/link_clickwrap_event_migration.rb.erb +12 -0
- data/lib/generators/clickwrap/templates/policy.rb.erb +31 -0
- data/lib/generators/clickwrap/templates/policy_test.rb.erb +56 -0
- data/lib/generators/clickwrap/templates/privacy.md.erb +58 -0
- data/lib/generators/clickwrap/templates/terms.md.erb +49 -0
- data/lib/generators/clickwrap/upgrade_generator.rb +50 -0
- data/lib/generators/clickwrap/views_generator.rb +101 -0
- data/lib/tasks/clickwrap.rake +569 -0
- metadata +311 -16
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "erb"
|
|
4
|
+
# Require the sanitizer implementation without loading the Action View helper
|
|
5
|
+
# facade. Requiring `rails-html-sanitizer` while Action View is booting can
|
|
6
|
+
# create a circular require; these are the library's implementation entrypoints
|
|
7
|
+
# and do not install unrelated helper methods.
|
|
8
|
+
require "loofah"
|
|
9
|
+
require "rails/html/scrubbers"
|
|
10
|
+
require "rails/html/sanitizer"
|
|
11
|
+
|
|
12
|
+
module Clickwrap
|
|
13
|
+
# Safe, dependency-free rendering for the text formats most legal documents
|
|
14
|
+
# use. It intentionally favors faithful, readable text over clever Markdown
|
|
15
|
+
# interpretation: source characters are escaped, then placed in a <pre>
|
|
16
|
+
# element. Applications that want richer Markdown may supply a renderer, but
|
|
17
|
+
# its HTML still passes through #sanitize_html before Clickwrap stores it.
|
|
18
|
+
class DocumentRenderer
|
|
19
|
+
NAME = "clickwrap_safe_document_renderer"
|
|
20
|
+
VERSION = "1"
|
|
21
|
+
SANITIZER_NAME = "rails_safe_list_sanitizer"
|
|
22
|
+
SANITIZER_VERSION = "1"
|
|
23
|
+
|
|
24
|
+
SAFE_TAGS = %w[
|
|
25
|
+
a abbr b blockquote br cite code dd del details div dl dt em h1 h2 h3 h4 h5 h6
|
|
26
|
+
hr i ins kbd li mark ol p pre q s samp small span strong sub summary sup table
|
|
27
|
+
tbody td tfoot th thead tr u ul var
|
|
28
|
+
].freeze
|
|
29
|
+
SAFE_ATTRIBUTES = %w[href title lang dir class id colspan rowspan scope].freeze
|
|
30
|
+
|
|
31
|
+
def call(bytes, definition)
|
|
32
|
+
text = utf8_text!(bytes, definition)
|
|
33
|
+
|
|
34
|
+
html = case definition.media_type
|
|
35
|
+
when "text/html" then text
|
|
36
|
+
when "text/markdown", "text/plain", "application/json"
|
|
37
|
+
%(<pre class="clickwrap-document-source">#{ERB::Util.html_escape(text)}</pre>)
|
|
38
|
+
else
|
|
39
|
+
return nil
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
{
|
|
43
|
+
bytes: self.class.sanitize_html(html),
|
|
44
|
+
media_type: "text/html; charset=utf-8",
|
|
45
|
+
renderer_name: NAME,
|
|
46
|
+
renderer_version: VERSION,
|
|
47
|
+
sanitizer_name: SANITIZER_NAME,
|
|
48
|
+
sanitizer_version: SANITIZER_VERSION
|
|
49
|
+
}
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def self.sanitize_html(html)
|
|
53
|
+
safe_list_sanitizer_class.new.sanitize(
|
|
54
|
+
html.to_s,
|
|
55
|
+
tags: SAFE_TAGS,
|
|
56
|
+
attributes: SAFE_ATTRIBUTES
|
|
57
|
+
).to_s
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def self.safe_list_sanitizer_class
|
|
61
|
+
return Rails::HTML5::SafeListSanitizer if defined?(Rails::HTML5::SafeListSanitizer)
|
|
62
|
+
return Rails::HTML4::SafeListSanitizer if defined?(Rails::HTML4::SafeListSanitizer)
|
|
63
|
+
|
|
64
|
+
raise ConfigurationError,
|
|
65
|
+
"Clickwrap could not find Rails::HTML5::SafeListSanitizer or " \
|
|
66
|
+
"Rails::HTML4::SafeListSanitizer. Install a rails-html-sanitizer version that " \
|
|
67
|
+
"provides one of those supported safe-list sanitizers."
|
|
68
|
+
end
|
|
69
|
+
private_class_method :safe_list_sanitizer_class
|
|
70
|
+
|
|
71
|
+
private
|
|
72
|
+
|
|
73
|
+
def utf8_text!(bytes, definition)
|
|
74
|
+
text = bytes.to_s.dup.force_encoding(Encoding::UTF_8)
|
|
75
|
+
return text if text.valid_encoding?
|
|
76
|
+
|
|
77
|
+
raise DefinitionError,
|
|
78
|
+
"Document #{definition} cannot be rendered as text because its bytes are not valid UTF-8. " \
|
|
79
|
+
"Store binary source with :active_storage (or a byte-preserving resolver) and provide " \
|
|
80
|
+
"a reviewed renderer for the representation people should be offered."
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Clickwrap
|
|
4
|
+
module DocumentRenderers
|
|
5
|
+
# Optional Markdown rendering for the representation people are offered.
|
|
6
|
+
#
|
|
7
|
+
# The reference renderer deliberately escapes Markdown into a <pre> block:
|
|
8
|
+
# faithful, dependency-free, and ugly for a forty-page Terms document. This
|
|
9
|
+
# renderer produces real HTML instead, using whichever Markdown library the
|
|
10
|
+
# application already bundles — it is opt-in and it adds no dependency:
|
|
11
|
+
#
|
|
12
|
+
# Clickwrap.configure do |config|
|
|
13
|
+
# config.document_renderer = :markdown
|
|
14
|
+
# end
|
|
15
|
+
#
|
|
16
|
+
# Three engines are recognized, in order: Commonmarker (1.x and the older
|
|
17
|
+
# CommonMarker 0.x), Redcarpet, and Kramdown. An application whose Markdown
|
|
18
|
+
# dialect needs specific options can inject its own rendering instead and
|
|
19
|
+
# must name it, because the receipt records which renderer produced the
|
|
20
|
+
# offered bytes:
|
|
21
|
+
#
|
|
22
|
+
# config.document_renderer = Clickwrap::DocumentRenderers::Markdown.new(
|
|
23
|
+
# render_markdown_with: ->(markdown_text) { MyMarkdown.render(markdown_text) },
|
|
24
|
+
# engine_name: "my_markdown",
|
|
25
|
+
# engine_version: MyMarkdown::VERSION
|
|
26
|
+
# )
|
|
27
|
+
#
|
|
28
|
+
# A leading YAML front-matter block (the Jekyll/Sitepress convention) is
|
|
29
|
+
# stripped from the RENDERED representation only — the source digest still
|
|
30
|
+
# covers the exact file bytes, front matter included. Bundled engines'
|
|
31
|
+
# output always passes through the same safe-list sanitizer as the
|
|
32
|
+
# reference renderer, so a Markdown library's raw-HTML passthrough cannot
|
|
33
|
+
# smuggle markup into an offer. A host-supplied `render_markdown_with:`
|
|
34
|
+
# may additionally pass `sanitize_rendered_html: false` when its own pages
|
|
35
|
+
# serve the renderer's output verbatim: re-sanitizing reparses the HTML
|
|
36
|
+
# and changes its bytes, and a stored digest must describe exactly what
|
|
37
|
+
# readers were shown. The provenance records which choice was made.
|
|
38
|
+
# Non-Markdown documents delegate to the reference renderer unchanged.
|
|
39
|
+
class Markdown
|
|
40
|
+
NAME = "clickwrap_markdown_document_renderer"
|
|
41
|
+
VERSION = "1"
|
|
42
|
+
|
|
43
|
+
def initialize(render_markdown_with: nil, engine_name: nil, engine_version: nil,
|
|
44
|
+
sanitize_rendered_html: true)
|
|
45
|
+
@sanitize_rendered_html = sanitize_rendered_html != false
|
|
46
|
+
|
|
47
|
+
if !@sanitize_rendered_html && render_markdown_with.nil?
|
|
48
|
+
raise ConfigurationError,
|
|
49
|
+
"sanitize_rendered_html: false is only available with your own " \
|
|
50
|
+
"render_markdown_with. Clickwrap's bundled engines always sanitize; skipping " \
|
|
51
|
+
"sanitization is a promise about a rendering pipeline YOU own — that its " \
|
|
52
|
+
"output is exactly what your own pages already serve to readers."
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
if render_markdown_with
|
|
56
|
+
unless render_markdown_with.respond_to?(:call)
|
|
57
|
+
raise ConfigurationError,
|
|
58
|
+
"render_markdown_with must respond to call(markdown_text) and return HTML."
|
|
59
|
+
end
|
|
60
|
+
if engine_name.to_s.strip.empty?
|
|
61
|
+
raise ConfigurationError,
|
|
62
|
+
"A custom render_markdown_with needs an engine_name (and ideally an " \
|
|
63
|
+
"engine_version), because receipts record which renderer produced the " \
|
|
64
|
+
"bytes a person was offered."
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
@render_markdown = render_markdown_with
|
|
68
|
+
@engine_name = engine_name.to_s
|
|
69
|
+
@engine_version = engine_version.to_s.strip.empty? ? "unstated" : engine_version.to_s
|
|
70
|
+
else
|
|
71
|
+
@render_markdown, @engine_name, @engine_version = resolve_bundled_engine
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
@fallback = DocumentRenderer.new
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
attr_reader :engine_name, :engine_version
|
|
78
|
+
|
|
79
|
+
def call(bytes, definition)
|
|
80
|
+
return @fallback.call(bytes, definition) unless definition.media_type == "text/markdown"
|
|
81
|
+
|
|
82
|
+
text = utf8_text!(bytes, definition)
|
|
83
|
+
html = @render_markdown.call(FrontMatter.strip(text))
|
|
84
|
+
|
|
85
|
+
if @sanitize_rendered_html
|
|
86
|
+
{
|
|
87
|
+
bytes: DocumentRenderer.sanitize_html(html),
|
|
88
|
+
media_type: "text/html; charset=utf-8",
|
|
89
|
+
renderer_name: NAME,
|
|
90
|
+
renderer_version: "#{VERSION} (#{@engine_name} #{@engine_version})",
|
|
91
|
+
sanitizer_name: DocumentRenderer::SANITIZER_NAME,
|
|
92
|
+
sanitizer_version: DocumentRenderer::SANITIZER_VERSION
|
|
93
|
+
}
|
|
94
|
+
else
|
|
95
|
+
# Byte parity with the host's own pages is the point: re-sanitizing
|
|
96
|
+
# reparses and re-serializes the HTML, so the stored digest would
|
|
97
|
+
# describe bytes nobody was ever shown. The provenance says plainly
|
|
98
|
+
# that no sanitizer ran — the host's rendering pipeline owns safety.
|
|
99
|
+
{
|
|
100
|
+
bytes: html.to_s.dup.force_encoding(Encoding::UTF_8),
|
|
101
|
+
media_type: "text/html; charset=utf-8",
|
|
102
|
+
renderer_name: NAME,
|
|
103
|
+
renderer_version: "#{VERSION} (#{@engine_name} #{@engine_version})",
|
|
104
|
+
sanitizer_name: "none",
|
|
105
|
+
sanitizer_version: "host_renderer_output_stored_verbatim"
|
|
106
|
+
}
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
private
|
|
111
|
+
|
|
112
|
+
def resolve_bundled_engine
|
|
113
|
+
commonmarker_engine || redcarpet_engine || kramdown_engine ||
|
|
114
|
+
raise(ConfigurationError,
|
|
115
|
+
"config.document_renderer = :markdown needs a Markdown library, and none of " \
|
|
116
|
+
"the ones Clickwrap recognizes (commonmarker, redcarpet, kramdown) is " \
|
|
117
|
+
"available in this application. Add one of them to your Gemfile, or supply " \
|
|
118
|
+
"your own rendering with " \
|
|
119
|
+
"Clickwrap::DocumentRenderers::Markdown.new(render_markdown_with:, engine_name:).")
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def commonmarker_engine
|
|
123
|
+
attempt_require("commonmarker")
|
|
124
|
+
|
|
125
|
+
if defined?(::Commonmarker)
|
|
126
|
+
[->(text) { ::Commonmarker.to_html(text) }, "commonmarker", loaded_gem_version("commonmarker")]
|
|
127
|
+
elsif defined?(::CommonMarker)
|
|
128
|
+
[->(text) { ::CommonMarker.render_html(text) }, "commonmarker", loaded_gem_version("commonmarker")]
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def redcarpet_engine
|
|
133
|
+
attempt_require("redcarpet")
|
|
134
|
+
return unless defined?(::Redcarpet::Markdown)
|
|
135
|
+
|
|
136
|
+
# A fresh renderer per call: Redcarpet's renderer objects carry state
|
|
137
|
+
# between renders and are not safe to share across threads.
|
|
138
|
+
render = lambda do |text|
|
|
139
|
+
::Redcarpet::Markdown.new(
|
|
140
|
+
::Redcarpet::Render::HTML.new(with_toc_data: true),
|
|
141
|
+
tables: true, autolink: true, fenced_code_blocks: true, strikethrough: true,
|
|
142
|
+
footnotes: true, no_intra_emphasis: true
|
|
143
|
+
).render(text)
|
|
144
|
+
end
|
|
145
|
+
[render, "redcarpet", loaded_gem_version("redcarpet")]
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def kramdown_engine
|
|
149
|
+
attempt_require("kramdown")
|
|
150
|
+
return unless defined?(::Kramdown::Document)
|
|
151
|
+
|
|
152
|
+
[->(text) { ::Kramdown::Document.new(text).to_html }, "kramdown", loaded_gem_version("kramdown")]
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def attempt_require(name)
|
|
156
|
+
require name
|
|
157
|
+
rescue LoadError
|
|
158
|
+
nil
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def loaded_gem_version(name)
|
|
162
|
+
Gem.loaded_specs[name]&.version&.to_s || "unstated"
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def utf8_text!(bytes, definition)
|
|
166
|
+
text = bytes.to_s.dup.force_encoding(Encoding::UTF_8)
|
|
167
|
+
return text if text.valid_encoding?
|
|
168
|
+
|
|
169
|
+
raise DefinitionError,
|
|
170
|
+
"Document #{definition} cannot be rendered as Markdown because its bytes are " \
|
|
171
|
+
"not valid UTF-8."
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
end
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Clickwrap
|
|
4
|
+
module DocumentRenderers
|
|
5
|
+
# Byte parity with the application's own pages, by construction.
|
|
6
|
+
#
|
|
7
|
+
# Content-file Rails apps (Sitepress and friends) render their public
|
|
8
|
+
# legal pages through the `markdown-rails` gem, whose initializer
|
|
9
|
+
# registers a renderer for `.md` templates:
|
|
10
|
+
#
|
|
11
|
+
# MarkdownRails.handle :md do
|
|
12
|
+
# ApplicationMarkdown.new
|
|
13
|
+
# end
|
|
14
|
+
#
|
|
15
|
+
# When the same files are also Clickwrap documents, what people accept and
|
|
16
|
+
# what the public page serves must be the SAME BYTES — same renderer, same
|
|
17
|
+
# options, no re-sanitization pass changing entities behind the digest.
|
|
18
|
+
# Wiring that by hand means a lambda, an engine name, gem versions, and a
|
|
19
|
+
# parity test, all copied between applications. This renderer says it once:
|
|
20
|
+
#
|
|
21
|
+
# Clickwrap.configure do |config|
|
|
22
|
+
# config.document_renderer = :markdown_rails
|
|
23
|
+
# end
|
|
24
|
+
#
|
|
25
|
+
# At render time it asks markdown-rails for the exact renderer object the
|
|
26
|
+
# application's own `.md` pages go through, renders through it, stores the
|
|
27
|
+
# output verbatim (`sanitizer_name: "none"` in the provenance — safety
|
|
28
|
+
# stays where it already lives, in the pipeline every public page trusts),
|
|
29
|
+
# and records honest provenance: the renderer class the application
|
|
30
|
+
# registered, plus the markdown-rails and Markdown-engine gem versions.
|
|
31
|
+
#
|
|
32
|
+
# The handler is resolved LAZILY, at first render, never at configuration
|
|
33
|
+
# time: Rails initializers run alphabetically, so `clickwrap.rb` usually
|
|
34
|
+
# runs before `markdown.rb`, and an eager lookup would capture the stock
|
|
35
|
+
# default renderer instead of the application's own.
|
|
36
|
+
#
|
|
37
|
+
# Honest bound: the renderer is called directly, without a view context —
|
|
38
|
+
# exactly how a frozen document must render, because a snapshot cannot
|
|
39
|
+
# depend on the request it happened to be published during. Markdown that
|
|
40
|
+
# calls Rails view helpers (image_tag and friends) fails loudly at publish;
|
|
41
|
+
# documents like that need the explicit lambda form of
|
|
42
|
+
# Clickwrap::DocumentRenderers::Markdown instead.
|
|
43
|
+
class MarkdownRails
|
|
44
|
+
HANDLER_EXTENSION = :md
|
|
45
|
+
|
|
46
|
+
def initialize
|
|
47
|
+
# The gem must be bundled for this choice to ever work, and a missing
|
|
48
|
+
# bundle is a configuration mistake worth failing the boot for. The
|
|
49
|
+
# HANDLER may legitimately not be registered yet — see the class
|
|
50
|
+
# comment — so that half of the check waits until render time.
|
|
51
|
+
#
|
|
52
|
+
# The handler file loads first on purpose: markdown-rails' engine runs
|
|
53
|
+
# an on_load(:action_view) hook that references its own Handler
|
|
54
|
+
# constant, and when Action View is already loaded (it is, here) that
|
|
55
|
+
# hook fires mid-require, before the entry file has declared the
|
|
56
|
+
# autoload the hook depends on.
|
|
57
|
+
begin
|
|
58
|
+
require "markdown-rails/handler"
|
|
59
|
+
require "markdown-rails"
|
|
60
|
+
rescue LoadError
|
|
61
|
+
raise ConfigurationError,
|
|
62
|
+
"config.document_renderer = :markdown_rails needs the markdown-rails gem, and " \
|
|
63
|
+
"it is not in this application's bundle. Add `gem \"markdown-rails\"` (the " \
|
|
64
|
+
"Sitepress content stack), or render through your own pipeline with " \
|
|
65
|
+
"Clickwrap::DocumentRenderers::Markdown.new(render_markdown_with:, engine_name:)."
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
@fallback = DocumentRenderer.new
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def call(bytes, definition)
|
|
72
|
+
return @fallback.call(bytes, definition) unless definition.media_type == "text/markdown"
|
|
73
|
+
|
|
74
|
+
application_markdown_renderer_for(definition).call(bytes, definition)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# The provenance the application's registered renderer would produce
|
|
78
|
+
# right now — exposed so hosts and tests can ask "which renderer will my
|
|
79
|
+
# documents publish through?" in one call.
|
|
80
|
+
def engine_name
|
|
81
|
+
"markdown_rails/#{registered_renderer_class_name}"
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def engine_version
|
|
85
|
+
%w[markdown-rails redcarpet commonmarker kramdown].filter_map do |gem_name|
|
|
86
|
+
spec = Gem.loaded_specs[gem_name]
|
|
87
|
+
"#{gem_name} #{spec.version}" if spec
|
|
88
|
+
end.join(" / ").presence || "unstated"
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
private
|
|
92
|
+
|
|
93
|
+
# A fresh delegate per call, resolved from the registry markdown-rails
|
|
94
|
+
# keeps: publish-time work, and late binding keeps development reloads of
|
|
95
|
+
# the application's renderer class honest.
|
|
96
|
+
def application_markdown_renderer_for(definition)
|
|
97
|
+
renderer = registered_renderer(definition)
|
|
98
|
+
|
|
99
|
+
Markdown.new(
|
|
100
|
+
render_markdown_with: ->(markdown_text) { renderer.renderer.render(markdown_text) },
|
|
101
|
+
engine_name: "markdown_rails/#{renderer.class.name}",
|
|
102
|
+
engine_version: engine_version,
|
|
103
|
+
sanitize_rendered_html: false
|
|
104
|
+
)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def registered_renderer(definition = nil)
|
|
108
|
+
handler = ::MarkdownRails::Handler.handler_for(HANDLER_EXTENSION)
|
|
109
|
+
if handler.nil?
|
|
110
|
+
raise ConfigurationError,
|
|
111
|
+
"config.document_renderer = :markdown_rails found no markdown-rails handler " \
|
|
112
|
+
"registered for .#{HANDLER_EXTENSION}#{" while rendering document #{definition}" if definition}. " \
|
|
113
|
+
"Register one in config/initializers/markdown.rb — " \
|
|
114
|
+
"`MarkdownRails.handle :md { ApplicationMarkdown.new }` — so Clickwrap can " \
|
|
115
|
+
"render documents through the exact pipeline your own pages use."
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
handler.create_renderer
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def registered_renderer_class_name
|
|
122
|
+
registered_renderer.class.name
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|